C++11 - 线程库
前言:
Vue框架:
从项目学Vue
OJ算法系列:
神机百炼 - 算法详解
Linux操作系统:
风后奇门 - linux
回顾<pthread.h>:
- Linux中的线程:线程
创建线程:
- 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(可调用对象,可调用对象参数);
- 可调用对象:
- 函数 / 函数指针
- 仿函数类对象
- 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:
形参列表:
- 使用形参列表时,传参方式有两种:
- 传值
- 传指针
- 传引用 + 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;
}
-
运行结果:
-
发现只有子线程中的变量发生值的变化,而主线程没有
难道开辟在主线程中的变量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;
}
- 运行结果:
捕捉列表:
- 有了捕捉列表,不必再传参了:
#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脱离:
- 进程脱离要求:
- 只有匿名线程可以脱离
- 由于< thread>中创建线程时,用户未记录其tid,
所以要声明一个线程脱离,只能在创建线程后马上声明 - 子线程运行时,要让主线程阻塞,不能释放进程资源,终止所有线程
- 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;
}
- 运行效果:
线程列表:
- 我们在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的速度更快,资源消耗更少,且同样保证了线程安全
结论:
互斥锁的加锁位置:
-
当业务代码比较简单,执行速度块,执行时间短时:
将锁加在循环外,避免循环内部不断申请释放锁的资源浪费
-
当业务代码比较复杂,执行速度慢,执行时间长时:
将锁加载循环内,一方面一次线程调度内可能完不成一次业务代码
另一方面申请释放锁的消耗相对业务代码的消耗来说可以忽略了
自旋锁的加锁位置:
- 自旋锁不存在不断申请释放锁造成的资源消耗
- 单纯的循环访问对资源消耗不大,所以加锁位置问题可以忽略