此代码演示了互斥锁在两个线程之间共享,但是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
总是等待?
答案 0 :(得分:2)
我在Linux上使用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+来使它更公平,这使我相信实际的库实现细节,OS调度程序以及缓存和内存子系统效果还有其他细微差别。
顺便说一句,谢谢你的提问。测试和玩起来真的很容易。
答案 1 :(得分:0)
您可以在不拥有互斥锁的情况下通过产生线程(或通过休眠)来提示重新安排时间。下面较长的睡眠可能会 使其输出#。#。#。#。完美。如果切换为屈服,从长远来看,您可能会得到############# ...............块,但从长远来看大约为50/50。 / p>
#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;
}
}