目录
一、线程创建
每个程序都有一个主线程,主线程的入口函数就是main函数,一般情况下,主线程结束,无论子线程是否执行完毕,子线程都将结束
#include <iostream>
#include <thread>
#include <string>
using std::cout;
using std::thread;
//线程入口函数,函数完毕,线程结束
void MyPrint() {
cout << "This is MyThread!\n";
cout << "MyThread is finish!\n";
}
int main() {
cout << "你好\n";
thread MyThread1(MyPrint);//创建子线程,接收可调用对象
cout<<"Main finish!\n";
return 0;
}
二、线程的相关操作
2.1 join
阻塞函数
join: 阻塞函数,在这段代码中表示,主线程阻塞,等待MyThread1运行完后再结束
int main() {
cout << "你好\n";
thread MyThread1(MyPrint);//接收可调用对象
MyThread1.join();//jion,阻塞,等待MyThread运行结束后在继续运行主线程
cout<<"Main finish!\n";
return 0;
}
2.2 detach
分离函数
detach:分离函数,分离子线程和主线程,调用后,主线程是否结束不会影响子线程的运行
用的比较少,因为子线程不可控
int main() {
cout << "你好\n";
thread MyThread1(MyPrint);
MyThread1.detach();
cout<<"Main finish!\n";
return 0;
}
调用detach后,线程转入后台运行。此时被C++运行时库接管,当子线程执行完毕后,由运行时库负责清理线程资源。
一旦detach
,就无法join
,因为失去了关联。
所以detach应用的情况相对较少
2.3 joinable
判断函数
joinable:判断线程是否能join或者detach,返回ture
或false
因为很多时候我们不知道某个线程能否join或者detach,所以需要用这个进行判断
三、线程参数
3.1传参所引发的资源回收问题
void myprint(const int &i,char* p){
cout<<i<<endl<<p<<endl;
}
int main(){
int a=10;
char mystr[]="hello world";
thread myth(myprint,myI,mystr);//线程传参
myth.join()
//myth.detach();
}
看上述例子,如果detach
分离了线程的话,有可能造成主线程已经结束,但是myth
未结束的情况,则此时str已经被系统回收,p所指向的内容就不确定,程序出错
所以,为了避免资源被系统回收,使用临时变量
thread myth(myprint,myI,string(mystr));//线程传参
当使用了临时对象后,便没有出现资源被回收程序出问题的情况
为什么使用临时对象就不会出现问题?
猜想:使用临时对象是进行了资源的拷贝?
为了验证猜想,自己写一个类
class TA {
int a;
public:
void operator()() {//仿函数
cout << "线程TA开始执行了" << endl;
}
TA() = default;
TA(int b) {
this->a = b; //获取当前线程的id
cout << "构造函数!TA() id = " << std::this_thread::get_id() << "\taddr = " << this << endl;
}
TA(const TA &t) {
this->a = t.a;
cout << "拷贝构造TA(const TA & t) id = " << std::this_thread::get_id() << "\taddr = " << this << endl;
}
void work(int num){
cout<<"子线程work执行 "<< this<<" id = "<<std::this_thread::get_id()<<endl;
}
~TA() {
cout << "析构函数 id = " << std::this_thread::get_id() << "\taddr = " << this << endl;
}
};
void myprint2(const TA &ta) {//class类型参数 即使是引用,仍会进行拷贝,值传递
cout << "子线程开始 " << "addr = " << &ta << "\t子线程id = " << std::this_thread::get_id() << endl;
}
传入a为int类型,可是函数参数类型为TA,此时会发生隐式类型转化,调用构造函数,将int转为TA类型
int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
thread myobj(myprint2, a);//发现对象构造是在子线程中
myobj.join();
}
运行结果:
main id = 139656476180864
构造函数!TA() id = 139656476165696 addr = 0x7f044ea6fd74
子线程开始 addr = 0x7f044ea6fd74 子线程id = 139656476165696
析构函数 id = 139656476165696 addr = 0x7f044ea6fd74
可以看出,由于入口函数参数为类型TA
,程序发生隐式转换,用变量a
构造了一个临时对象,再将临时对象传入入口函数。并且可以观察到,构造对象的过程并不是在主线程完成的,而是在子线程中进行。所以我们可以推测,如果使用detach
,就有可能造成主线程比子线程先结束,导致a
被回收,从而构造出现问题,引发程序异常!大家不妨可以试试join
改为detach
,多运行几次可能就会发现问题所在。
那我们如何解决这类问题呢
不要使用隐式转换,自己完成强转操作
int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
thread myobj(myprint2, TA(a));
myobj.join();
}
运行结果:
main id = 139705515577728
构造函数!TA() id = 139705515577728 addr = 0x7ffd013a24a8
拷贝构造TA(const TA & t) id = 139705515577728 addr = 0x55a7c06ad2c8
析构函数 id = 139705515577728 addr = 0x7ffd013a24a8
子线程开始 addr = 0x55a7c06ad2c8 子线程id = 139705515562560
析构函数 id = 139705515562560 addr = 0x55a7c06ad2c8
通过上一例子可以看出,首先在主线程构造了一个临时变量,由 TA(a)
引起,因为这一对象很快被析构了,所以我们判断它是临时对象,这很容易理解;然后发生了拷贝构造,并且拷贝构造依然发生在主线程,并且可以看到,拷贝出来的对象进入到了子线程去执行,我们就可以推测,临时对象作为入口函数的参数,子线程会对其进行拷贝。
因此,在detach
一个线程时,我们尽量不要让编译器对参数进行隐式类型转化,这样有可能引发传入资源被回收的问题!对应的解决办法就是自己对其进行转化
我们还可以看出,即使子线程入口函数的的参数是引用类型,传参是仍然是拷贝构造,相当于就是值传递
,那如果我们需要子线程对某一数据进行更改,需要传入引用怎么办呢?
使用std::ref()
int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
TA ta(a);
thread myobj(myprint2, std::ref(ta));
myobj.join();
}
运行结果:
main id = 139781778977152
构造函数!TA() id = 139781778977152 addr = 0x7fffd0ec9d00
子线程开始 addr = 0x7fffd0ec9d00 子线程id = 139781778961984
析构函数 id = 139781778977152 addr = 0x7fffd0ec9d00
可以很清除的看到,当我们使用了std::ref()
时,构造函数在主线程中进行,并且子线程没有发生拷贝构造。传给子线程的是一个引用。此时我们将入口函数参数列表中的const
修饰符去掉,就可以修改对象中的数据,编译器也不会报错。这种情况下应该注意被引用对象回收时间的问题。
3.2 将对象的成员函数作为入口函数
int main(){
cout << "main id = " << std::this_thread::get_id() << endl;
int a=1
TA ta(a);
thread myobj(&TA::work,ta, 1);//类的成员函数作线程入口,第三个参数传入的对象,第四个参数是work函数的参数
// 也可以使用引用
//thread myobj(&TA::work,&ta, 1); 等价于std::ref(ta) 不会调用拷贝构造
myobj.join();
}
类内线程创建
class Ta{
public:
Ta(){
std::thread t(std::bind(&Ta::task,this));
}
void task();
}
四、线程的互斥量的使用
很多时候,多个线程需要对同一数据进行操作,同时读取某一数据往往不会出现问题,而当由某些线程读取数据,某些线程又会写入数据时,往往会发生读写冲突。
我们试想一下,线程A
在读取读取数据Data时,时间片结束,系统开始运行线程B
,而线程B
恰好要对Data进行修改,那么是不是会发生冲突。再想一下,一个线程对文件进行写入操作,结果写入结束还没保存并关闭文件时,时间片结束,另一个线程也访问这个文件,可是由于写文件线程没有保存文件,那么访问线程不就拿不到数据了吗。
所以引入了互斥量
这一概念
当访问共享数据前,使用互斥量将相关数据锁住,再当访问结束后,再将数据解锁。多线程程序需要保证,当一个线程使用特定互斥量锁住共享数据时,其他的线程想要访问锁住的数据,都必须等到之前那个线程对数据进行解锁后,才能进行访问。这就保证了所有线程能看到共享数据
举个栗子
/* 假如说我们有很多用户,他对我们的系统下了很多指令,使用一个线程接收指令,将指令存入一个list中
* 再由另一个线程读取list中的指令
*/
class ReadWrite {
private:
std::list<int> M;
public:
void RecvMsg() { //接收指令的线程
for (size_t i = 0; i < 10000; ++i) {
cout << "RecvMsg insert a num=" << i << endl;
M.emplace_back(i);
}
}
void GetFromM() { //读取指令的线程
for (size_t i = 0; i < 10000; ++i) {
if (!M.empty()) {
int com = M.front(); //返回地一个元素,无论是否为空
M.pop_front(); //取出后移除
cout<<"cmd="<<com<<endl;
} else {
cout << "list is null\ti=" << i << endl;
}
}
cout << "Get end" << endl;
}
};
int main(){
ReadWrite r;
thread out(&ReadWrite::GetFromM, std::ref(r));
thread get(&ReadWrite::RecvMsg, std::ref(r));
get.join();
out.join();
cout << "main end" << endl;
}
上述代码在运行时会发生问题,由于涉及到了同步读写同一块内存区域的问题,要解决这个问题,需要引用mutex
互斥量这个概念
#include<mutex>
class ReadWrite {
private:
std::list<int> M;
public:
void RecvMsg() { //接收指令的线程
for (size_t i = 0; i < 10000; ++i) {
cout << "RecvMsg insert a num=" << i << endl;
mymutex.lock();
M.emplace_back(i);
mymutex.unlock();
}
}
void GetFromM() { //读取指令的线程
for (size_t i = 0; i < 10000; ++i) {
mymutex.lock();
if (!M.empty()) {
int com = M.front(); //返回第一个元素,无论是否为空
M.pop_front(); //取出后移除
cout<<"cmd="<<com<<endl;
} else {
cout << "list is null\ti=" << i << endl;
}
mymutex.lock();
}
cout << "Get end" << endl;
}
};
int main(){
ReadWrite r;
thread out(&ReadWrite::GetFromM, std::ref(r));
thread get(&ReadWrite::RecvMsg, std::ref(r));
get.join();
out.join();
cout << "main end" << endl;
}
引入mutex,在对数据进行读写的时候,将把锁锁上,然后再操作数据,这样其他线程在试图访问的时候,由于锁被锁住了,其他线程也无法进行上锁操作,就会被阻塞,数据操作结束,将锁释放,让其他线程可以操作数据。
说的通俗一点,就像上厕所一样,上厕所之前要把门(mutex
)先锁好(lock
),这样其他人访问厕所就会被门拦住,上完了后将门打开(unlock
),厕所就能被其他人用了
有一点一定要记住,就是加锁和解锁一定是成对出现的,否则其他线程就会进入无限等待的状态。所以C++标准库提供了类似智能指针的锁std::lock_guard
,可以自动上锁自动解锁,当锁变量离开作用域时就会自动解锁,除此之外还可以使用std::unique_lock
,相较于lock_guard
更为灵活,它由不同的成员函数由用户自己控制何时加锁,能否加锁
unique_lock
详解
在使用互斥量的时候,一定要注意死锁,死锁就是两个线程同时等待对方持有的某一资源,导致程序无法正常推进
比如说我们由两个锁A
和B
,线程1
和线程2
,线程一
需要先锁A
,然后锁B
才能继续进行数据访问操作,而线程二
需要先锁B
再锁A
才能进行数据访问操作。假设程序运行到某一时间点,线程一
锁住了A
,线程二
锁住了B
,而此时,线程一
需要将B
上锁,才能继续推进,可是B
已经被线程二
锁住了,于是线程一
被阻塞
了,必须等待B
被解锁,同时线程二
也是要等待A
被解锁才能继续推进,程序再此刻便发生了死锁
解决死锁,只需要上锁的顺序一致便可,即线程一和线程二都是先上锁A再上锁B。使用std::lock
就可以保证多个锁的顺序一致
除了std::mutex
外,C++还提供了递归的独占互斥量std::recursive_mutex
,它允许一个线程,同一个互斥量被lock多次
timed_mutex和recursive_timed_mutex
带超时功能的独占互斥量和带超时功能的递归独占互斥量
两个接口:try_lock_for
和try_lock_until
try_lock_for
:等待一段时间,超时未拿到锁则放弃,程序继续执行,参数为std::chrono::*
std::timed_mutex my_mutex;
std::chrono::milliseconds my_times(100);//100毫秒
my_mutex.try_lock_for(my_times);//等待100毫秒,如果100毫秒内获得锁,则返回false
try_lock_until
:等待到某一个时间点
std::timed_mutex my_mutex;
std::chrono::milliseconds my_times(100);//100毫秒
my_mutex.try_lock_until(std::chrono::steady_clock::now()+my_times);//等待100毫秒,如果100毫秒内获得锁,则返回false
shared_mutex读写锁
可以理解为读写锁,允许多人读,单人写,但是写和读,写和写不能同时进行
可以直接lock
和lock_shared
,前者表示写的方式,一旦上锁,其他线程无法进入,后者表示读的方式,上锁后其他线程只能读,无法写
五、条件变量
5.1condition_variable
条件变量condition_vriable需要搭配互斥量使用,它是一个类,包含在同名的头文件中,有一些自己的成员函数,这里主要介绍
notify_one
和wait
函数
首先看一个例子
class MsgList {
private:
list<int> Command;
std::mutex mutex1;
std::condition_variable MyCod;
public:
void GetMsg() {
int cmd = 0;
while (true) {
std::unique_lock<std::mutex> guard(this->mutex1);//条件变量要与互斥量配合使用
this->MyCod.wait(guard, [this]() {
return !this->Command.empty();
});
cmd = Command.front();
Command.pop_front();
guard.unlock();
cout << "读取指令" << cmd << endl;
}
}
void PutMsg() {
for (size_t i = 0; i < 100000; ++i) {
std::unique_lock<std::mutex> guard(this->mutex1);
cout << "插入指令:" << i << endl;
Command.emplace_back(i);
this->MyCod.notify_one();
guard.unlock();//可要可不要,unique_lock的好处就在于可随时控制
}
}
};
int main(){
MsgList msgList;
thread Get(&MsgList::GetMsg, &msgList);
thread Put(&MsgList::PutMsg, &msgList);
Get.join();
Put.join();
cout << "main end" << endl;
}
两个线程Get
和Put
分别访问一个共享数据区,Get
从共享区拿数据,Put
往共享区放数据
5.2成员函数wait
this->MyCod.wait(guard, [this]() {return !this->Command.empty();});
,这行代码表示,若list
中没有数据则阻塞当前线程。
这行代码第一个参数是互斥量mutex,第二个参数是一个lambda表达式,用来判断list
是否有数据
wait
用于等一个信号,如果第二个参数返回值为false
,那么wait
将解锁互斥量并阻塞,即取消对临界区的占用并阻塞自己,直到有线程调用notify_one
,如果其他线程notify
了,当前线程唤醒,那么wait
就会不断获取mutex
(临界区的访问权),直到获取到,然后往下走
如果没有参数,只需要看是否有线程唤醒自己,唤醒则往下走
除wait
外, 条件变量还提供了wait_for
和wait_until
,前者用于等待指定时长,后者用于等待到指定的时间。
5.3成员函数notify_one
this->MyCod.notify_one();
这行代码就是在Put
线程完成了数据插入后,将Get
线程唤醒,只能唤醒一个线程。
5.4 存在的问题
Put
线程在执行notify
的时候,如果Get
没有被 wait() 所阻塞,那么唤醒操作就无效,此时Put
就没有将Get
叫起。
当线程Put
获取到的时间片比较多时,list 中积压的数据就会比较多,这个时候是否考虑要对存数据的线程进行限流呢?
六、线程返回值
6.1future和async
std::future
、std::async
创建后台任务并返回值
std::future
可以放置线程执行结束返回的结果值,可以调用get成员函数获取线程的返回值
std::async
与创建线程用法无异
#include<future>
int myTh5_2(){
cout<<"mythread() start"<<" thread id="<<std::this_thread::get_id()<<endl;
std::chrono::milliseconds milliseconds(1000);
std::this_thread::sleep_for(milliseconds);//休息1000毫秒
cout<<"mythread() end"<<" thread id="<<std::this_thread::get_id()<<endl;
return 1;
}
int main(){
cout<<"main start; id="<<std::this_thread::get_id()<<endl;
std::future<int> result=std::async(myTh5_2);//未来的值
cout<<"continue"<<endl;
//get一定会等待子线程执行结束,仅能执行一次
cout<<"return "<<result.get()<<endl;//卡住,等待myTh执行结束
cout<<"main end"<<endl;
}
运行结果:
main start; id=140234514006400
continue
return mythread() start thread id=140234513991232
mythread() end thread id=140234513991232
1
main end
get()
函数不拿到返回值,就会一直阻塞,还有wait()
函数,只是会等待返回,不会获取返回值
使用std::launch::deferred
延迟调用
std::future<int> result=std::async(std::launch::deferred,myTh5_2);
cout<<result.get();
当使用了deferred,只有执行了result.get()
线程才会被执行
main start; id=140553532236160
continue
return mythread() start thread id=140553532236160
mythread() end thread id=140553532236160
1
main end
可以看见,没有创建主线程,入口函数是在主线程中执行
与之相对应的是std::launch::async
,加入这个参数,表示线程会立即执行,而不是等到调用了get()
后执行
6.2package_task
打包任务,一个类模板,参数是各种可调用对象,作用是将可调用对象包装起来,方便作为线程入口函数
用法示例:
cout << "main start; id=" << std::this_thread::get_id() << endl;
//表示要求可调用对象的返回值为int,参数类型为int
std::packaged_task<int(int)> packagedTask(myTh5_3);
thread th(std::ref(packagedTask), 1);
th.join();
std::future<int> res = packagedTask.get_future();
cout<<"res.get()="<<res.get()<<endl;
packaged_task
还可以独自调用
std::packaged_task<int(int)> packagedTask1([](int a) -> int { cout << "a=" << a << endl; });
packagedTask1(5);//想当于函数调用
std::future<int> res1 = packagedTask1.get_future();
cout << "res1.get()=" << res1.get() << endl;
6.3std::promise
获取线程运行结果
通过promise保存一个值,在将来某一时刻通过future获取这个值
void myTh4(std::promise<int> &temp, int res) {
srand((int)time(0));
res++;
res *= (rand()%100+1);
std::chrono::milliseconds d(3000);
std::this_thread::sleep_for(d);
int result = res;
temp.set_value(result);
}
int main() {
std::promise<int> promise;
thread th(myTh4, std::ref(promise), 1);//使用这个,就可以在不同的线程传参
th.join();
std::future<int> fu = promise.get_future();
cout << "get res=" << fu.get() << endl;
}
//多线程应用
void myTh5(std::future<int> &f) {
cout << "thread id=" << std::this_thread::get_id() << "\tstart" << endl;
auto res = f.get();//等到有运算结果时才会继续
cout << "other thread return=" << res << endl;
}
void myTh4(std::promise<int> &temp, int res) {
srand((int) time(0));
res++;
res *= (rand() % 100 + 1);
cout << "thread id=" << std::this_thread::get_id() << "正在计算。" << endl;
std::chrono::milliseconds d(3000);
std::this_thread::sleep_for(d);
int result = res;
temp.set_value(result);
}
int main() {
std::promise<int> promise;
std::future<int> fu = promise.get_future();
thread th(myTh4, std::ref(promise), 1);//使用这个,就可一在不同的线程传参
thread th1(myTh5, std::ref(fu));
th1.join();
th.join();
}
使用
promise
和future
可以实现线程之间的通信,两个线程,一个线程计算,另一个线程等待计算结束后再继续运行
运行结果:
thread id=139657918248512 start
thread id=139657926641216 正在计算。
other thread return=88
但是一个future对象只能get一次,如果有多个线程需要获取这个值,可以使用shared_future
,这样就可以get多次
void myTh4(std::promise<int> &temp, int res) {
srand((int) time(0));
res++;
res *= (rand() % 100 + 1);
cout << "thread id=" << std::this_thread::get_id() << "正在计算。" << endl;
std::chrono::milliseconds d(3000);
std::this_thread::sleep_for(d);
int result = res;
temp.set_value(result);
}
void myTh6(std::shared_future<int> &f) {
cout << "thread id=" << std::this_thread::get_id() << "\tstart" << endl;
auto res = f.get();
cout << "other thread return=" << res << endl;
}
void main() {
std::promise<int> promise;
std::shared_future<int> future = promise.get_future();
thread th1(myTh4, std::ref(promise), 1);
thread th3(myTh6, std::ref(future));
thread th2(myTh6, std::ref(future));
th1.join();
th2.join();
th3.join();
}
6.4future及其他
std::future<int> fu= std::async(std::launch::deferred, myTh5_2);
std::future_status status=fu.wait_for(std::chrono::seconds(1));//等待线程1秒
if(status==std::future_status::timeout)//超时 ready执行完了 deferred 延迟执行
七、原子操作
void myth6_1(int &a) {
cout << "write start! address:" << &a << endl;
for (size_t i = 0; i < 100000; ++i) {
a++;
}
}
void myth6_2(int &a) {
cout << "read start! address:" << &a;
for (size_t i = 0; i < 100000; ++i) {
cout << a << endl;
}
}
void test6_1() {
int a = 8;
cout << "address " << &a << "\ta=" << a << endl;
thread th1(myth6_1, std::ref(a));
thread th2(myth6_1, std::ref(a));
th2.join();
th1.join();
cout << "address " << &a << "\ta=" << a << endl;
}
运行结果:
address 0x7fff7c796f3c a=8
write start! address:0x7fff7c796f3c
write start! address:0x7fff7c796f3c
address 0x7fff7c796f3c a=192558
上述代码就是两个线程,对同一个变量执行+1操作
多运行几次会发现,出现的结果可能和我们预期不一样
因为+1操作不是原子操作,线程在进行+1操作的时候,可能突然失去时间片,导致操作未完成,就造成了最终结果比预期小的现象
原子操作:不会被打断的程序执行片段
原子操作效率比互斥量更胜一筹,互斥量一般是针对某一片代码,原子操作通常针对某一变量
原子对象关键字std::atomic
void myth6_1(std::atomic<int> &a) {
cout << "write start! address:" << &a << endl;
for (size_t i = 0; i < 1000000; ++i) {
a++;
}
}
void myth6_2(std::atomic<int> &a) {
cout << "read start! address:" << &a;
for (size_t i = 0; i < 1000000; ++i) {
cout << a << endl;
}
}
void main() {
std::atomic<int> a = 8;
cout << "address " << &a << "\ta=" << a << endl;
thread th1(myth6_1, std::ref(a));
thread th2(myth6_1, std::ref(a));
th2.join();
th1.join();
cout << "address " << &a << "\ta=" << a << endl;
}
将共享关键字封装为原子变量后,操作就不不会被打断,这样每次运行的结果就都正确了
一般用于统计,计数
使用原子操作实现操控线程结束
std::atomic<bool> gFlag = false;
void myth6_3() {
std::chrono::milliseconds d(1000);
while (!gFlag) { //为假时不让停
cout << "thread id = " << std::this_thread::get_id() << " running..." << endl;
std::this_thread::sleep_for(d);//休息一秒
}cout << "thread id = " << std::this_thread::get_id() << " stop..." << endl;
}
void main() {
thread th1(myth6_3);
thread th2(myth6_3);
std::chrono::milliseconds s(5000);
std::this_thread::sleep_for(s);
gFlag = true;//停止两个线程
th1.join();
th2.join();
}
运行结果:
thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 running...
thread id = 140360114566720 running...
thread id = 140360106174016 stop...
thread id = 140360114566720 stop...
在上上个例子中,如果将原子变量的a++
操作改为a=a+1
,那么结果又会不符合我们预期
一般只针对++,--,+=,&=,|=,^=
适用
八、async深入
上文讲过,std::launch::deferred是延迟调用,std::launch::async是强制创建一个线程
在使用std::thread()
创建线程时,如果系统资源紧张,那么可能会创建失败,程序崩溃
std::async
一般称之为创建一个异步任务
上文提到过,使用了deferred
延迟调用后,甚至不会创建一个新的线程,只有调用了get或者wait才会开始执行入口函数
当使用sync
创建任务时std::future<int> result = std::async(std::launch::deferred, myTh5_2);
,若第一个参数没有指定,默认为std::launch::deferred | std::launch::async
,意思就是让系统决定是异步(创建)还是同步(不创建)运行。