为什么多余的额外作用域块会影响std :: lock_guard行为?

时间:2018-10-30 16:26:15

标签: c++ multithreading pthreads std stdmutex

此代码演示了互斥锁在两个线程之间共享,但是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总是等待?

2 个答案:

答案 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;
    }
}