C++11 - 7 - 线程库

c++

前言:

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

回顾<pthread.h>:

创建线程:

  • pthread_create():
#include <pthread>
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;
}

线程等待:

  • pthread_join()线程等待:
#include <pthread>
void *func(void *args){
	cout<<"线程任务函数"<<endl;
}
int main(){
	pthread_t tid;
	pthread_create(&tid, nullptr, func, nullptr) < 0);
	pthread_join(tid, nullptr);
	return 0;
}
  • pthread_detach()自动分离:
#include <pthread>
void *func(void *args){
	pthread_detach(pthread_self());
	cout<<"线程任务函数"<<endl;
}
int main(){
	pthread_t tid;
	pthread_create(&tid, nullptr, func, nullptr) < 0);
	return 0;
}

终止线程:

  • 自然return:
#include <pthread>
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;
}
  • 结果:
    不论是return “0” 还是 return &“0”
    退出码都是48
  • 自己pthread_exit():
#include <pthread>
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;
}
  • 他人pthread_cancel():

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

#include <pthread>
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;
}

官方线程库:

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

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

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

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

线程创建:

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

函数指针:

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

仿函数类:

  • 仿函数使用struct 或 class public均可:
#include <iostream>
#include <thread>
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;
}

lambda:

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

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

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

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

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

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

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

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

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

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

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

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

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

线程等待:

join等待:

  • thread 变量.join():

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

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

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

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

detach脱离:

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

线程列表:

  • 我们在Linux专栏下写过ThreadPool类:线程池
  • 下面我们先不实现可以添加任务的线程池类,先看看< thread>下的线程列表:
  • 创建n个线程,每个线程为原子变量x循环加1,加到M:
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
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;
}
  • 运行结果:5个线程每个线程为x加5,耗时44ms
    线程列表

mutex锁:

用法:

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

加锁位置:

分析:
  • 分析下面这段代码中锁应该加在位置1还是位置2:
#include <iostream>
#include <thread>
#include <mutex>
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的效果:

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

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

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

  • 位置2的效果:

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

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

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

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

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

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

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

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

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

自旋锁的加锁位置:
  • 自旋锁不存在不断申请释放锁造成的资源消耗
  • 单纯的循环访问对资源消耗不大,所以加锁位置问题可以忽略
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

starnight531

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值