在持有互斥锁的同时调用 notify_one()/notify_all() 是不好的做法吗?

Posted

技术标签:

【中文标题】在持有互斥锁的同时调用 notify_one()/notify_all() 是不好的做法吗?【英文标题】:Is it bad practice to call notify_one()/notify_all() while holding the lock on the mutex? 【发布时间】:2021-04-24 12:58:53 【问题描述】:

我正在编写一个练习程序来理解 C++ 中的多线程概念。这个程序只是在一个线程上读取用户输入的字符串,然后在另一个线程中处理它。

1   #include <iostream>
2   #include <thread>
3   #include <condition_variable>
4   #include <mutex>
5   #include <queue>
6   #include <string>
7   
8   #define __DEBUG
9   
10  #ifdef __DEBUG
11  #define PRINT cout << __FUNCTION__ << " --- LINE : " << __LINE__ << '\n'
12  #else
13  #define PRINT
14  #endif
15  
16  using namespace std;
17  
18  condition_variable g_CondVar;
19  mutex g_Mutex;
20  queue<string> g_Queue;
21  
22  void AddTaskToQueue()
23  
24      string InputStr;
25      while (true)
26      
27          lock_guard<mutex> LG(g_Mutex);
28          PRINT;
29          cin >> InputStr;
30          PRINT;
31          g_Queue.push(InputStr);
32          PRINT;
33          g_CondVar.notify_one();
34          PRINT;
35          if (InputStr == "Exit")
36              break;
37          this_thread::sleep_for(50ms);
38      
39  
40  
41  void ProcessQueue()
42  
43      PRINT;
44      unique_lock<mutex> UL(g_Mutex);
45      PRINT;
46      string ProcessingStr;
47      PRINT;
48      while (true)
49      
50          PRINT;
51          g_CondVar.wait(UL, [] return !g_Queue.empty(); );
52          PRINT;
53          ProcessingStr = g_Queue.front();
54          cout << "Processing ----" << ProcessingStr << "----" << '\n';
55          PRINT;
56          g_Queue.pop();
57          PRINT;
58          if (ProcessingStr == "Exit")
59              break;
60          this_thread::sleep_for(50ms);
61      
62  
63
64  int main()
65  
66      thread TReadInput(AddTaskToQueue);
67      thread TProcessQueue(ProcessQueue);
68  
69      TReadInput.join();
70      TProcessQueue.join();
71  

输出如下。

AddTaskToQueue --- LINE : 28
ProcessQueue --- LINE : 43
TestString
AddTaskToQueue --- LINE : 30
AddTaskToQueue --- LINE : 32
AddTaskToQueue --- LINE : 34
AddTaskToQueue --- LINE : 28

我有几个问题我不想自己总结/我无法理解。

    从输出中注意到 Line: 44 上从未获得锁,为什么会这样? 在 Line: 33 中锁定互斥体时调用 notify_one()/notify_all() 是否是一种好习惯。 线程 TProcessQueue 是否有可能在TReadInput 之前开始执行?我的意思是问n线程的执行顺序是否与实例化的方式相同? 在condition_variable 上调用wait 之前是否应该锁定mutex?我的意思是问下面的代码行不行?
//somecode
unique_lock<mutex> UL(Mutex, defer_lock);  //Mutex is not locked here
ConditionVariable.wait(UL, /*somecondition*/);
//someothercode
    如果我将 Line: 44 更改为 unique_lock&lt;mutex&gt; UL(g_Mutex, defer_lock); 输出如下,Line: 38 上抛出异常
AddTaskToQueue --- LINE : 28
ProcessQueue --- LINE : 43
ProcessQueue --- LINE : 45
ProcessQueue --- LINE : 47
ProcessQueue --- LINE : 50
TestString
AddTaskToQueue --- LINE : 30
AddTaskToQueue --- LINE : 32
AddTaskToQueue --- LINE : 34
ProcessQueue --- LINE : 52
Processing ----TestString----
ProcessQueue --- LINE : 55
ProcessQueue --- LINE : 57
d:\agent\_work\1\s\src\vctools\crt\crtw32\stdcpp\thr\mutex.c(175): unlock of unowned mutex
ProcessQueue --- LINE : 50

为什么会发生这种情况,unlock of unowned mutex 是什么?

    如果您发现我应该在此代码的任何其他部分使用任何更好的编程实践,请指出。

【问题讨论】:

【参考方案1】:

至于unlocknotify_one 之前,cppreference 表示如下(强调我的):

通知线程不需要持有与等待线程持有的互斥锁相同的互斥锁;实际上这样做是一种悲观,因为被通知的线程会立即再次阻塞,等待通知线程释放锁。但是,一些实现(特别是许多 pthread 的实现)认识到这种情况并避免这种情况“快点等待”场景,通过将等待线程从条件变量的队列直接转移到通知调用中的互斥体队列,而不唤醒它。

在需要精确安排事件时可能需要在锁定时通知,例如如果条件满足,等待线程退出程序,导致通知线程的condition_variable被破坏。在互斥锁解锁之后但在 notify 之前的虚假唤醒将导致在​​被破坏的对象上调用 notify。

更详细的解释here。

【讨论】:

【参考方案2】:
    您目前最大的问题很可能是您的线程sleep 同时持有互斥锁。这意味着唯一一次不同线程可以获得互斥锁的所有权是在 50 毫秒后的微小时间窗口内,此时互斥锁很快被释放并重新获取。您应该解决此问题,并且可能会看到不同的结果。 最好在锁外调用notify。这样做的原因是,被唤醒的线程可能会立即从那里开始运行,但随后会在互斥锁上被阻塞,需要再次休眠并稍后再次被操作系统唤醒。在您的情况下,这肯定会发生,因为 Mutex 会再保持 50 毫秒。但这更多是一种效率优化,而不是一种正确性。 不保证线程的启动顺序。 和 5/6:您只能在拥有关联互斥锁时等待条件变量,因为等待会在此期间释放互斥锁。这就是您遇到的错误。

【讨论】:

感谢您的信息。从您的第 2 点出发,不会错过这样的notify() 吗?还请详细说明第 4 点。我不太明白。

以上是关于在持有互斥锁的同时调用 notify_one()/notify_all() 是不好的做法吗?的主要内容,如果未能解决你的问题,请参考以下文章

读写锁概述

LiteOS 互斥锁机制

锁volatileCAS的比较

Redis 分布式锁的正确实现方式

Mutex互斥锁的使用方法

ReentrantLock (独占锁、互斥锁)