mutex 互斥量
多线程访问共享资源时,为了保证资源的线程安全,有必要做互斥处理。C++11 的mutex相比于pthread库提供互斥量使用起来更方便灵活。
C++11提供如下4种语义的mutex:
std::mutex
,独占的互斥量。std::time_mutex
,带超时的独占互斥量。std::recursive_mutex
,递归互斥量。std::recursive_timed_mutex
,带超时的递归互斥量。
独占的互斥量 mutex
常用成员函数
lock()
——互斥量加锁。线程调用该函数后存在 3 种情况:(1). 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁;(2). 如果当前互斥量被其他线程锁住,则当前线程会阻塞;(3). 如果当前线程重复加锁,则会产生死锁(deadlock)。unlock()
——互斥量解锁。try_lock()
——尝试加锁,如果互斥量已被其他线程加锁则当前调用立即返回,不阻塞。
示例代码
下面代码中,一般只有当线程函数使用increaseCounterByLock()
版本时,counter的最终值才是正确的;使用increaseCounterByTryLock()
版本时,最终的计数值会比前者要小,显然这是因为try_lock()并非每一次都能成功。
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
volatile int counter = 0;
mutex mtx;
void increaseCounterByNoLock()
{
for(int i = 0; i < 10000; i++)
counter++;
}
void increaseCounterByLock()
{
for(int i = 0; i < 10000; i++)
{
mtx.lock();
counter++;
mtx.unlock();
}
}
void increaseCounterByTryLock()
{
for(int i = 0; i < 10000; i++)
{
if(mtx.try_lock())
{
counter++;
mtx.unlock();
}
}
}
int main()
{
thread threads[10];
for(auto& t : threads)
t = thread(increaseCounterByNoLock);
for(auto& t : threads)
t.join();
cout << "counter through increaseCounterByNoLock is " << counter << endl;
counter = 0;
for(auto& t : threads)
t = thread(increaseCounterByLock);
for(auto& t : threads)
t.join();
cout << "counter through increaseCounterByLock is " << counter << endl;
counter = 0;
for(auto& t : threads)
t = thread(increaseCounterByTryLock);
for(auto& t : threads)
t.join();
cout << "counter through increaseCounterByTryLock is " << counter << endl;
}
输出打印:
counter through increaseCounterByNoLock is 78146
counter through increaseCounterByLock is 100000 # correct
counter through increaseCounterByTryLock is 51825
递归互斥量 recursive_mutex
递归互斥量允许一个线程多次地加锁,它的用法跟 mutex 几乎一样,但可以用来解决同一线程多次加锁导致的死锁问题。
尽管如此,我们还是不建议使用递归互斥量,而应该改进程序的逻辑。
带超时的互斥量 time_mutex
timed_mutex 只是比 mutex 多了两个可以设置超时时间的方法:try_lock_for
和try_lock_until
try_lock_for
设置一个超时时间,超时后还没获得锁就立即返回;
try_lock_until
设置一个时间点,到达时间点后还没获得锁就立即返回,用法参考std::timed_mutex::try_lock_until;
代码示例:
#include <iostream>
#include <mutex>
#include <thread>
using namespace std;
using ms = chrono::milliseconds;
using this_thread::sleep_for;
using this_thread::get_id;
timed_mutex mtx;
void work()
{
ms timeout(100);
while(true)
{
if(mtx.try_lock_for(timeout))
{
cout << "Thread " << get_id() << " work with lock...\n";
ms duration(300);
sleep_for(duration);
mtx.unlock();
sleep_for(duration);
}
else
{
cout << "Thread " << get_id() << " work without lock...\n";
ms duration(500);
sleep_for(duration);
}
}
}
int main()
{
thread t1(work);
thread t2(work);
t1.join();
t2.join();
}
lock_guard和unique_lock
这二者是互斥量的 RAII(通过类的构造析构)实现,类似于智能指针。
使用 lock_guard 时,可以这样给某一端临界区域的代码加锁:
// ......
{
lock_guard<std::mutex> locker(mtx);
.....
} // 出了大括号的区域后,lock_guard 被析构,自动解锁
// ......
在一块代码段中。将mutex丢给 unique_lock 与 lock_guard 类的实例都能实现自动加锁和解锁,但是unique_lock更加灵活,因为unique_lock允许主动调用unlock,但lock_guard不提供这个接口,只有在其析构时才能解锁。所以,在有需要主动调用unlock的场景必须使用unique_lock。