【Linux】线程封装与互斥(万字)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
目录
文章目录
前言
C++多线程的用法
对原生线程进行一次封装
理解pthread线程
Linux线程互斥
进程线程间的互斥相关背景概念
互斥量mutex
操作共享变量会有问题的售票系统代码
互斥量的接口
初始化互斥量
销毁互斥量
互斥量加锁和解锁
改进上面的售票系统:
方法一:定义一个静态或全局的锁变量gmutex
方法二:定义一个局部的锁
方法三: 临时对象, RAII风格的加锁和解锁(构造加锁,析构解锁)
互斥量实现原理探究
可重入VS线程安全
概念
常见的线程不安全的情况
常见的线程安全的情况
常见不可重入的情况
常见可重入的情况
可重入与线程安全联系
可重入与线程安全区别
总结
前言
世上有两种耀眼的光芒,一种是正在升起的太阳,一种是正在努力学习编程的你!一个爱学编程的人。各位看官,我衷心的希望这篇博客能对你们有所帮助,同时也希望各位看官能对我的文章给与点评,希望我们能够携手共同促进进步,在编程的道路上越走越远!
提示:以下是本篇文章正文内容,下面案例可供参考
C++多线程的用法
#include <thread> // C++多线程所对应的头文件
#include <unistd.h>void threadrun(int num)
{while(num){std::cout << "I am a thread, num: " << num << std::endl;sleep(1);}
}
int main()
{std::thread t1(threadrun, 10);std::thread t2(threadrun, 10);std::thread t3(threadrun, 10);std::thread t4(threadrun, 10);std::thread t5(threadrun, 10);while(true){std::cout << "I am a main thread "<< std::endl;sleep(1);}t1.join();t2.join();t3.join();t4.join();t5.join();return 0;
}
对原生线程进行一次封装
C++11的多线程,是对原生线程的封装,所以在编译时,要链接上 -lpthread原生线程库。
为什么要封装呢?
- 语言的跨平台性。在Linux当中,我们所使用的C++11的多线程,用的是Linux的pthread库;如果是在Windows当中,用的是Windows的原始对应的系统调用创建线程的接口,C++在给Linux和Windows当中提供的标准库是不一样的,C++给我们提供的标准库编译出来,在Windows中是Windows版本的,在Linux中是Linux版本的,所以对应的库不一样,但是代码是一样的。
Windows当中还要不要包含pthread库呢?
- 不需要。语言具有跨平台性。
其它语言呢?
- 大部分的语言要在Linux下跑多线程,必须要用原生线程库,因为pthread库是Linux提供多线程的底层唯一方式。
thread.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>// C++线程的头文件namespace ThreadModule
{// using就相当于typedef,using定义了一个新类型std::function<void(T&)>,是一个新语法template<typename T>using func_t = std::function<void(T)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:void Excute(){_func(_data);}public:Thread(func_t<T> func, T data, const std::string& name = "none-name"): _func(func), _data(data), _threadname(name), _stop(true){}// 方法:static修饰的函数中的参数是没有this指针的// 因为没有this指针,所以该函数里面也无法调用该类的成员对象了static void* threadroutine(void* args) // 类成员函数,形参是有this指针的!!{Thread<T>* self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;}bool Start(){// pthread_create()函数中的参数3的函数指针,要求的参数类型是void*,// 而threadroutine()函数是类成员函数,有一个this指针,所以调不了该函数int n = pthread_create(&_tid, nullptr, threadroutine, this);// 把当前对象this传threadroutine()if (!n){_stop = false;return true;}else{return false;}}void Detach(){if (!_stop){pthread_detach(_tid);}}void Join(){if (!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data; // 模板的参数类型T就直接是指针了func_t<T> _func;bool _stop;};
} #endif
testThread.cc
using namespace ThreadModule;void print(int &cnt)
{while (cnt){std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;sleep(1);}
}const int num = 10;int main()
{std::vector<Thread<int> > threads;// 1. 创建一批线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(print, 10, name);}// 2. 启动 一批线程for (auto &thread : threads){thread.Start();}// 3. 等待一批线程for (auto &thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}// Thread<int> t1(print, 10);// t1.Start();// std::cout << "name: " << t1.name() << std::endl;// t1.Join();return 0;
}
理解pthread线程
- 二进制代码刚运行时,pthread_t tid;只有一个进程,等执行到pthread_create()代码时,才创建出了新线程,从操作层面上:创建、终止、等待线程的接口都是在库当中实现的,库是将轻量级进程做了封装,所以给上层用户提供的就是库当中的方法,所以把我们用的线程叫做用户级线程。
- 线程库首先要映射到当前进程的地址空间中(堆栈之间的共享区)!
- 线程的管理工作要由库来进行管理!
那么库要如何管理线程呢?
- 先描述,再组织!
- 库里面要有描述线程的结构体,以及把所有的线程都组织在一起,线程的控制块:struct_pthread,一般我们喜欢将线程的控制块叫做struct_tcb,只不过Linux不提供struct_tcb,创建一个线程就为我们在库当中维护一个控制块结构,而每一个控制块结构的起始地址,就叫做线程的tid。tid的本质就是一个堆栈之间共享区的线程库中的控制块结构的起始地址(虚拟地址)。
- 线程的整体结构:struct_pthread、线程局部存储、线程栈。
- 动态库是共享库,多个进程,每一个进程都创建多个线程,每个进程的进程地址空间堆栈之间的共享区都是同一个动态库。
- 全局变量在进程地址空间当中的已初始化数据区。
- 线程局部存储:不能用来存储stl的容器数据,只能用来存储内置类型,因为它是一个C语言的库,不认识其它容器的数据。
线程库是不是磁盘当中的一个普通文件呢?
- 线程库是一个动态库,也是磁盘当中的一个文件。
执行流(task_struct)是如何找到线程的线程栈的?
man clone
轻量级进程是Linux当中线程实现的底层方案,但真正线程实现是在库当中实现的。
Linux线程互斥
进程线程间的互斥相关背景概念
- 临界资源:多线程执行流共享的资源就叫做临界资源
- 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
- 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
- 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成
- 当共享资源做了保护,就叫做临界资源。
互斥量mutex
- 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。
- 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。
- 多个线程并发的操作共享变量,会带来一些问题。
操作共享变量会有问题的售票系统代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
void* route(void* arg)
{char* id = (char*)arg;while (1) {if (ticket > 0) {usleep(1000);printf("%s sells ticket:%d\n", id, ticket);ticket--;}else {break;}}
}
int main(void)
{pthread_t t1, t2, t3, t4;pthread_create(&t1, NULL, route, "thread 1");pthread_create(&t2, NULL, route, "thread 2");pthread_create(&t3, NULL, route, "thread 3");pthread_create(&t4, NULL, route, "thread 4");pthread_join(t1, NULL);pthread_join(t2, NULL);pthread_join(t3, NULL);pthread_join(t4, NULL);
}
一次执行结果:
thread 4 sells ticket : 100
...
thread 4 sells ticket : 1
thread 2 sells ticket : 0
thread 1 sells ticket : -1
thread 3 sells ticket : -2
抢票的结构最终出现了负数,造成了数据不一致,为什么?
- 因为g_tickets是一个全局的变量,这个全局的变量是没有被保护起来的,并且对全局变量_tickets的判断不是原子的。
- 当_tickets == 1时,多个线程并发的判断,让很多线程都进入抢票逻辑。
- 全局变量g_tickets是在内存当中的,当线程1执行if语句时,要进行票数的判断,判断是逻辑运算,必须在CPU内部运行,此时内存中的票数数据拷贝到CPU的寄存器当中,执行到usleep语句时,线程1被切换了出去,因为寄存器只有一套,所以为了保存线程1的上下文数据,数据被线程1带走了;
- 此时切换到线程2执行判断逻辑,与线程1的情况一样也被切换走了,线程3和4都是如此;
- 那么当线程1再次切换回来的时候,要重新在内存中读取数据,打印并--操作;
- _tickets--(不是原子的)等价于_tickets = _tickets - 1;--操作数据改变,会影响原生内存中的数据,因为会写回内存;
- 那么其它3个线程也都进入了抢票逻辑,所以会读取内存中的数据,打印并--操作,所以会打印出了负数的情况。
-- 操作并不是原子操作,而是对应三条汇编指令:
- load :将共享变量ticket从内存加载到寄存器中
- update : 更新寄存器里面的值,执行-1操作
- store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
- 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
- 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
- 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
互斥量的接口
初始化互斥量
初始化互斥量有两种方法:
- 方法1,如果你定义的锁是静态的或者是全局的:那么这个锁可以不用init初始化和destroy销毁;你可以直接定义一个锁,并用PTHREAD_ MUTEX_ INITIALIZER宏对其进行初始化。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
- 方法2,动态分配:
如果这把锁是一个局部的:建议init初始化和destroy销毁
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict
attr);
参数:
mutex:要初始化的互斥量
attr:NULL
尝试的去申请锁:
int pthread_mutex_trylock(pthread_mutex_t *mutex);
尝试的去申请锁,跟申请锁成功和函数调用失败是一样的,但是申请锁失败了,不会阻塞,会立马出错返回。
销毁互斥量
销毁互斥量需要注意:
- 使用 PTHREAD_ MUTEX_ INITIALIZER初始化的互斥量不需要销毁
- 不要销毁一个已经加锁的互斥量
- 已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调用 int pthread_mutex_lock(pthread_mutex_t *mutex) 时,可能会遇到以下情况:
- 申请成功:函数就会返回,允许你继续向后运行;
- 申请锁失败:函数就会阻塞,不允许你继续向后运行;
- 函数调用失败:出错返回,比如:申请锁的对象已经被释放了
改进上面的售票系统:
方法一:定义一个静态或全局的锁变量gmutex
// 抢票逻辑
#include <iostream>
#include <vector>
#include <mutex> // C++11里面锁的头文件
#include "Thread.hpp"using namespace ThreadModule;// 数据不一致
int g_tickets = 10000; // 共享资源,没有保护的
// 线程的数据类型
class ThreadData
{
public:ThreadData(int& tickets, const std::string& name): _tickets(tickets), _name(name), _total(0)){}~ThreadData(){}public:int& _tickets; // 所有的线程,最后都会引用同一个全局的g_ticketsstd::string _name;int _total;
};// 方法一:定义一个静态或全局的锁变量gmutex
// gmutex中的g表示globle的意思
pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;// 定义一个锁// 线程执行的方法void route(ThreadData *td){// 加锁while (true){// 访问临界资源的代码,叫做临界区!// 我们加锁,本质就是把多线程的并行执行变为串行执行 --- 加锁的力度要越细越好pthread_mutex_lock(&gmutex); // 加锁 : 竞争锁是自由竞争的,竞争锁的能力太强的线程,会导致其他线程抢不到锁 --- 造成了其他线程的饥饿问题!if (td->_tickets > 0) {// 模拟一次抢票的逻辑usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets);td->_tickets--; pthread_mutex_unlock(&gmutex); // 解锁 法一:td->_total++;}else{pthread_mutex_unlock(&gmutex); // 解锁 法一:break;}}// 解锁}const int num = 4;
int main()
{std::cout << "main: &tickets: " << &g_tickets << std::endl;// std::mutex mutex;// C++11的做法,不用初始化,因为它有构造函数std::vector<Thread<ThreadData*>> threads;std::vector<ThreadData*> datas;// 每个线程抢了多少张票// 1. 创建一批线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);ThreadData* td = new ThreadData(g_tickets, name);threads.emplace_back(route, td, name);datas.emplace_back(td);}// 2. 启动 一批线程for (auto& thread : threads){thread.Start();}// 3. 等待一批线程for (auto& thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}sleep(1);// 4. 输出统计数据for (auto data : datas){std::cout << data->_name << " : " << data->_total << std::endl;delete data;}// pthread_mutex_destroy(&mutex);return 0;
}
方法二:定义一个局部的锁
// 抢票逻辑
#include <iostream>
#include <vector>
#include <mutex> // C++11里面锁的头文件
#include "Thread.hpp"using namespace ThreadModule;// 数据不一致
int g_tickets = 10000; // 共享资源,没有保护的
// 线程的数据类型
class ThreadData
{
public:ThreadData(int& tickets, const std::string& name, pthread_mutex_t &mutex): _tickets(tickets), _name(name), _total(0), _mutex(mutex){}~ThreadData(){}public:int& _tickets; // 所有的线程,最后都会引用同一个全局的g_ticketsstd::string _name;int _total;pthread_mutex_t& _mutex;
};// 线程执行的方法void route(ThreadData *td){// 加锁while (true){ // 方法二:加锁pthread_mutex_lock(&td->_mutex);if (td->_tickets > 0) {// 模拟一次抢票的逻辑usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); td->_tickets--; pthread_mutex_unlock(&td->_mutex); // 解锁 法二:td->_total++;}else{pthread_mutex_unlock(&td->_mutex); // 解锁 法二:break;}}// 解锁}const int num = 4;
int main()
{// 方法二:定义一个局部的锁pthread_mutex_t mutex;pthread_mutex_init(&mutex, nullptr);// 锁的属性设为nullptr// std::mutex mutex;// C++11的做法,不用初始化,因为它有构造函数std::vector<Thread<ThreadData*>> threads;std::vector<ThreadData*> datas;// 每个线程抢了多少张票// 1. 创建一批线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);ThreadData* td = new ThreadData(g_tickets, name, mutex);// 把局部的锁,以参数的形式传递到线程内部,而不是以全局的形式threads.emplace_back(route, td, name);datas.emplace_back(td);}// 2. 启动 一批线程for (auto& thread : threads){thread.Start();}// 3. 等待一批线程for (auto& thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}sleep(1);// 4. 输出统计数据for (auto data : datas){std::cout << data->_name << " : " << data->_total << std::endl;delete data;}pthread_mutex_destroy(&mutex);return 0;
}
方法三: 临时对象, RAII风格的加锁和解锁(构造加锁,析构解锁)
LockGuard.hpp
#ifndef __LOCK_GUARD_HPP__
#define __LOCK_GUARD_HPP__#include <iostream>
#include <pthread.h>class LockGuard
{
public:LockGuard(pthread_mutex_t* mutex) :_mutex(mutex){pthread_mutex_lock(_mutex); // 构造加锁}~LockGuard(){pthread_mutex_unlock(_mutex);}
private:pthread_mutex_t* _mutex;
};#endif
void route(ThreadData* td)
{while (true){{ // 担心就用这个LockGuard guard(&td->_mutex); // 临时对象, RAII风格的加锁和解锁(构造加锁,析构解锁)// td->_mutex.lock();C++11的做法// std::lock_guard<std::mutex> lock(td->_mutex);C++11中也封装了lock_guardif (td->_tickets > 0) // 1{usleep(1000);printf("%s running, get tickets: %d\n", td->_name.c_str(), td->_tickets); // 2td->_tickets--; // 3td->_total++;// td->_mutex.unlock();C++11}else{// td->_mutex.unlock();break;}}}
}
什么是原子的?
- 一条语句将来被汇编之后,只有一条汇编。
互斥量实现原理探究
- 经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题
- 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一 个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下
互斥的底层实现:
- 假设CPU中有一个寄存器%al,锁相当于内存当中的整型变量;
- 假设刚开始把锁初始化为1,线程1此时要申请锁,它把0放入%al的寄存器中,再把寄存器中的值和锁变量中的值进行交换,就完成了加锁,若线程1加锁成功,那么线程1就执行它的代码;
- 假设线程1在执行完第二条语句时,线程1被切换成线程2,线程1被切换走时,会把寄存器中的数据带走;
- 线程2开始申请锁,线程2调用pthread_mutex_lock()函数从0开始申请锁,将0放入寄存器中,再与内存中的值做交换,因为都是0,所以申请锁失败,挂起等待,将数据带走;
- 等线程1回来时,将之前带走的数据恢复到寄存器中,加锁成功;
- 成功了之后,还要解锁,将mutex变量重新置为1。
- 寄存器内部的数据不属于CPU,它属于当前线程的硬件上下文
- 临界区内部,正在访问临界区的线程,可以被OS切换调度,被切换出去的时候,把锁也带走了。申请锁成功的线程1正在访问临界区,即使线程1被挂起了,其它任何线程都进不来临界区,那么临界区对于其它的线程来说就是原子的,该线程是安全的。
- 互斥是为了解决数据安全的问题;同步是为了解决资源被充分利用的问题。
- 线程被切换的时机是随机的。
- 交换的本质:不是拷贝到寄存器,而是所有线程在争锁的时候,只有一个1。
- 交换的时候,只有一条汇编 --- 原子的。
- CPU寄存器硬件只有一套,但是CPU寄存器内部的数据是线程的硬件上下文。
- 数据在内存里,所有线程都能访问,属于共享的。但是如果转移到CPU内部寄存器中,就属于一个线程私有了。
- 互斥:任何时刻只允许一个线程进行访问。
线程互斥:
- 保护并不是把临界资源怎么样,而是保护多个线程都会执行访问临界资源的代码,我们要保护的是临界区。
可重入VS线程安全
概念
- 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
- 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
可重入与线程安全区别
- 可重入函数是线程安全函数的一种
- 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
- 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生 死锁,因此是不可重入的。
总结
好了,本篇博客到这里就结束了,如果有更好的观点,请及时留言,我会认真观看并学习。
不积硅步,无以至千里;不积小流,无以成江海。
相关文章:

【Linux】线程封装与互斥(万字)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 文章目录 前言 C多线程的用法 对原生线程进行一次封装 理解pthread线程 Linux线程互斥 进程线程间的互斥相关背景概念 互斥量mutex 操作共享变量会有问题的售票…...

5分钟教你部署MySQL8.0环境
此方法基于Windows操作系统! 一、在MySQL官网单击downloads(下载)MySQLhttps://www.mysql.com/cn/ 选择在Windows操作系统下载 二、选择合适的版本 推荐下载第二种,安装时离线安装即可 三、安装MySQL8.0 1、找到MySQL下载完成…...

LLM应用:传统NLP任务
LLM出来以后,知乎上就出现了“传统NLP已死”的言论,但是传统NLP真的就被扔进历史的垃圾桶了吗? 其实,尽管LLM具有出色的通用能力,但仍然无法有效应对低资源领域的自然语言处理任务,如小语种翻译。为了更好地…...

基于Hadoop平台的电信客服数据的处理与分析③项目开发:搭建Kafka大数据运算环境---任务11:基础环境准备
任务描述 任务主要是安装配置基础环境,主要内容包括: 1、安装java Kafka和ZooKeeper都需要安装Java环境,推荐至少Java8及以上版本 2、安装ZooKeeper ZooKeeper是Kafka集群的必要组件 3、安装kafka Kafka版本包括使用的scala语言版本和kafka版…...

Golang中swtich中如何强制执行下一个代码块
switch 语句中的 case 代码块会默认带上 break,但可以使用 fallthrough 来强制执行下一个 case 代码块。 package mainimport ("fmt" )func main() {isSpace : func(char byte) bool {switch char {case : // 空格符会直接 break,返回 false…...

读书笔记-Java并发编程的艺术-第4章(Java并发编程基础)-第2节(启动和终止线程)
文章目录 4.2 启动和终止线程4.2.1 构造线程4.2.2 启动线程4.2.3 理解中断4.2.4 过期的suspend()、resume()和stop()4.2.5 安全地终止线程 4.2 启动和终止线程 在前面章节的示例中通过调用线程的start()方法进行启动,随着run()方法的执行完毕,线程也随之…...

通俗大白话理解Docker
什么是Docker Docker本质上是一种容器化技术,用于将应用程序及其所有依赖打包到一个标准化的单元中。这些单元(容器)可以在任何运行Docker的机器上运行。每个容器是相互隔离的,具有自己的文件系统、网络和进程空间。 以下是大白话…...

题解:CF1981C(Turtle and an Incomplete Sequence)
题解:CF1981C(Turtle and an Incomplete Sequence) Part 1:题意理解 地址链接:CF、洛谷。题面翻译:给定一个长度为 n n n 的序列 a a a,其中有一些元素未知,用 − 1 -1 −1 表示…...

Swift 中强大的 Key Paths(键路径)机制趣谈(上)
概览 小伙伴们可能不知道:在 Swift 语言中隐藏着大量看似“其貌不扬”实则却让秃头码农们“高世骇俗”,堪称卧虎藏龙的各种秘技。 其中,有一枚“不起眼”的小家伙称之为键路径(Key Paths)。如若将其善加利用ÿ…...

(十二)纹理和采样
纹理 在绘制三角形的过程中,将图片贴到三角形上进行显示的过程,就是纹理贴图的过程 uv坐标 如果如果图片尺寸和实际贴图尺寸不一致,就会导致像素不够用了的问题 纹理与采样 纹理对象(Texture):在GPU端,用来以一…...

QT创建地理信息shp文件编辑器shp_editor
空闲之余创建一个简单的矢量shp文件编辑器,加深对shp文件的理解。 一、启动程序 二、打开shp文件 三、显示shp文件的几何图形 四、双击右边表格中的feature,主窗体显示选中feature的各个节点。 五、鼠标在主窗体中选中feature的节点,按鼠标左…...

解析Kotlin中扩展函数与扩展属性【笔记摘要】
1.扩展函数 1.1 作用域:扩展函数写的位置不同,作用域就也不同 扩展函数可以写成顶层函数(Top-level Function),此时它只属于它所在的 package。这样你就能在任何类里使用它: package com.rengwuxianfun …...

【Java学习笔记】java图形界面编程
在前面的章节中,我们开发运行的应用程序都没有图形界面,但是很多应用软件,如Windows下的Office办公软件、扑克牌接龙游戏软件、企业进销存ERP系统等,都有很漂亮的图形界面。素以需要我们开发具有图形界面的软件。 Java图形界面编程…...

STM32入门笔记(03): ADC(SPL库函数版)(2)
A/D转换的常用技术有逐次逼近式、双积分式、并行式和跟踪比较式等。目前用的较多的是前3种。 A/D转换器的主要技术指标 转换时间 分辨率 例如,8位A/D转换器的数字输出量的变化范围为0~255,当输入电压的满刻度为5V时,数字量每变化…...

2024年7月2日 (周二) 叶子游戏新闻
老板键工具来唤去: 它可以为常用程序自定义快捷键,实现一键唤起、一键隐藏的 Windows 工具,并且支持窗口动态绑定快捷键(无需设置自动实现)。 卸载工具 HiBitUninstaller: Windows上的软件卸载工具 经典名作30周年新篇《恐怖惊魂夜…...

如何使用Spring Boot Profiles进行环境配置管理
如何使用Spring Boot Profiles进行环境配置管理 大家好,我是免费搭建查券返利机器人省钱赚佣金就用微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨如何利用Spring Boot Profiles来管理不同环境…...

Java错题归纳(二)
1、若有如下接口A的定义,下列哪些类下确实现了该接口:C interface A { void method1(int i); void method2(int j); } A class B implements A{ void method1( ) { } void method2( ) { } } B class B implements A { void method1(int i ) { }…...

Grafana面试题精选和参考答案
目录 Grafana是什么以及它的主要应用场景 Grafana支持的数据源 Grafana的体系结构及主要组件 Grafana如何实现数据的可视化和监控 Grafana支持的图表类型 如何在Grafana中创建和编辑仪表盘 Grafana的查询编辑器功能 Grafana支持的认证方式 Grafana的性能调优建议 Gra…...

Node版本管理工具 fnm 安装使用
fnm 是一个基于 Rust 开发的 Node 版本管理工具,它的目标是提供一个快速、简单且可靠的方式来管理 Node.js 的不同版本。同时,它是跨平台的,支持 macOS、Linux、Windows。🚀 Fast and simple Node.js version manager, built in R…...

vector模拟实现【C++】
文章目录 全部的实现代码放在了文章末尾准备工作包含头文件定义命名空间和类类的成员变量 迭代器迭代器获取函数 构造函数默认构造使用n个值构造迭代器区间构造解决迭代器区间构造和用n个值构造的冲突拷贝构造 析构函数swap【交换函数】赋值运算符重载emptysize和capacityopera…...

《每天5分钟用Flask搭建一个管理系统》第11章:测试与部署
第11章:测试与部署 11.1 测试的重要性 测试是确保应用质量和可靠性的关键步骤。它帮助开发者发现和修复错误,验证功能按预期工作。 11.2 Flask测试客户端的使用 Flask提供了一个测试客户端,可以在开发过程中模拟请求并测试应用的响应。 …...

Landsat数据从Collection1更改为Collection2
目录 问题解决 问题 需要注意!您使用的是废弃的陆地卫星数据集。为确保功能持续,请在2024年7月1日前更新。 在使用一些以前的代码时会遇到报错,因为代码里面用的是老的数据集 解决 对于地表反射率SR,需要在name中,将C01换为C02&…...

《每天5分钟用Flask搭建一个管理系统》第12章:安全性
第12章:安全性 12.1 Web应用的安全威胁 Web应用面临的安全威胁包括但不限于跨站脚本攻击(XSS)、SQL注入、跨站请求伪造(CSRF)、不安全的直接对象引用(IDOR)等。 12.2 Flask-Talisman扩展的使…...

Unity之创建与导出PDF
内容将会持续更新,有错误的地方欢迎指正,谢谢! Unity之创建与导出PDF TechX 坚持将创新的科技带给世界! 拥有更好的学习体验 —— 不断努力,不断进步,不断探索 TechX —— 心探索、心进取! 助力快速…...

【Android面试八股文】优化View层次过深问题,选择哪个布局比较好?
优化深层次View层次结构的问题,选择合适的布局方式是至关重要的。以下是几点建议: 使用ConstraintLayout:ConstraintLayout是Android开发中推荐的布局,能够有效减少嵌套,提高布局性能。相比RelativeLayout,…...

什么是带有 API 网关的代理?
带有 API 网关的代理服务显著提升了用户体验和性能。特别是对于那些使用需要频繁创建和轮换代理的工具的用户来说,使用 API 可以节省大量时间并提高效率。 了解 API API,即应用程序编程接口,是服务提供商和用户之间的连接网关。通过 API 连接…...

sql拉链表
1、定义:维护历史状态以及最新数据的一种表 2、使用场景 1、有一些表的数据量很大,比如一张用户表,大约1亿条记录,50个字段,这种表 2.表中的部分字段会被update更新操作,如用户联系方式,产品的…...

STM32CubeMX实现矩阵按键(HAL库实现)
功能描述: 实现矩阵按键验证,将矩阵按键的按键值,通过串口显示,便于后面使用。 实物图 原理图: 编程原理: 原理很简单,就是通过循环设置引脚为低电平,另外引脚扫描读取电平值&…...

mmdetection3D指定版本安装指南
1. 下载指定版本号 选择指定版本号下载mmdetection3d的源码,如这里选择的是0.17.2版本 git clone https://github.com/open-mmlab/mmdetection3d.git -b v0.17.22. 安装 cd mmdetection3d安装依赖库 pip install -r requirment.txt编译安装 pip install -v e .…...

SQLMap工具详解与SQL注入防范
SQLMap工具详解与SQL注入防范 大家好,我是微赚淘客系统3.0的小编,也是冬天不穿秋裤,天冷也要风度的程序猿!今天我们将深入探讨SQLMap工具的详细使用方法以及如何防范SQL注入攻击。 SQL注入简介 SQL注入是一种常见的安全漏洞&am…...