• C++11 - 7 - 线程库


    c++

    前言:

    Vue框架:从项目学Vue
    OJ算法系列:神机百炼 - 算法详解
    Linux操作系统:风后奇门 - linux

    回顾<pthread.h>:

    创建线程

    • pthread_create():
    #include 
    void *func(void *args){
    	char* name = (char*)args;
    	cout<<name<<pthread_self()<<endl;
    	cout<<"线程任务函数"<<endl;
    }
    int main(){
    	pthread_t tid;
    	if(pthread_create(&tid, nullptr, func, "linux线程 : ") < 0){
    		cout<<"线程创建失败"<<endl;
    	}
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    线程等待:

    • pthread_join()线程等待:
    #include 
    void *func(void *args){
    	cout<<"线程任务函数"<<endl;
    }
    int main(){
    	pthread_t tid;
    	pthread_create(&tid, nullptr, func, nullptr) < 0);
    	pthread_join(tid, nullptr);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • pthread_detach()自动分离:
    #include 
    void *func(void *args){
    	pthread_detach(pthread_self());
    	cout<<"线程任务函数"<<endl;
    }
    int main(){
    	pthread_t tid;
    	pthread_create(&tid, nullptr, func, nullptr) < 0);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    终止线程:

    • 自然return:
    #include 
    void *func(void *args){
    	cout<<"线程任务函数"<<endl;
    	//return (void*)&"0";
    	return (void*)"0";
    }
    int main(){
    	pthread_t tid;
    	pthread_create(&tid, nullptr, func, nullptr) < 0);
    	void* exit_code;
    	pthread_join(tid, &exit_code);
    	cout<<*(int*)exit_code<<endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 结果:
      不论是return “0” 还是 return &“0”
      退出码都是48
    • 自己pthread_exit():
    #include 
    void *func(void *args){
    	pthread_detach(pthread_self());
    	cout<<"线程任务函数"<<endl;
    	int *p = new int(0);
    	pthread_exit((void*)p);
    }
    int main(){
    	pthread_t tid;
    	pthread_create(&tid, nullptr, func, nullptr) < 0);
    	void* ret;
    	pthread_join(func, &ret);
    	cout<<*(int*)ret<<endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 他人pthread_cancel():

      被cancel终止的线程,退出码都是常量PTHREAD_CANCELED

    #include 
    void *func(void *args){
    	pthread_detach(pthread_self());
    	while(1){
    		cout<<"线程任务函数"<<endl;
    		sleep(1);
    	}
    }
    int main(){
    	pthread_t tid;
    	pthread_create(&tid, nullptr, func, nullptr) < 0);
    	sleep(3);
    	pthread_cancel(tid);
    	void* ret;
    	pthread_join(tid, &ret);
    	cout<<*(int*)ret<<endl;
    	if(*(int*)ret == PTHREAD_CANCELED){
          printf("thread PTHREAD_CANCELED\n");           
        }else{
          printf("thread isn't pthread_canceled\n");
        }
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    官方线程库:

    • 虽然上述的 #include 我们在linux下使用的已经很熟练了

      但是毕竟该库不是一个官方库,未自动添加到环境变量中

      别忘了我们使用g++ / gcc编译时,总要携带命令行参数-lpthread

    • 这个易忘问题终于在C++11得到了解决,官方线程库 #include < thread>来了

    线程创建

    • 创建线程本质是创建一个thread类的对象:
    #include 
    thread t(可调用对象,可调用对象参数);
    
    • 1
    • 2
    • 可调用对象:
      1. 函数 / 函数指针
      2. 仿函数类对象
      3. lambda表达式

    函数指针:

    • 最基本的用法:
    #include 
    #include 
    using namespace std;
    void func(int a, int b){
    	cout<<a <<" " <<b <<endl;
    }
    int main(){
    	thread t(func, 1, 2);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 运行报错:t1erminate called without an active exception
    • 错因:子线程创建后,主线程未等待子线程运行完成,就终止了子线程
    • 对策:加join()阻塞主线程:
    #include 
    #include 
    using namespace std;
    void func(int a, int b){
    	cout<<a <<"   " <<b <<endl;
    }
    int main(){
    	thread t(func, 1, 2);
    	t.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    仿函数类:

    • 仿函数使用struct 或 class public均可:
    #include 
    #include 
    using namespace std;
    struct Func{
    	void operator()(int a, double b){
    		cout<<a <<"  " <<b <<endl;
    	}
    };
    int main(){
    	thread t(Func(), 1, 2);
    	t.join();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    lambda:

    形参列表:
    • 使用形参列表时,传参方式有两种:
      1. 传值
      2. 传指针
      3. 传引用 + ref()函数
    • 对,没错,直接传引用会报错(visual studio13不报错,直接改为传值)
    传值:
    • lambda传值:发生了变量拷贝
    #include 
    #include 
    using namespace std;
    int main(){
    	int x = 1, y = 2;
    	thread t([](int a, int b){
    		cout<<a <<" " <<b <<endl;
    	}, x, y);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    传指针:
    • 传指针不加mutable也可以修改所指向变量值:
    #include 
    #include 
    using namespace std;
    int main(){
    	int x = 1, y = 2;
    	thread t([](int *a, int *b){
    		*a = 3;
    		cout<<"子线程 :" <<*a <<" " <<*b <<endl;
    	}, &x, &y);
    	cout<<"主线程 :"<<x<<endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 运行结果:
      子线程lambda传值

    • 发现只有子线程中的变量发生值的变化,而主线程没有

      难道开辟在主线程中的变量x y不被所有线程共享吗?

      子线程重新在进程共享区开辟了变量空间,发生类似写实拷贝了吗?

    • 继续看下面的代码:
      主线程和子线程共享变量

    • 原来是主线程调度优先级总是比子线程高

      导致子线程尚未修改变量,主线程已经打印完成

      符合所有线程共享进程绝大部分内容

      线程自己的局部变量以不同tid的结构体存储在进程地址空间的共享区

    传左值引用 + ref():
    • 大多数编译器直接传参左值引用会报错,

      visual studio 13不会,但是会直接视为传值

    • 传左值引用 + ref()函数,可以实现多线程看到同一块内存:

    #include 
    #include 
    #include 
    using namespace std;
    int main(){
    	int x = 0;
    	thread t([](int &n){
    		n = 1;
    		cout<<"子线程:"<<n<<endl;
    	}, ref(x));	
    	sleep(1);
    	cout<<"主线程:"<<x<<endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 运行结果:
      ref()
    捕捉列表:
    • 有了捕捉列表,不必再传参了:
    #include 
    #include 
    using namespace std;
    int main(){
    	int x = 0, y = 1;
    	thread t([&](){
    		x = 2;
    		cout<<"子线程:"<<x <<"  "<<y<<endl; 
    		});
    	cout<<"主线程"<<x <<" "<<y <<endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 同样,由于进程调度优先级的问题,主线程优先在子线程修改xy值前将xy打印出来:
      线程写时拷贝
    • 先让主线程休眠,等子线程修改完毕x y值后,主线程打印结果:
      引用传参看到的是同一物理地址上变量
    • 注意:atomic<>变量可以通过引用捕捉在lambda表达式使用,但是不能取地址后通过形参列表在lambda表达式使用

    线程等待:

    join等待:

    • thread 变量.join():

      一行代码让主线程陷入等待,

      若不加join(),主线程创建完子线程后,继续向下运行

      遇到return时马上终止该进程及其内部所有线程

      子线程可能尚未运行结束就被强迫终止

    detach脱离:

    • 进程脱离要求:
      1. 只有匿名线程可以脱离
      2. 由于< thread>中创建线程时,用户未记录其tid,
        所以要声明一个线程脱离,只能在创建线程后马上声明
      3. 子线程运行时,要让主线程阻塞,不能释放进程资源,终止所有线程
    • detach():
    #include 
    #include 
    #include 
    using namespace std;
    void func(){
    	while(1){
    		cout<<"子线程脱离,但是主线程不要释放资源"<<endl;
    		sleep(1); 
    	}
    }
    int main(){
    	thread (func).detach();
    	sleep(3);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 运行效果:
      thread().detach()

    线程列表:

    • 我们在Linux专栏下写过ThreadPool类:线程池
    • 下面我们先不实现可以添加任务的线程池类,先看看< thread>下的线程列表:
    • 创建n个线程,每个线程为原子变量x循环加1,加到M:
    #include 
    #include 
    #include 
    #include 
    using namespace std;
    int main(){
    	int N, M;
    	cin >>N >>M;
    	
    	atomic<int> x;				//原子变量可视为自带锁的变量
    	x = 0;						//原子变量不能初始化构造,但是可以赋值拷贝构造
    	vector<thread> vthds;
    	vthds.resize(N);
    	atomic<int> costTime;
    	costTime = 0;
    	for(int i=0; i<vthds.size(); i++){
    		vthds[i] = thread([M, &x, &costTime]{
    			int begin = clock();
    			for(int i=0; i<M; i++){
    				cout<<this_thread::get_id()<<"->"<<x<<endl;
    				x++;
    			}
    			int end = clock();
    			costTime += (end - begin);
    		});
    	}
    	for(auto &e: vthds){
    		e.join();
    	}
    	cout<<x<<endl;
    	cout<<"CostTime : "<<costTime;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 运行结果:5个线程每个线程为x加5,耗时44ms
      线程列表

    mutex锁:

    用法:

    回顾Linux:
    • Linux下的mutex属于< pthread.h>:
    #include 
    pthread_mutex_t mutex;
    int main(){
    	//动态初始化:
    	pthread_mutex_init(&mutex, nullptr);
    	//加锁:
    	 pthread_mutex_lock(&mutex);
    	//解锁:
    	pthread_mutex_unlock(&mutex);
    	//销毁锁:
    	pthread_mutex_destory(&mutex);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    < mutex>中:
    • C++11对mutex的用法有所化简:
    #include 
    //创建锁:
    mutex mtx;
    //加锁:
    mtx.lock();
    //解锁:
    mtx.unlock();
    //不必销毁锁:
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    加锁位置:

    分析:
    • 分析下面这段代码中锁应该加在位置1还是位置2:
    #include 
    #include 
    #include 
    using namespace std;
    int x = 0;
    mutex mtx;
    int N = 10000000;
    void func(){
    	//加锁位置2:mtx.lock()
    	for(int i=0; i<N; i++){
    		//加锁位置1:mtx.lock()
    		++x;
    		//解锁位置1:mtx.unlock()
    	}
    	//解锁位置2:mtx.unlock()
    }
    int main(){
    	thread t1(func);
    	thread t2(func);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 位置1的效果:

      每次进程调度中存在多次循环

      每次循环都申请释放一次锁

      每次申请释放锁的主要任务只是+1

    • 位置2的效果:

      每次进程调度中存在多次循环

      所有循环只申请释放一次锁

      每次申请释放锁的主要任务是重复几万次+1

    • 显然,位置2的速度更快,资源消耗更少,且同样保证了线程安全

    结论:
    互斥锁的加锁位置:
    • 当业务代码比较简单,执行速度块,执行时间短时:

      将锁加在循环外,避免循环内部不断申请释放锁的资源浪费

    • 当业务代码比较复杂,执行速度慢,执行时间长时:

      将锁加载循环内,一方面一次线程调度内可能完不成一次业务代码

      另一方面申请释放锁的消耗相对业务代码的消耗来说可以忽略了

    自旋锁的加锁位置:
    • 自旋锁不存在不断申请释放锁造成的资源消耗
    • 单纯的循环访问对资源消耗不大,所以加锁位置问题可以忽略
  • 相关阅读:
    BIRCH算法全解析:从原理到实战
    中介子方程二十二
    入门力扣自学笔记163 C++ (题目编号:面试题 01.09)
    qemu sriov
    436. 寻找右区间--LeetCode_二分
    猿创征文|SfM(Structure from Motion)学习之路
    企业办公OA系统适合多少人使用?
    Flink Table API & SQL
    (Tekla Structures二次开发)获取当前模型文件夹路径
    Vite依赖预构建
  • 原文地址:https://blog.csdn.net/buptsd/article/details/126895854