3

这段代码演示了互斥锁在两个线程之间共享,但是在thread_mutex.

(我在另一个问题中有此代码的变体,但这似乎是第二个谜。)

#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

4

2 回答 2

2

我使用 pthreads 在带有 GCC(7.3.0)的 Linux 上测试了您的代码(删除了块范围),并得到了与您相似的结果。主线程饿死了,但如果我等得够久,我偶尔会看到主线程做一些工作。

但是,我使用 MSVC (19.15) 在 Windows 上运行了相同的代码,并且没有线程被饿死。

看起来您正在使用 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+ 来使它更公平,这让我相信在实际的库实现细节、操作系统调度程序以及缓存和内存子系统效果中还有其他细微差别。

顺便说一句,谢谢你的好问题。测试和使用它真的很容易。

于 2018-10-30T22:32:39.067 回答
0

您可以在不拥有互斥锁的情况下通过产生线程(或通过休眠)给它重新调度的提示。下面相当长的睡眠可能会导致它输出#.#.#.#。完美。如果你切换到让步,你可能会得到############......但从长远来看大约是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;
    }
}
于 2018-10-30T18:34:06.130 回答