【问题标题】:Thread scheduling when releasing lock释放锁时的线程调度
【发布时间】:2018-11-22 16:14:35
【问题描述】:

我在解锁互斥锁时的期望是调度程序检查当前尝试锁定该互斥锁的其他线程,然后执行其中一个等待线程。我编写了一个测试程序(见下面的代码),其中有 2 个线程都试图在一个循环中获取相同的互斥锁并做一些工作(睡眠 1 毫秒)。不同之处在于,一个线程 t1 在解锁和尝试重新获取互斥锁之间等待片刻,而另一个线程 t2 则不会。我期待两个线程获取互斥锁的次数大致相同。但是,在 Windows 上,t1 通常只获取一次互斥锁,而其他线程则获取数百次。在 linux 上,行为是不同的,两个线程都使用 t2 大约两倍的数量完成工作。为什么 Windows 上的 t1 几乎从不获取互斥锁?我该如何修改代码才能做到这一点?

示例代码:

#include <iostream>
#include <thread>
#include <mutex>
#include <atomic>

using namespace std;

int main()
{
    mutex m;
    atomic<bool> go(false);
    int t1Counter = 0, t2Counter = 0;

    thread t1([&] {
        while(!go);
        while(go) {
            this_thread::sleep_for(100us);
            lock_guard<mutex> lg(m);
            this_thread::sleep_for(1ms);
            ++t1Counter;
        }
    });
    thread t2([&] {
        while(!go);
        while(go) {
            lock_guard<mutex> lg(m);
            this_thread::sleep_for(1ms);
            ++t2Counter;
        }
    });

    go = true;
    this_thread::sleep_for(1s);
    go = false;
    t1.join();
    t2.join();

    cout << t1Counter << " " << t2Counter << endl;
}

【问题讨论】:

  • 您正在寻找的搜索词是“公平”和“饥饿”。 Windows 互斥锁不保证公平。我不熟悉Linux;看起来它的实现更公平。
  • 在解锁和重新锁定之间调用 std::this_thread::yield 可能会稍微改变画面。
  • @IgorTandetnik 变化不大。线程 t1 运行了大约 5 次而不是只运行一次,但 t2 仍然运行了数百次。
  • 意外同步问题,那些 sleep_for() 调用不是无辜的。您可以从 t2Counter 值中看出一些问题。如果 t2 运行(几乎)畅通无阻,那么该值应该接近 1000。我的水晶球说你看到的东西接近 60 或 95。在 Windows 上,睡眠只能在时钟滴答中断时完成,它每滴答 64 次默认为第二个。
  • @HansPassant 你是对的。用一些忙碌的等待睡眠替换 sleep_for 确实会产生更明智的结果。

标签: c++ multithreading mutex


【解决方案1】:

在 Windows 上,std::mutex 使用细长的读/写锁实现。这种锁实现是不公平的(意味着它不保证等待线程获取锁的顺序)。一段时间前,Windows 放弃了公平锁,因为微软认为锁护送比线程饥饿更严重。

您可以在 Microsoft 文档中阅读有关超薄读取器/写入器锁的更多信息:Slim Reader/Writer (SRW) Locks

Joe Duffy 还写了一篇关于公平与锁定车队问题的博客:Anti-convoy locks in Windows Server 2003 SP1 and Windows Vista

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-12-01
    • 2016-09-19
    • 2018-11-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多