为什么冗余的额外作用域块会影响 std::lock_guard 行为?
Why does a redundant extra scoping block affect std::lock_guard behaviour?
这段代码演示了互斥锁在两个线程之间共享,但是 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
总是等待?
您可以在不拥有互斥体的情况下让出线程(或休眠)来提示它重新安排时间。下面相当长的睡眠会可能导致它输出#.#.#.#。完美。如果你切换到屈服,你可能会得到############......但在 long [=15 中大约是 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;
}
}
我在 GCC (7.3.0) 上使用 pthreads 在 Linux 上测试了您的代码(删除了块作用域),得到了与您相似的结果。主线程饿死了,虽然如果我等的时间足够长,我偶尔会看到主线程做一些工作。
但是,我 运行 使用 MSVC (19.15) 在 Windows 上使用相同的代码并且没有线程被饿死。
看起来您正在使用 posix,所以我猜您的标准库在后端使用了 pthreads? (即使使用 C++11,我也必须 link pthreads。)Pthreads 互斥体不保证 运行 公平。但这只是故事的一半。您的输出似乎与 usleep
调用有关。
如果我取出usleep
,我看到公平(Linux):
// fair again
while (true)
{
std::lock_guard <std::mutex> thread_lock (m);
std::cerr << "#";
std::cerr.flush ();
}
我的猜测是,由于持有互斥量时睡眠时间过长,因此几乎可以保证运行主线程将尽可能地阻塞[=38] =].想象一下,一开始主线程可能会尝试自旋,希望互斥锁很快可用。一段时间后,它可能会被放入等候名单。
在辅助线程中,lock_guard
对象在循环结束时被销毁,从而释放互斥量。它会唤醒主线程,但它会立即构造一个新的 lock_guard
再次锁定互斥量。主线程不太可能会抢到互斥锁,因为它刚刚被调度。所以除非在这个小window中发生上下文切换,否则辅助线程很可能会再次获得互斥锁。
在带有作用域块的代码中,辅助线程中的互斥量在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 调度程序和缓存中还有其他细微差别和内存子系统的影响。
顺便说一句,谢谢你提出了一个很好的问题。它真的很容易测试和使用。
这段代码演示了互斥锁在两个线程之间共享,但是 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
总是等待?
您可以在不拥有互斥体的情况下让出线程(或休眠)来提示它重新安排时间。下面相当长的睡眠会可能导致它输出#.#.#.#。完美。如果你切换到屈服,你可能会得到############......但在 long [=15 中大约是 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;
}
}
我在 GCC (7.3.0) 上使用 pthreads 在 Linux 上测试了您的代码(删除了块作用域),得到了与您相似的结果。主线程饿死了,虽然如果我等的时间足够长,我偶尔会看到主线程做一些工作。
但是,我 运行 使用 MSVC (19.15) 在 Windows 上使用相同的代码并且没有线程被饿死。
看起来您正在使用 posix,所以我猜您的标准库在后端使用了 pthreads? (即使使用 C++11,我也必须 link pthreads。)Pthreads 互斥体不保证 运行 公平。但这只是故事的一半。您的输出似乎与 usleep
调用有关。
如果我取出usleep
,我看到公平(Linux):
// fair again
while (true)
{
std::lock_guard <std::mutex> thread_lock (m);
std::cerr << "#";
std::cerr.flush ();
}
我的猜测是,由于持有互斥量时睡眠时间过长,因此几乎可以保证运行主线程将尽可能地阻塞[=38] =].想象一下,一开始主线程可能会尝试自旋,希望互斥锁很快可用。一段时间后,它可能会被放入等候名单。
在辅助线程中,lock_guard
对象在循环结束时被销毁,从而释放互斥量。它会唤醒主线程,但它会立即构造一个新的 lock_guard
再次锁定互斥量。主线程不太可能会抢到互斥锁,因为它刚刚被调度。所以除非在这个小window中发生上下文切换,否则辅助线程很可能会再次获得互斥锁。
在带有作用域块的代码中,辅助线程中的互斥量在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 调度程序和缓存中还有其他细微差别和内存子系统的影响。
顺便说一句,谢谢你提出了一个很好的问题。它真的很容易测试和使用。