线程
0.Linux中有POSIX标准的线程,Boost库中也支持线程(比如thread_group
和 upgrade_lock
),C++11<thread>头文件中也提供了相应的API支持我们使用线程。它们三个,你学会一个,自然触类旁通。
1.创建线程:
#include<iostream>
#include<thread>
using namespace std;
void fun(){
cout << "fun()" << endl;
}
int main(){
std::thread t(fun);
t.join();
return 0;
}
编译命令:
g++ thread.cc -lpthread -std=c++11
解释:
我们实例化一个thread对象,把fun函数地址传给它,让他去执行fun函数,在主线程中我们还让t调用了join方法,此方法会阻塞住主线程直到t执行完fun函数。也就是谁调用了join,join就会等待它执行完成。然后调用join的线程的资源会被释放。如果没有t.join();那么主线程执行完时,子线程 t 可能还没有执行完成。
需要注意的点是:每个线程只能调用一次 join()
。如果尝试对同一个线程多次调用 join()
,或者对已经被 join()
或 detach()
的线程调用 join()
,会抛出 std::system_error
异常:
至于detach是什么?请看下述代码:
#include<iostream>
#include<thread>
using namespace std;
void fun(){
std::this_thread::sleep_for(std::chrono::seconds(3));
cout << "fun()done" << endl;
}
void fun2(){
std::thread t(fun);
t.detach();
cout << "fun2()done" << endl;
}
int main(){
fun2();
std::this_thread::sleep_for(std::chrono::seconds(5));
cout << "main thread done" << endl;
return 0;
}
调用 detach()
会使线程在后台独立运行,调用者线程不会等待它完成。有一个“脱离”的作用,而且你也不必再join它了。它会释放资源的。
如上代码:fun2作为调用者,调用了fun1函数,自己先结束了,但这不会影响fun1的执行。
2.创建线程对象时,还可以送参数进去。
3.thread没有拷贝构造函数和拷贝赋值函数,有移动构造函数和移动赋值函数。
4.在初次学习thread时,常用到两个睡眠函数。
std::this_thread::sleep_for(std::chrono::seconds(1));//睡眠一秒模拟数据处理。
std::this_thread::sleep_for(std::chrono::microseconds(1000)); //睡眠1000微秒也就是1秒钟。
他俩可以让当前线程睡眠若干时间,方便我们模拟一些事件。
锁
0.多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex(互斥锁)。这是我们要讲的第一种锁。
std::mutex mtx;//定义一把锁
mtx.lock();//加锁
mtx.unlock();//解锁
1.还有一种使用 lock_guard
自动加锁、解锁。它和mutex配合使用。原理是 RAII,和智能指针类似:实例化一个对象之后,这个对象的构造函数就是加锁,这个对象生存期到了调用析构时就解锁了。底层代码很简单。
std::mutex mtx;
void fun()
{
{
std::lock_guard<std::mutex> lock(mtx);//lock调用构造函数,对mtx加锁。
cout << "hello world" << endl;
}
//到这里,mtx就被自动释放了。总之我们不需要显式解锁,只需要看清楚lock的生存周期。lock在自己析构的时候会解锁。
}
2.我们还可以使用 unique_lock
自动加锁、解锁。他常常与条件变量配合使用,也可以与mutex配合。 unique_lock
与 lock_guard
原理相同,但是提供了更多功能(比如可以结合条件变量使用)。
另外:mutex::scoped_lock
其实就是 unique_lock<mutex>
的 typedef
。 至于 unique_lock
和 lock_guard
详细比较,可移步 StackOverflow。
unique_lock与条件变量配合我们在条件变量部分讲解。
条件变量
条件变量我们在Linux学习过,所以我们简单阐述:
0.什么时候用条件变量:线程 A 等待某个条件并挂起,直到线程 B 设置了这个条件,并通知条件变量,然后线程 A 被唤醒。
1.常常与unique_lock配合使用:
也就是说,你传入的参数应是一个 std::unique_lock<std::mutex>
类型的对象 。
2.条件变量被notify_all() 或者notify_one()也就是唤醒之后,之前挂起的线程就被唤醒,但是唤醒也有可能是假唤醒,因为有可能你等待的那个条件任然不满足。只是另一个线程条件满足了。所以wait一般会放在一个while循环中或者使用lambda表达式。两种是一样的:
相当于cv.wait(lock, [] { return ready; });
相当于:while (!ready) { cv.wait(lock); }
。 都是为了防止假的唤醒。
3.如(2)我们看到wait除了单参数的,还支持两个参数,是的,它有两种重载。如果是后者,第二个参数要求是一个函数对象,可以是仿函数也可以是Lambda表达式。
4.我们就这一个例子详细解释条件变量的相关API。
//注意,编译这种代码要用$ g++ a.cc -std=c++11 -lpthread。否则会收获:terminate called after throwing an instance of 'std::system_error'
//what(): Enable multithreading to use std::thread: Operation not permitted
#include<mutex>
#include<iostream>
#include<condition_variable>
#include<thread>
using namespace std;
std::mutex mtx;
std::condition_variable cv;
int flag = 0;
const int n = 10;
void fun1(char ch){
std::unique_lock<std::mutex> lock(mtx);
for (int i = 0; i < n; i++){
cv.wait(lock, []() -> bool
{ return flag == 0; });
cout << "fun1->" << ch << endl;
flag = 1;
cv.notify_all();
}
}
void fun2(char ch){
std::unique_lock<std::mutex> lock(mtx);
for (int i = 0; i < n; i++){
cv.wait(lock, []() -> bool
{ return flag == 1; });
cout<< "fun2->" << ch << endl;
flag = 2;
cv.notify_all();
}
}
void fun3(char ch){
std::unique_lock<std::mutex> lock(mtx);
for (int i = 0; i < n; i++){
cv.wait(lock, []() -> bool
{ return flag == 2; });
cout<< "fun3->" << ch << endl;
flag = 0;
cv.notify_all();
}
}
int main(){
std::thread t1(fun1, 'A');
std::thread t2(fun2, 'B');
std::thread t3(fun3, 'C');
t1.join();
t2.join();
t3.join();
return 0;
}
上述代码依次打印ABC,打印n次。
上述代码最关键的一个点是:锁加在for循环之外,进入循环之后,判断flag,也就是是不是该我打印,如果不是,把锁释放,然后把自己挂起来。如果flag就是我要的,也就是第二个参数条件为真,那么此时他不会释放锁,然后它会继续往下执行,打印,唤醒,然后执行下一次for循环,然后又遇到了wait,此时flag不是他,然后它才释放掉锁,把自己挂起来。
注意:当wait的第二个参数是lambda表达式且值为假时,那么wait的第一个参数的锁将被解掉,然后当前线程被放在等待队列中。直到被其他线程唤醒notify。
被唤醒后,它要做的第一个事是抢刚才释放的锁,一直抢,抢到后继续判断第二个表达式的真假,若第二个返回true,那wait才算调用结束,然后执行下面代码,注意此时还是持有锁的。
上述代码,我们使用了 std::unique_lock 而不是其他种类的锁。原因:
条件变量支持它:条件变量的wait()方法需要一个 std::unique_lock 类型的锁作为参数而不是 std::lock_guard。深层次原因是 std::unique_lock 可以在等待时自动释放锁,当条件满足时再重新获取锁。而后者则要等待对象生命周期到调用析构释放锁。
他还可以转移所有权:unique_lock 可以通过移动语义将锁的所有权转移给其他对象,而 lock_guard 不支持这个特性。
5.再看一段代码。
#include<iostream>
#include<condition_variable>
#include<mutex>
#include<thread>
using namespace std;
int i = 0;
std::condition_variable cv;
std::mutex mtx;
void waits(char ch)
{
std::unique_lock<std::mutex> lock(mtx);
while (0 == i)
{
cout << "thread" << ch << " is waiting." << endl;
cv.wait(lock);//这行代码的作用:把lock中的mtx锁释放掉。把当前线程放入等待队列(就像是等待在cv这个条件变量上)。当被其他线程唤醒之后,它先做的事设法获取锁,获取到之后到达就绪状态的,等待被调度,当cv.wait(lock)这一行执行完成中。然后上去判断0==i是否满足。不满足就进来,cout之后又wait了.....
//等待在条件变量上与获取锁时都不是就绪状态,
}
cout << "thread" << ch << " is done." << endl;
}
void singals(){
std::this_thread::sleep_for(std::chrono::microseconds(1000));
{
std::lock_guard<std::mutex> lock(mtx);//这里我们使用了lock_guard,因为我们只想让他打印一句,然后释放掉mtx,而不是像waits中的lock一样,不断解锁加锁,只有程序结束后才释放掉mtx。不要占用。但是还是包装的mtx,这是全局唯一的。
cout << "first signals()" << endl;
}
//上面的块结束,那么lock调用自己的析构函数,析构函数会释放mtx这个互斥锁。
cv.notify_all();//这里模拟虚假唤醒,因为i的值并没有改为1
std::this_thread::sleep_for(std::chrono::microseconds(1000));
{
std::lock_guard<std::mutex> lock(mtx);
i = 1;
cout << "second signals()" << endl;
}
cv.notify_all();
cout << "singals()done" << endl;
}
int main(){
std::thread tha(waits,'A');
std::thread thb(waits,'B');
std::thread thc(waits,'C');
std::thread thd(singals);
tha.join();
thb.join();
thc.join();
thd.join();
return 0;
}
//上述代码中的整个while(){//...}可以换成
//cout << "thread" << ch << " is waiting." << endl;
//cv.wait(lock, []() -> bool { return i == 1; });
// 通过lambda表达式的真假判断条件是否满足,判断当前线程做什么
//总之,while循环与单参数的wait配合使用。lambda表达式和两个参数的wait配合使用。
6.条件变量的wait方法如果是只有一个参数的版本。那么它在被唤醒后会做什么?
std::condition_variable cv;//定义条件变量cv
当调用 cv.wait(lock)
时,当前线程会释放与 lock
关联的互斥锁。为什么?因为wait这个代码肯定在一个循环中,它不满足人家的条件,然后进入了函数体中,调用了wait函数。所以我们此时就把锁让出来,这样其他线程就有了获得锁的可能。避免死锁。
此时该线程被放在等待队列中,直到被notify_all() 或者notify_one()也就是唤醒之后,然后wait方法此时做的事是尝试重新获取那个互斥锁,直到获取到之后,然后因为是while循环,所以它又进行判断是否条件满足,此时如果满足,就脱离了while了,执行下面代码。不满足,说明是虚假唤醒,所以有执行了wait(),又把锁解掉,进入等待。
7.要彻底掌握条件变量,可以做一些练习:哲学家就餐问题、多个线程合作打印出1-100的值。
参考
链接