多线程中互斥信号量(Mutex)的使用
1.0 互斥量的基本概念
1.1 Example
- 我们来看一个简单的操作
#include <atomic>
#include <iostream>
#include <thread>
#include <chrono>
#include <pthread.h>
using namespace std;
int i = 0;
const int maxCnt = 1000000;
void mythread()
{
for (int j = 0; j < maxCnt; j++)
{
i++; // 线程同时操作变量
}
}
int main()
{
auto begin = chrono::high_resolution_clock::now();
thread t1(mythread);
thread t2(mythread);
t1.join();
t2.join();
auto end = chrono::high_resolution_clock::now();
cout << "i=" << i << endl;
cout << "time: "
<< chrono::duration_cast<chrono::microseconds>(end - begin).count() *
1e-6
<< "s" << endl; // 秒计时
}
可以看到在我的电脑上程序的输出为
i=1022418
time: 0.010445s
很明显和我们预想的结果是不一致的,我们使用两个线程同时对该变量进行加法操作,根据运行此书来计算,结果因该为 2000000,但事实上却不是这样的,这就是因为有多个线程在对同一个变量进行写操作的时候会出现难以排查的问题,意想不到的结果。此时mutex就派上用场了,我们对程序进行稍微的改动。
std::mutex var_mutex;
int i = 0;
const int maxCnt = 1000000;
void mythread()
{
for (int j = 0; j < maxCnt; j++)
{
var_mutex.lock();
i++; // 线程同时操作变量
var_mutex.unlock();
}
}
此时再运行程序可以发现结果如下,这是符合我们的预期的。
i=2000000
time: 0.09337s
1.2 互斥量用法解释
#include<mutex>
然后使用mutex 类即可创建对象。更重要的一点是,在代码中 lock()
(上锁)和 unlock()
(解锁)必须成对使用,代码中使用互斥量的时绝不允许非对称调用,即 lock()
和 unlock()
一定是成对出现的。步骤如下:
- 先
lock()
上锁; - 然后操作共享数据;
- 再
unlock()
解锁
2.0其他C++11新特性
2.1 std::lock_guard类模板
std::lock_guard
的类模板,它在开发者忘记解锁的时候,会替开发者自动解锁。std::lock_guard
可以直接取代 lock()
和 unlock()
,也就说使用 std::lock_guard
后,就不能再使用 lock()
和 unlock()
了。
-
如下所示
std::mutex var_mutex; int i = 0; const int maxCnt = 1000000; void mythread() { for (int j = 0; j < maxCnt; j++) { lock_guard<mutex> guard(var_mutex); i++; // 线程同时操作变量 } }
-
输出结果为
i=2000000 time: 0.102605s
std::lock_guard
虽然用起来方便,但是不够灵活,它只能在析构函数中 unlock()
,也就是对象被释放的时候,这通常是在函数返回的时候,或者通过添加代码块 { /* 代码块 */ }
限定作用域来指定释放时机。其还有一个特性是在构造的时候可以传入第二个参数为std::adopt_lock,此时在析构的时候就不会unlock了,但是此时就必须我们手动unlock了,这种使用场景也不多。
2.2 死锁
- 张三在北京说:等李四来了之后,我就去广东。
- 李四在广东说:等张三来了之后,我就去北京。
- 线程A执行时,先上锁1————再上锁2。
- 线程B执行时,先上锁2————再上锁1。
- 例如下面的程序
#include <pthread.h>
#include <atomic>
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <list>
using namespace std;
list<int> msgRecvQueue; // 容器(实际上是双向链表):存放玩家发生命令的队列
mutex m_mutex1; // 创建互斥量1
mutex m_mutex2; // 创建互斥量2
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
m_mutex1.lock(); // 实际代码中,两把锁不一定同时上,它们可能保护不同的数据
m_mutex2.lock();
msgRecvQueue.push_back(i); // 假设数字 i 就是收到的玩家命令
m_mutex2.unlock();
m_mutex1.unlock();
}
}
bool outMsgLULProc(int &command)
{
m_mutex2.lock();
m_mutex1.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); // 返回第一个元素
msgRecvQueue.pop_front(); // 移除第一个元素
m_mutex1.unlock();
m_mutex2.unlock();
return true;
}
m_mutex1.unlock();
m_mutex2.unlock();
return false;
}
void outMsgRecvQueue()
{
int command = 0;
for (int i = 0; i < 100000; ++i)
{
bool result = outMsgLULProc(command);
if (result)
cout << "outMsgLULProc exec, and pop_front: " << command << endl;
else
cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
cout << "outMsgRecvQueue exec end!" << i << endl;
}
}
int main()
{
thread myInMsgObj(inMsgRecvQueue);
thread myOutMsgObj(outMsgRecvQueue);
myInMsgObj.join();
myOutMsgObj.join();
cout << "Hello World!" << endl;
return 0;
}
笔者运行的时候发现程序会卡死,无法输出最后的一句话
outMsgLULProc exec, and pop_front: 271
outMsgRecvQueue exec end!289
outMsgLULProc exec, and pop_front: 272
outMsgRecvQueue exec end!290
inMsgRecvQueue exec, push an elem 491
通常来讲,死锁的一般解决方案,只要保证多个互斥量上锁的顺序一致,就不会出现死锁,比如把上面示例代码的两个线程回调函数中的上锁顺序改一下,保持一致就好了(都改为先上锁1,再上锁2)。读者可以自己试一下改动下代码。
- 线程A执行时,先上锁1————再上锁2。
- 线程B执行时,先上锁1————再上锁2。
这样的顺序之下就形不成死锁了。因为当切换到B的时候B由于没有锁1所以值接让出执行权限。
2.3 死锁的另一种解决方案
std::lock()
函数模板是C++11引入的,它能一次锁住两个或两个以上的互斥量,并且它不存在上述的在多线程中由于上锁顺序问题造成的死锁现象,原因如下:std::lock()
函数模板在锁定两个互斥量时,只有两种情况:
- 两个互斥量都没有锁住;
- 两个互斥量都被锁住。
如果只锁了一个,另一个没锁成功,则它会立即把已经锁住的互斥量解锁。将上面的接收函数改为如下就可以避免死锁的出现。
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
// m_mutex1.lock(); // 实际代码中,两把锁不一定同时上,它们可能保护不同的数据
// m_mutex2.lock();
std::lock(m_mutex1,m_mutex2);
msgRecvQueue.push_back(i); // 假设数字 i 就是收到的玩家命令
m_mutex2.unlock();
m_mutex1.unlock();
}
}
在使用 std::lock()
函数模板锁上多个互斥量时,也必须得记得把每个互斥量解锁,此时借助 std::lock_guard
的 std::adopt_lock
参数可以省略解锁的代码。我们再稍微更改一下代码,让他看上去更modern一些。
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
// m_mutex1.lock(); // 实际代码中,两把锁不一定同时上,它们可能保护不同的数据
// m_mutex2.lock();
std::lock(m_mutex1, m_mutex2); // 锁上两个互斥量
std::lock_guard<std::mutex> m_guard1(m_mutex1, std::adopt_lock); // 构造时不上锁,但析构时解锁
std::lock_guard<std::mutex> m_guard2(m_mutex2, std::adopt_lock); // 构造时不上锁,但析构时解锁
msgRecvQueue.push_back(i); // 假设数字 i 就是收到的玩家命令
}
}