本文系列大部分来自c++11并发与多线程视频课程的学习笔记,系列文章有(不定期更新维护):
- C++并发与多线程(一)线程传参
- C++并发与多线程(二) 创建多个线程、数据共享问题分析、案例代码
- C++并发与多线程(三)单例设计模式与共享数据分析、call_once、condition_variable使用
- C++并发与多线程(四)async、future、packaged_task、promise、shared_future
- C++并发与多线程(五)互斥量,atomic、与线程池
创建和等待多个线程
如果想要创建多个子线程的话,我们可以把创建好的子线程入vector
中来管理多个子线程。用vector<thread> thread_vec;
创建一个数组,然后将多个子线程push
进这个数组中并进行管理。
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void Print(int num) {
cout << "sub thread is is: " << std::this_thread::get_id() << " num is: " << num << endl;
return;
}
int main() {
vector<thread> thread_vec;
for (int i = 0; i < 10; ++i) {
thread_vec.push_back(thread(Print, i)); // 创建临时线程对象,并将其加入到thread_vec中去。
}
for (auto iter = thread_vec.begin(); iter != thread_vec.end(); ++iter) {
iter->join(); // 等待所有子线程返回
}
cout << "main thread done and pid is: " << std::this_thread::get_id() << endl;
}
上述代码的执行结果如下所示:
可以看到上述代码的子线程执行顺序是乱的,这个和操作系统内部对线程的调度机制有关,并且有些子线程没有运行完就被抢走了时间片。
数据共享问题分析
在实际的应用场景中,我们可能会需要多个子线程对同一块数据进行读取或者写入操作。
只读的数据
通过static vector<int> data = { 0, 1, 2 };
设置一个全局变量,每个子线程都对这块数据进行读取处理。
#include <iostream>
#include <thread>
#include <vector>
using namespace std;
void Print(int num) {
static vector<int> data = { 0, 1, 2 };
cout << "sub thread is is: " << std::this_thread::get_id() << " num is: " << num << " address of data is : " << &data << endl;
return;
}
int main() {
vector<thread> thread_vec;
for (int i = 0; i < 10; ++i) {
thread_vec.push_back(thread(Print, i)); // 创建临时线程对象,并将其加入到thread_vec中去。
}
for (auto iter = thread_vec.begin(); iter != thread_vec.end(); ++iter) {
iter->join(); // 等待所有子线程返回
}
cout << "main thread done and pid is: " << std::this_thread::get_id() << endl;
}
上述代码的输出结果为:
有读有写
比如说两个线程往里面写,八个线程读的话,如何来做呢?其实我们就是要满足读的时候不能写,写的时候不能读这样一种功能。我们可以做一个共享数据的保护案例代码:假设做一个网络游戏服务器,有两个线程,一个线程收集玩家命令,并把命令数据写到一个队列中。另外一个线程从队列中取出命令并执行。因为是对同一个队列进行操作,所以我们需要做一些上锁的操作。在开始之前,我们需要先理解一些基本的概念:
- 互斥量:
互斥量(mutex
)的基本概念:互斥量就是个类对象,可以理解为一把锁,多个线程尝试用lock()
来给成员函数来加锁,只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()
这里不断尝试去锁定。互斥量使用要小心,尽量使得需要保护的数据不多也不少,少了达不到效果,多了影响效率。互斥量的用法:包含#include <mutex>
头文件。std::mutex mymutex;
就能创建一个mutex
的成员变量。lock()
,unlock()
是mutex
的两个成员函数。一般的使用用方法可以分为三步:1. lock()
,2. 操作共享数据,3. unlock()
。并且这里一定要注意:lock()
和unlock()
要成对使用。
C++
中为了防止用户忘记unlock()
,引入了一个叫做std::lock_guard
的类模板。当用户忘记unlock()
的时候不要紧,它会替你unlock()
。用了std::lock_guard
的类模板之后就不能使用lock()
和unlock()
了。lock_guard sbguard(myMutex);
取代lock()
和unlock()
,用法如下:lock_guard<mutex> sbguard(myMutex1);
。lock_guard
构造函数执行了mutex::lock();
在作用域结束时,调用析构函数,执行mutex::unlock()
。
- 死锁:死锁至少有两个互斥量
mutex1
,mutex2
。
线程A
执行时,这个线程先锁mutex1
,并且锁成功了,然后去锁mutex2
的时候,出现了上下文切换。之后线程B
执行,这个线程先锁mutex2
,因为mutex2
没有被锁,即mutex2
可以被锁成功,然后线程B
要去锁mutex1
。此时,死锁产生了,A
锁着mutex1
,需要锁mutex2
,B
锁着mutex2
,需要锁mutex1
,两个线程没办法继续运行下去。死锁的一般解决方案:只要保证多个互斥量上锁的顺序一样就不会造成死锁。
std::lock()
函数模板是C++11
中引入的函数模板。std::lock(mutex1,mutex2……);
。一次锁定多个互斥量(一般这种情况很少),用于处理多个互斥量。如果互斥量中一个没锁住,它就等着,等所有互斥量都锁住,才能继续执行。如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)。
如果用了std::lock()
函数模板但是怕忘记unlock()
的话,我们可以采用std::lock_guard
的std::adopt_lock
参数。std::lock_guard
std::mutex my_guard(my_mutex, std::adopt_lock);
在退出的时候会调用析构函数,自动unlock()
。加入adopt_lock
后,在调用lock_guard
的构造函数时,不再进行lock();
,adopt_guard
为结构体对象,起一个标记作用,表示这个互斥量已经lock()
,不需要在lock()
,但是析构的时候会unlock()
。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//lock_guard<mutex> sbguard(myMutex1);
lock(myMutex1, myMutex2); // 只有等所有互斥量都锁住才能锁成功。
//myMutex2.lock(); // 先锁2再锁1,就会产生死锁。
//myMutex1.lock();
msgRecvQueue.push_back(i);
myMutex1.unlock(); // 解锁的时候先解锁哪一个就无所谓。
myMutex2.unlock();
}
}
}
bool outMsgLULProc(){
myMutex1.lock(); // 这里与之前的先锁2后锁1会产生死锁。
myMutex2.lock();
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
myMutex2.unlock();
myMutex1.unlock();
return true;
}
myMutex2.unlock();
myMutex1.unlock();
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
if (outMsgLULProc()){
cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
}
else{
// 消息队列为空
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
mutex myMutex;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
私有成员list<int> msgRecvQueue;
为读取和写入的对象。读取的时候不能写入,写入的时候不能读取。用锁mutex myMutex1;
和mutex myMutex2;
来解决对list
成员的冲突读写问题。
unique_lock
unique_lock
是个类模板,在工作中,一般使用lock_guard
就足够了。lock_guard
取代mutex
的lock()
和unlock()
,unique_lock
比lock_guard
灵活很多(多出来很多用法),效率差一点,内存占用上可能会多一点。按照常规使用的话,lock_guard
和unique_lock
没有什么区别。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
std::unique_lock<std::mutex> sbguard1(myMutex1);
msgRecvQueue.push_back(i);
}
}
}
bool outMsgLULProc(){
std::unique_lock<std::mutex> sbguard1(myMutex1);
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
return true;
}
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
if (outMsgLULProc()){
cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
}
else{
// 消息队列为空
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
std::adopt_lock
用作第二个标记,表示这个互斥量已经被lock()
,我们必须把互斥量提前lock
了,否则会报异常,因为我们不需要在unique_lock
的构造函数中lock
这个互斥量了。(lock_guard
中也可以用这个参数。)
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
myMutex1.lock(); // 要先lock之后才能用unique_lock的std::adopt_lock参数
std::unique_lock<std::mutex> sbguard1(myMutex1, std::adopt_lock);
msgRecvQueue.push_back(i);
}
}
}
bool outMsgLULProc(){
std::unique_lock<std::mutex> sbguard1(myMutex1);
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
return true;
}
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
if (outMsgLULProc()){
cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
}
else{
// 消息队列为空
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
对于unique_lock
的灵活性,我们可以采用std::try_to_lock
代码演示。假设有如下的一个应用场景:在outMsgLULProc
函数中,myMutex1
这个把锁被拿走之后,卡住了20s
,这导致inMsgRecvQueue
函数中的锁也被卡住了20s
,非常的不灵活。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
myMutex1.lock(); // 要先lock之后才能用unique_lock的std::adopt_lock参数
std::unique_lock<std::mutex> sbguard1(myMutex1, std::adopt_lock);
msgRecvQueue.push_back(i);
}
}
}
bool outMsgLULProc(){
std::unique_lock<std::mutex> sbguard1(myMutex1);
std::chrono::milliseconds dura(20000); // 20000毫秒 = 20秒
std::this_thread::sleep_for(dura); // 休息一定的时长
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
return true;
}
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
if (outMsgLULProc()){
cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
}
else{
// 消息队列为空
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
std::try_to_lock
尝试用mutx
的lock()
去锁定这个mutex
,但如果没有锁定成功,会立即返回,不会阻塞在那里。但是使用try_to_lock
的前提是不能先去使用lock
,因为如果连续调用两次mutex
的lock
会导致程序卡死。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//myMutex1.lock(); // 用try_to_lock不能先lock
std::unique_lock<std::mutex> sbguard1(myMutex1, std::try_to_lock);
if (sbguard1.owns_lock()) { // 如果拿到了锁
msgRecvQueue.push_back(i);
}
else {
cout << "inMsgRecvQueue函数中myMutex1没拿到锁" << endl;
}
}
}
}
bool outMsgLULProc(){
std::unique_lock<std::mutex> sbguard1(myMutex1);
std::chrono::milliseconds dura(20000); // 20000毫秒 = 20秒
std::this_thread::sleep_for(dura); // 休息一定的时长
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
return true;
}
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
if (outMsgLULProc()){
cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
}
else{
// 消息队列为空
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
使用try_to_lock
的原因是防止其他的线程锁定mutex
太长时间,导致本线程一直阻塞在lock
这个地方。前提是不能提前lock()
。然后用owns_locks()
方法判断是否拿到锁,如拿到返回true
。
第二参数std::defer_lock
,是始化了一个没有加锁的mutex
,不给它加锁的目的是以后可以调用unique_lock
的一些方法,使用std::defer_lock
的前提是不能提前lock
。unique_lock
的成员函数有(前三个与std::defer_lock
联合使用):
lock()
,加锁,此方法不用自己unlock()
。
#include <iostream>
#include <thread>
#include <list>
#include <mutex>
using namespace std;
class A {
public:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//myMutex1.lock(); // 用try_to_lock不能先lock
std::unique_lock<std::mutex> sbguard1(myMutex1, std::defer_lock); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起
sbguard1.lock(); // 不用自己unlock
msgRecvQueue.push_back(i);
}
}
}
bool outMsgLULProc(){
std::unique_lock<std::mutex> sbguard1(myMutex1);
std::chrono::milliseconds dura(200); // 20000毫秒 = 20秒
std::this_thread::sleep_for(dura); // 休息一定的时长
if (!msgRecvQueue.empty())
{
cout << "删删删删删删删删删删删删删删删删删删删删删删删除元素。" << msgRecvQueue.front() << endl;
msgRecvQueue.pop_front();
return true;
}
return false;
}
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
if (outMsgLULProc()){
cout << "outMsgLULProc()执行了,取出一个元素。" << endl;
}
else{
// 消息队列为空
cout << "空空空空空空空空空空空空空空空空空空空空空空空空空空数组为空" << endl;
}
}
}
private:
list<int> msgRecvQueue;
mutex myMutex1;
mutex myMutex2;
};
int main()
{
A myobja;
thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myOutMsgObj.join();
myInMsgObj.join();
return 0;
}
unlock()
:解锁。在一些场合下,因为一些非共享代码要处理,可以暂时先unlock()
,用其他线程把它们处理了,处理完后再lock()
。
unique_lock<mutex> myUniLock(myMutex, defer_lock);
myUniLock.lock();
//处理一些共享代码
myUniLock.unlock();
//处理一些非共享代码
myUniLock.lock();
//处理一些共享代码
try_lock()
:尝试给互斥量加锁,如果拿不到锁,返回false
,否则返回true
。inMsgRecvQueue
函数可以修改为:
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//myMutex1.lock(); // 用try_to_lock不能先lock
std::unique_lock<std::mutex> sbguard1(myMutex1, std::defer_lock); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起
if (sbguard1.try_lock() == true) {
msgRecvQueue.push_back(i);
}
else {
cout << "没有拿到锁" << endl;
}
}
}
}
release()
:返回它所管理的mutex
对象指针,并释放所有权。也就是说这个unique_lock
和mutex
不再有关系。std::unique_lock<std::mutex> sbguard1(myMutex1);
相当于把myMutex1
和sbguard1
绑定在了一起,release()
就是解除绑定,返回它所管理的mutex
对象的指针,并释放所有权。
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//myMutex1.lock(); // 用try_to_lock不能先lock
std::unique_lock<std::mutex> sbguard1(myMutex1); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起
std::mutex *ptx = sbguard1.release(); // 现在有责任自己解锁myMutex1了
msgRecvQueue.push_back(i);
ptx->unlock();
}
}
所有权由ptx
接管,如果原来mutex
对象处理加锁状态,就需要ptx
在以后进行解锁了。
unique_lock
所有权的传递。std::unique_lock<std::mutex> sbguard1(myMutex1);
相当于把myMutex1
和sbguard1
绑定在了一起,也就是sbguard1
拥有myMutex1
的所有权。sbguard1
可以把自己对myMutex1
的所有权转移给其它的unique_lock
对象,所以unique_lock
对象对myMutex1
的所有权是属于,不能复制。
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i){
cout << "插插插插插插插插插插插插插插插插插插插插入一个元素" << i << endl;
{
//myMutex1.lock(); // 用try_to_lock不能先lock
std::unique_lock<std::mutex> sbguard1(myMutex1); // 创建一个没有加锁的myMutex1,然后将其和sbguard1绑定在一起。
std::unique_lock<std::mutex> sbguard2(std::move(sbguard1)); // 移动语义,现在相当于sbguard2和myMutex1绑定到了一起。
msgRecvQueue.push_back(i);
/*if (sbguard1.try_lock() == true) {
msgRecvQueue.push_back(i);
}
else {
cout << "没有拿到锁" << endl;
}*/
}
}
}
还有一种方式,在函数中return
一个临时变量,即可以实现转移:
unique_lock<mutex> aFunction()
{
unique_lock<mutex> myUniLock(myMutex);
//移动构造函数那里讲从函数返回一个局部的unique_lock对象是可以的
//返回这种局部对象会导致系统生成临时的unique_lock对象,并调用unique_lock的移动构造函数
return myUniLock;
}
参考
- https://blog.csdn.net/qq_38231713/article/details/106091902