保护共享数据,操作时,某个线程,用代码把共享数据锁住,操作数据,解锁
其他操作共享数据的线程必须等待解锁。锁定住,操作,解锁。
一、互斥量(mutex)的基本概念
互斥量是个类对象,理解成一把锁,多个线程尝试用 lock() 成员函数来加锁这把锁头,只有一个线程能锁定成功(成功的标注是lock函数返回)。
如果没有锁成功,那么流程卡在 lock() 这里不断的尝试去锁这把锁头。
互斥量使用要小心,保护数据不能多也不能少,少了没有达到保护效果,多了影响效率。
二、互斥量的用法
<1>lock()、unlock()
先 lock() 操作共享数据,再 unlock()
lock() 和 unlock() 要成对使用,有 lock() 必然要有 unlock(),每调用一次 lock(),必然应该调用一次unlock()。
不应该也不允许调用1次 lock() 却调用了2次 unlock(),也不允许调用2次 lock() 却调用1次 unlock(),
这些非对称数量的调用都会导致代码不稳定甚至崩溃。
class MA
{
public:
//把收到的消息(玩家命令)放入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
my_mutex.lock();
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,直接弄到消息队列中
my_mutex.unlock();
}
}
bool outMsgLUProc(int& command)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
my_mutex.unlock();
return true;
}
my_mutex.unlock();
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
if (!msgRecvQueue.empty())
{
outMsgLUProc(i);
//这里就考虑处理数据...
//...
}
else
{
//消息队列为空
cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家发送的命令
std::mutex my_mutex; //创建了一个互斥量
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
有 lock(),忘记 unlock() 的问题,非常难排查;
为了防止大家忘记 unlock(),引入一个叫 std::lock_guard 的类模板:你忘记 unlock() 不要紧,我替你unlock();
学习过智能指针(unique_ptr<>):你忘记释放内存不要紧,我给你释放; 保姆
<2>std::lock_guard类模板:直接取代 lock() 和 unlock(),也就是说,你用了 lock_gurad 之后,再不能使用 lock() 和 unlock()。
class MA
{
public:
//把收到的消息(玩家命令)放入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
{
lock_guard<std::mutex> sbguard(my_mutex);
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,直接弄到消息队列中
}
}
}
bool outMsgLUProc(int& command)
{
lock_guard<std::mutex> sbguard(my_mutex); //sbguard是随便起的对象名
//lock_guard构造函数里执行了mutex::lock();
//lock_guard析构函数里执行了mutex::unlock();
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
return true;
}
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLUProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
//这里就考虑处理数据...
//...
}
else
{
cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家发送的命令
std::mutex my_mutex; //创建了一个互斥量
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
三、死锁
张三:站在北京等李四 不挪窝
李四:站在深圳等张三 不挪窝
C++中,比如我有两把锁(死锁这个问题是由至少两个锁头也就是两个互斥量才能产生),金锁(JinLock),银锁(YinLock);
两个线程A,B
1>线程A执行的时候,这个线程先锁金锁,把金锁lock()成功后,然后它去lock()银锁…
出现上下文切换
2>线程B执行了,这个线程先锁银锁,因为银锁还没有被锁,所以银锁会lock()成功,线程B要去lock()金锁…
此时此刻,死锁就产生了
3>线程A因为拿不到银锁,流程走不下去(所有后边代码有解锁金锁的但是流程走不下去,所以金锁解不开)
4>线程B因为拿不到金锁,流程走不下去(所有后边代码有解锁银锁的但是流程走不下去,所以银锁解不开)
大家都晾在这里,你等我,我等你
<1>死锁演示
class MA
{
public:
//把收到的消息(玩家命令)放入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
{
my_mutex_1.lock(); //实际工程这两个锁头不一定挨着,可能他们需要保护不同的数据共享块
my_mutex_2.lock();
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,直接弄到消息队列中
my_mutex_2.unlock();
my_mutex_1.unlock();
}
}
}
bool outMsgLUProc(int& command)
{
my_mutex_2.lock();
my_mutex_1.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
my_mutex_1.unlock();
my_mutex_2.unlock();
return true;
}
my_mutex_1.unlock();
my_mutex_2.unlock();
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLUProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
//这里就考虑处理数据...
//...
}
else
{
cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家发送的命令
mutex my_mutex_1; //创建了一个互斥量 (一把锁头)
mutex my_mutex_2; //创建了一个互斥量
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
<2>死锁的一般解决方案
只要保证这两个互斥量上锁的顺序一致就不会死锁
class MA
{
public:
//把收到的消息(玩家命令)放入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
{
std::lock_guard<std::mutex> guard_1(my_mutex_1);
std::lock_guard<std::mutex> guard_2(my_mutex_2);
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,直接弄到消息队列中
}
}
}
bool outMsgLUProc(int& command)
{
std::lock_guard<std::mutex> guard1(my_mutex_1);
std::lock_guard<std::mutex> guard2(my_mutex_2);
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
return true;
}
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLUProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
//这里就考虑处理数据...
//...
}
else
{
cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
}
}
cout << "end" << endl;
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家发送的命令
mutex my_mutex_1; //创建了一个互斥量 (一把锁头)
mutex my_mutex_2; //创建了一个互斥量
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
<3>std::lock()函数模板:用来处理多个互斥量的时候才出场
能力:一次锁住两个或者两个以上的互斥量 (至少2个,多了不限,1个不行)。
它不存这种因为在多个线程中,因为锁的顺序问题导致死锁的风险。
std::lock():如果互斥量中有一个没锁住,它就在那里等着,等所有互斥量都锁住,它才能往下走(返回)。
要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。
class MA
{
public:
//把收到的消息(玩家命令)放入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
{
std::lock(my_mutex_1, my_mutex_2); //相当于每个互斥量都调用了lock()
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,直接弄到消息队列中
my_mutex_2.unlock();
my_mutex_1.unlock();
}
}
}
bool outMsgLUProc(int& command)
{
std::lock(my_mutex_2, my_mutex_1);
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
my_mutex_1.unlock();
my_mutex_2.unlock();
return true;
}
my_mutex_1.unlock();
my_mutex_2.unlock();
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLUProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
//这里就考虑处理数据...
//...
}
else
{
cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家发送的命令
mutex my_mutex_1; //创建了一个互斥量 (一把锁头)
mutex my_mutex_2; //创建了一个互斥量
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
<4>std::lock_guard的std::lock参数
std::adopt_lock是个结构体对象,起一个标记作用,作用就是表示这个互斥量已经lock();
不需要在std::lock_guardstd::mutex构造函数里面再对mutex对象进行lock()了。
class MA
{
public:
//把收到的消息(玩家命令)放入到一个队列的线程
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue()执行,插入一个元素: " << i << endl;
{
std::lock(my_mutex_1, my_mutex_2); //相当于每个互斥量都调用了lock()
std::lock_guard<std::mutex> guard_1(my_mutex_1, std::adopt_lock);
std::lock_guard<std::mutex> guard_2(my_mutex_2, std::adopt_lock);
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,直接弄到消息队列中
}
}
}
bool outMsgLUProc(int& command)
{
std::lock(my_mutex_2, my_mutex_1);
std::lock_guard<std::mutex> guard1(my_mutex_2, std::adopt_lock);
std::lock_guard<std::mutex> guard2(my_mutex_1, std::adopt_lock);
if (!msgRecvQueue.empty())
{
//消息不为空
command = msgRecvQueue.front(); //返回第一个元素,但不检查元素是否存在;
msgRecvQueue.pop_front(); //移除第一个元素,但不返回
return true;
}
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLUProc(command);
if (result == true)
{
cout << "outMsgRecvQueue()执行,取出一个元素" << endl;
//这里就考虑处理数据...
//...
}
else
{
cout << "outMsgRecvQueue()执行,但目前消息队列中为空 " << i << endl;
}
}
cout << "end" << endl;
}
private:
list<int> msgRecvQueue; //容器(消息队列),专门用于代表玩家发送的命令
mutex my_mutex_1; //创建了一个互斥量 (一把锁头)
mutex my_mutex_2; //创建了一个互斥量
};
int main()
{
MA myobj;
std::thread myOutMsgObj(&MA::outMsgRecvQueue, &myobj); //第二个参数是引用,才能保证线程里用的是同一个对象
std::thread myInMsgObj(&MA::inMsgRecvQueue, &myobj);
myOutMsgObj.join();
myInMsgObj.join();
cout << "main主函数执行结束" << endl; //最后执行这句,整个进程退出
return 0;
}
总结:std::lock():一次锁定多个互斥量,谨慎使用(建议一个一个锁)。