C++并发编程中的锁---互斥元

说到并发编程就离不开锁的概念。锁其实是一种操作,这种操作可以避免竞争问题的出现。在c++中可以使用互斥元来实现锁的操作。

互斥元

互斥元也叫互斥量,在c++11中被命名为Mutex,所有其相关的类和函数都在头文件中。

在头文件mutex中一共有四种互斥元类,分别是:

  1. std::mutex,最基本的互斥元类。
  2. std::recursive_mutex,递归Mutex类(同一线程可以对互斥量多次上锁,来获得对互斥量对象的多层所有权)。
  3. std::time_mutex,定时Mutex类。
  4. std::recursive_timed_mutex,定时递归Mutex类。

上述四种互斥元类都有一个成员函数lock和一个unlock来实现锁定与解锁的操作。

除此之外,还有两种Lock类,分别是

  1. std::lock_guard,这个类的使用类似智能指针,可以销毁时自动解锁;
  2. std::unique_lock,这个类与1用法相同,但提供了更灵活的上锁和解锁控制,同时也更占资源;

lock类的作用类似智能指针,可以在销毁时自动解锁,不至于出现一个互斥元被一直锁住。

其他类型

  • std::once_flag
  • std::adopt_lock_t
  • std::defer_lock_t
  • std::try_to_lock_t

函数:

  • std::try_lock,尝试同时对多个互斥量上锁。
  • std::lock,可以同时对多个互斥量上锁。
  • std::call_once,如果多个线程需要同时调用某个函数,call_once 可以保证多个线程对该函数只调用一次。

重点1:std::mutex类

  • 构造函数,std::mutex不允许拷贝构造,也不允许 move 拷贝,最初产生的 mutex 对象是处于 unlocked 状态的。
  • lock(),调用线程将锁住该互斥量。线程调用该函数会发生下面 3 种情况
    1. 如果该互斥量当前没有被锁住,则调用线程将该互斥量锁住,直到调用 unlock之前,该线程一直拥有该锁。
    2. 如果当前互斥量被其他线程锁住,则当前的调用线程被阻塞住。
    3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
  • unlock(), 解锁,释放对互斥量的所有权。
  • try_lock(),尝试锁住互斥量,如果互斥量被其他线程占有,则当前线程也不会被阻塞。线程调用该函数也会出现下面 3 种情况
    1. 如果当前互斥量没有被其他线程占有,则该线程锁住互斥量,直到该线程调用 unlock 释放互斥量。
    2. 如果当前互斥量被其他线程锁住,则当前调用线程返回 false,而并不会被阻塞掉。
    3. 如果当前互斥量被当前调用线程锁住,则会产生死锁(deadlock)。
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
#include <iostream>       // std::cout
#include <thread> // std::thread
#include <mutex> // std::mutex

volatile int counter(0); // non-atomic counter
std::mutex mtx; // locks access to counter

void attempt_10k_increases() {
for (int i=0; i<10000; ++i) {
if (mtx.try_lock()) { // only increase if currently not locked:
++counter;
mtx.unlock();
}
}
}

int main (int argc, const char* argv[]) {
std::thread threads[10];
for (int i=0; i<10; ++i)
threads[i] = std::thread(attempt_10k_increases);

for (auto& th : threads) th.join();
std::cout << counter << " successful increases of the counter.\n";

return 0;
}

重点2:lock类的使用

  • std::lock_guard
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
#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的缺点是使用其锁住一个信号量之后,只有等到该lock_guard对象销毁后才能解锁。

  • std::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
#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;
}

unique_lock比lock_guard使用灵活很多,具体可以自行查阅。

死锁

死锁其实就是说线程被阻塞并且一直被阻塞无法跳出执行。举一个多个线程造成死锁的例子,两个线程A和B都需要做一个操作,这个操作需要同时涉及两个互斥元a和b才能完成。则当A锁定了互斥元a,B锁定了互斥元b,则此时A会一直阻塞,知道b解锁,B会一直阻塞,直到a解锁。因此最终导致A与B都一直被阻塞不能执行。

上述多个线程造成的死锁的解决方案:

  1. 始终使用相同的顺序锁定两个互斥元。

  2. c++标准库的std::lock可以同时锁定两个或更多的互斥元。

1个线程造成的死锁其实就是上面提到的在当前线程已经锁住一个互斥量的前提下继续尝试加锁。

锁粒度

锁粒度是一个文字术语,用来描述由单个锁所保护的数据量。细粒度锁保护着少量数据,粗粒度锁保护着大量的数据。锁粒度够大才能确保所需的数据都被保护,但同时会降低时效性。

可以使用std::unique_lock来灵活的确定锁粒度。一般来说,只应该以执行要求的操作所需的最小可能时间而去持有锁。

参考资料

  • [1] C++并发编程实战. Anthony Williams 著;
  • [2] https://www.cnblogs.com/haippy/p/3237213.html;

C++并发编程中的锁---互斥元
http://line.com/2021/03/03/2021-03-03-mutex/
作者
Line
发布于
2021年3月3日
许可协议