为啥冗余的额外范围块会影响 std::lock_guard 行为?
Posted
技术标签:
【中文标题】为啥冗余的额外范围块会影响 std::lock_guard 行为?【英文标题】:Why does a redundant extra scoping block affect std::lock_guard behaviour?为什么冗余的额外范围块会影响 std::lock_guard 行为? 【发布时间】:2018-10-30 16:26:14 【问题描述】:这段代码演示了互斥锁在两个线程之间共享,但是thread_mutex
周围的范围块发生了一些奇怪的事情。
(我在another question 中有此代码的变体,但这似乎是第二个谜。)
#include <thread>
#include <mutex>
#include <iostream>
#include <unistd.h>
int main ()
std::mutex m;
std::thread t ([&] ()
while (true)
std::lock_guard <std::mutex> thread_lock (m);
usleep (10*1000); // or whatever
std::cerr << "#";
std::cerr.flush ();
);
while (true)
std::lock_guard <std::mutex> main_lock (m);
std::cerr << ".";
std::cerr.flush ();
这基本上可以正常工作,但 thread_lock
周围的范围块理论上应该是不必要的。但是,如果您将其注释掉...
#include <thread>
#include <mutex>
#include <iostream>
#include <unistd.h>
int main ()
std::mutex m;
std::thread t ([&] ()
while (true)
//
std::lock_guard <std::mutex> thread_lock (m);
usleep (10*1000); // or whatever
//
std::cerr << "#";
std::cerr.flush ();
);
while (true)
std::lock_guard <std::mutex> main_lock (m);
std::cerr << ".";
std::cerr.flush ();
输出是这样的:
........########################################################################################################################################################################################################################################################################################################################################################################################################################################################################################
也就是说,thread_lock
似乎永远不会屈服于main_lock
。
如果删除了冗余范围块,为什么thread_lock
总是获得锁定而main_lock
总是等待?
【问题讨论】:
不能保证公平调度。这两种行为都是标准允许的。 不应该有一些调度的保证吗?main_lock
饿死了。在您的代码的未注释版本中,有一个 I/O 操作,这让您的主线程有时间获得互斥锁。互斥锁是不公平的,所以如果在移除锁和取回锁之间有很短的距离(如您的代码的第二个版本),您的线程 t
很有可能会首先再次锁定互斥锁。跨度>
@spraff 保证可能很昂贵,而且它不是 C++ 方式为您提供昂贵的解决方案只是“因为”; )。
范围块不是“冗余的”。互斥锁被锁定到thread_lock
的范围结束,所以thread_lock
s 范围的结束位置很重要
【参考方案1】:
我使用 pthreads 在带有 GCC (7.3.0) 的 Linux 上测试了您的代码(删除了块作用域),并得到了与您相似的结果。主线程饿死了,但如果我等得够久,我偶尔会看到主线程做一些工作。
但是,我在 Windows 上使用 MSVC (19.15) 运行了相同的代码,并且没有线程被饿死。
看起来您使用的是 posix,所以我猜您的标准库在后端使用 pthreads? (即使使用 C++11,我也必须链接 pthread。)pthread 互斥锁不能保证公平。但这只是故事的一半。您的输出似乎与usleep
调用有关。
如果我拿出usleep
,我看到公平(Linux):
// fair again
while (true)
std::lock_guard <std::mutex> thread_lock (m);
std::cerr << "#";
std::cerr.flush ();
我的猜测是,由于在持有互斥锁的同时休眠了这么长时间,几乎可以保证主线程将尽可能地阻塞。想象一下,起初主线程可能会尝试旋转,希望互斥锁很快可用。一段时间后,它可能会被列入等候名单。
在辅助线程中,lock_guard
对象在循环结束时被销毁,因此互斥体被释放。它将唤醒主线程,但它会立即构造一个新的lock_guard
再次锁定互斥锁。主线程不太可能获取互斥锁,因为它只是被安排的。所以除非在这个小窗口中发生上下文切换,否则辅助线程可能会再次获得互斥锁。
在带有作用域块的代码中,辅助线程中的互斥锁在 IO 调用之前被释放。打印到屏幕需要很长时间,因此主线程有足够的时间来获取互斥体。
正如@Ted Lyngmo 在他的回答中所说,如果您在创建lock_guard
之前添加一个睡眠,那么饥饿的可能性就会大大降低。
while (true)
usleep (1);
std::lock_guard <std::mutex> thread_lock (m);
usleep (10*1000);
std::cerr << "#";
std::cerr.flush ();
我也尝试过使用 yield,但我需要 5+ 以使其更公平,这让我相信实际库实现细节、操作系统调度程序以及缓存和内存子系统效果中还有其他细微差别。
顺便说一句,感谢您提出一个很好的问题。它真的很容易测试和使用。
【讨论】:
【参考方案2】:您可以在不拥有互斥锁的情况下通过让出线程(或休眠)给它重新调度的提示。下面相当长的睡眠可能会导致它输出#.#.#.#。完美。如果你切换到让步,你可能会得到############......但从长远来看大约是50/50。
#include <thread>
#include <mutex>
#include <iostream>
#include <unistd.h>
int main ()
std::mutex m;
std::thread t ([&] ()
while (true)
usleep (10000);
//std::this_thread::yield();
std::lock_guard <std::mutex> thread_lock (m);
std::cerr << "#" << std::flush;
);
while (true)
usleep (10000);
//std::this_thread::yield();
std::lock_guard <std::mutex> main_lock (m);
std::cerr << "." << std::flush;
【讨论】:
以上是关于为啥冗余的额外范围块会影响 std::lock_guard 行为?的主要内容,如果未能解决你的问题,请参考以下文章
为啥触摸 SCNView 顶部的滑块会在 iOS 8/Swift 上将相机移动到下方?