CPP——并发编程(四)condition_variable

并发编程中很重要的一点,是某些量是互斥的,他们不能同时允许。可以同时运行的比如多个读,而不能同时运行的,比如多个写。这篇文章介绍一下c++11中关于互斥(mutex)的内容。

关于互斥量的操作都需要包含头文件。

它包含了4个互斥类:

  • mutex,最基本的互斥类
  • recursive_mutex,递归互斥类
  • timed_mutex,定时互斥类
  • recursive_timed_mutex,递归定时互斥类

2个Lock类:

  • lock_guard,它管理一个互斥对象,通过保持它被锁定的状态。
  • unique_lock,它管理一个互斥对象,该对象在两种状态(锁定状态和非锁定状态)下都拥有唯一所有权。

其他类型:

  • once_flag
  • adopt_lock_t
  • defer_lock_t
  • try_to_lock_t

此外它还包含了几个函数:

  • try_lock
  • lock
  • call_once

下面对这些内容进行较为详细的介绍。

std::mutex

mutex是最基本的互斥量,它不支持递归地被上锁。mutex只有一个默认构造函数,不支持拷贝构造和移动构造,默认构造得到的mutex是未锁定的状态。

mutex的其他成员函数如下:

  • lock,锁上互斥量,如果互斥量已经被锁,当前线程会被阻塞。注意的是,如果互斥量已经被当前的线程锁住,再次调用lock会导致死锁,因为它会被阻塞一直等待unlock。
  • try_lock,如果互斥量状态是unlocked,那么锁上互斥量,如果互斥量已经被其他线程锁住,那么当前的线程也不会被阻塞,而是返回false。注意,和lock一样,如果互斥量已经被当前线程lock,那么调用这个会导致死锁。
  • unlock,解锁互斥量
  • native_handle,获取句柄

上面的函数直接看不知道怎么用,下面是一个官方给的代码示例,我们通过分析它来明白mutex的作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// mutex example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex

std::mutex mtx; // mutex for critical section

void print_block (int n, char c) {
// critical section (exclusive access to std::cout signaled by locking mtx):
mtx.lock();
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
mtx.unlock();
}

int main ()
{
std::thread th1 (print_block,50,'*');
std::thread th2 (print_block,50,'$');

th1.join();
th2.join();

return 0;
}

上面的代码中,prink_block使用了互斥量来控制循环部分不会被打断。首先,在循环前将mtx上锁,这时候调度到下一个线程的时候,因为mtx状态是locked,因此这个线程被阻塞,直到上个线程将mtx解锁。

上述程序的输出如下:

1
2
**************************************************
$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$

*和$的顺序可能会变,但是一定会整齐地输出50个。

std::recursive_mutex

上面的内容中,如果同一个线程对互斥量多次上锁,就会导致死锁,因为它不是递归的。而递归锁可以让同一个线程对其多次上锁,并且多次解锁。不过lock多少次,就必须unlock多少次,才能真正地解锁。除了这些,它和mutex的作用一致。

std::timed_mutex

timed_mutex比mutex多了两个成员函数:try_lock_for和try_lock_until.

1
2
template <class Rep, class Period>
bool try_lock_for (const chrono::duration<Rep,Period>& rel_time);

try_lock_for接受一个时间段,在这段时间范围内如果互斥量解锁了,就将其锁上,否则返回false。也就是和try_lock相比,它会等待更多的时间。

1
2
template <class Clock, class Duration>
bool try_lock_until (const chrono::time_point<Clock,Duration>& abs_time);

try_lock_until接受一个时间点,到时间点内互斥量依然被锁着,它就返回false,否则在时间点之前互斥量被解锁了,它就锁住互斥量。

std::recursize_timed_mutex

递归定时互斥量也就是recursive_mutex和timed_mutex的结合,它可以递归锁,也可以定解锁时长。这里就不多介绍了。

在介绍锁之前,先介绍几个与锁类型相关的 Tag 类,分别如下:

adopt_lock_t,一个空的标记类,定义如下:

1
struct adopt_lock_t {};

该类型的常量对象adopt_lock定义如下:

1
constexpr adopt_lock_t adopt_lock {};

通常作为参数传入给 unique_lock 或 lock_guard 的构造函数。

defer_lock_t,一个空的标记类,定义如下:

1
struct defer_lock_t {};

该类型的常量对象 defer_lock定义如下:

1
constexpr defer_lock_t defer_lock {};

通常作为参数传入给 unique_lock 或 lock_guard 的构造函数。

std::try_to_lock_t,一个空的标记类,定义如下:

1
struct try_to_lock_t {};

该类型的常量对象 try_to_lock定义如下:

1
constexpr try_to_lock_t try_to_lock {};

通常作为参数传入给 unique_lock 或 lock_guard 的构造函数。

下面介绍lock相关的类。

std::lock_guard

lock_guard是模板类:

1
template <class Mutex> class lock_guard;

它只有两个成员函数:构造和析构。构造函数,用法很简单,构造时候接受一个互斥量,然后互斥量就被锁定了。当析构函数调用,比如退出了作用域,那么互斥量就被解锁。但是它的构造函数是有两个选择,一个是默认,直接将互斥量锁定,另一个会接受一个参数,进行adopting initialization(传入参数adopt_lock),它可以绑定一个被当前线程锁定的互斥量。而默认的构造函数如果互斥量已经被当前线程锁定了,再次调用会进入死锁(一般的mutex类型)。

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// lock_guard example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::lock_guard
#include <stdexcept> // std::logic_error

std::mutex mtx;

void print_even (int x) {
if (x%2==0) std::cout << x << " is even\n";
else throw (std::logic_error("not even"));
}

void print_thread_id (int id) {
try {
// using a local lock_guard to lock mtx guarantees unlocking on destruction / exception:
std::lock_guard<std::mutex> lck (mtx);
print_even(id);
}
catch (std::logic_error&) {
std::cout << "[exception caught]\n";
}
}

int main ()
{
std::thread threads[10];
// spawn 10 threads:
for (int i=0; i<10; ++i)
threads[i] = std::thread(print_thread_id,i+1);

for (auto& th : threads) th.join();

return 0;
}

上面的代码通过建立lock_guard,将mtx互斥量锁住。当退出作用域的时候,mtx解锁。上面的代码我有个疑惑的地方在于catch之后的作用域,lck已经析构掉了,如果这段时间互斥量没有被锁住,那么别的线程获取了互斥量的控制权,会不会导致exception和其他的输出发生冲突?也许对于异常处理有更深的一套机制我了解得还不够。

std::unique_lock

unique_lock相对于lock_gaurd会复杂很多。它其实本身更像是一个互斥量,是对互斥量的封装。

对于unique_lock的构造函数都会有多种情况:

  1. default constructor,默认构造不绑定任何对象。

  2. locking initialization,绑定一个互斥量,并且锁住它,如果互斥量本身就是锁住的,则线程被阻塞,和lock_gaurd一样。

  3. try-locking initialization(传入参数try_to_lock),尝试绑定一个互斥量并且锁住,互斥量已经被锁,当前的unique_lock没有绑定任何对象。

  4. deferred initialization(传入参数defer_lock),绑定一个互斥量,设定互斥量状态为解锁,这个互斥量没有被别的线程锁住。

  5. adopting initialization((传入参数adopt_lock)),绑定一个互斥量,即使该互斥量已经被当前线程锁定了,这是它和locking initialization的区别,即使被当前线程锁定了依然可以绑定,而不会继续调用lock导致死锁。如果没有锁定,就会将它锁定,

  6. locking for duration,相当于调用try_lock_for,在一段时间内互斥量都被别的线程锁定,那么它不会绑定任何互斥量。

  7. locking until time point,相当于调用try_lock_until,在一个时间点前互斥量都被别的线程锁定,那么它不会绑定任何互斥量。

  8. copy construction [deleted]

  9. move construction

它有很多其他的成员函数:

  • lock,对拥有的互斥量上锁
  • unlock,解锁
  • try_lock
  • try_lock_for,try_lock_until
    上述成员函数都与互斥量本身的成员函数类似(目前不清楚它能否对mutex对象调用try_lock_for和try_lock_unique)。除此之外,它还有赋值函数,只不过只有move操作,没用拷贝功能。它还有其他的成员函数:
  • swap,交换互斥量以及它们的状态
  • release,释放当前拥有的互斥量
  • owns_lock,当前的锁拥有的互斥量被锁定了,返回true,否则的话,包括当前对象没有互斥量,或者互斥量状态为unlocked,都返回false
  • operate bool,它本身可以用来进行bool值的判断,依据是它是否拥有一个互斥量
  • mutex,得到当前绑定的互斥量的指针

下面是一个unique_lock的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// unique_lock example
#include <iostream> // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock

std::mutex mtx; // mutex for critical section

void print_block (int n, char c) {
// critical section (exclusive access to std::cout signaled by lifetime of lck):
std::unique_lock<std::mutex> lck (mtx);
for (int i=0; i<n; ++i) { std::cout << c; }
std::cout << '\n';
}

int main ()
{
std::thread th1 (print_block,50,'*');
std::thread th2 (print_block,50,'$');

th1.join();
th2.join();

return 0;
}

这里再列出来一个operate bool的例子,lck后面的参数决定了它会try_to_lock,也就是如果mtx已经被锁定了,则unique_lock不会绑定它。因此下面的函数会输出绑定成功的个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// unique_lock::operator= example
#include <iostream> // std::cout
#include <vector> // std::vector
#include <thread> // std::thread
#include <mutex> // std::mutex, std::unique_lock, std::try_to_lock

std::mutex mtx; // mutex for critical section

void print_star () {
std::unique_lock<std::mutex> lck(mtx,std::try_to_lock);
// print '*' if successfully locked, 'x' otherwise:
if (lck)
std::cout << '*';
else
std::cout << 'x';
}

int main ()
{
std::vector<std::thread> threads;
for (int i=0; i<500; ++i)
threads.emplace_back(print_star);

for (auto& x: threads) x.join();

return 0;
}