互斥量什么时候可以预定义为条件变量?

When is a mutex prefarable to a condition variable?

discovered a mutex starvation problem 并且建议的答案是改用条件变量

int main ()
{
    std::mutex m;
    std::condition_variable cv;
    std::thread t ([&] ()
    {
        while (true)
        {
            std::unique_lock<std::mutex> lk(m);
            std::cerr << "#";
            std::cerr.flush ();
            cv.notify_one();
            cv.wait(lk);
        }
    });

    while (true)
    {
        std::unique_lock<std::mutex> lk(m);
        std::cerr << ".";
        std::cerr.flush ();
        cv.notify_one();
        cv.wait(lk);
    }
}

由于我平台上的饥饿问题甚至使简单的演示情况几乎无法使用,我有什么理由想要条件变量而不是互斥体?

例如,如果不存在互斥公平性保证,并且我可以预期我的普通 Ubuntu 平台在正常情况下 病态地饿死一个互斥 ,这似乎就像在现实世界条件下使用互斥锁从来都不是明智的选择。

如果有的话,什么时候使用互斥量而不是条件变量会更好?

条件变量和互斥量有两个不同的用途。

互斥锁一般用来防止两个不同的线程同时操作同一个数据。例如,当您需要以原子方式更新共享数据结构的多个相关成员时。互斥也可以用来保护非线程安全的代码段(例如所有的 STL 容器),这实际上只是同一件事的另一种说法。

当线程 A 想要 'hand off' 为线程 B 工作时,条件变量很有用。在现实生活中,线程 A 可能会将某种工作项放在队列中,然后向条件变量发出信号。然后线程 B 唤醒并处理任何排队的项目,然后再次等待条件变量。

我发现问题中的示例相当做作。我从来不需要像那样在两个繁忙的循环线程之间 'ping-pong'。相反,通常存在供应商/消费者关系,例如在音频处理中,传入的音频可能到达高优先级线程,必须进行缓冲,然后交给低优先级线程(比如)写入磁盘。

您应该几乎总是使用互斥量。它应该是您的默认同步方式。它轻巧高效。有两种情况你应该使用条件变量:

  1. 互斥体大部分时间都被锁定的非常罕见的情况。通常,线程完成的大部分工作不会影响共享数据,因此大多数线程完成大部分工作时不持有互斥锁。在大多数时间都持有锁的极少数情况下,互斥体不是一个合适的选择。你的例子属于这种情况。

  2. 较少见的情况是您需要一个线程等待另一个线程完成某事。例如,假设您有一个保存一些数据的缓存。线程可能会获取缓存上的锁并查看缓存中是否有某些数据。如果是这样,它将使用缓存的副本。如果没有,它将自己计算出数据,然后将其放入缓存中。但是,如果一个新线程在缓存中查找数据,而另一个线程正在处理数据呢?该线程只需要等待另一个线程将数据放入缓存。互斥锁不适合让一个线程等待另一个线程。

对于您有一些共享数据需要被多个线程短暂访问的典型情况,互斥锁是最合适的。在绝大多数情况下,当线程试图获取互斥量时,互斥量将是无人拥有的,因此它是否提供公平性根本无关紧要。

等待应该很少见。如果一个线程大部分时间都在等待另一个线程,那么它通常不应该是它自己的线程。在您的示例中,您有两个线程,除非另一个线程停止,否则两个线程都无法取得任何进展,并且其中一个线程可以在另一个线程停止时取得无限进展。这在任何现实情况下几乎都不会发生,通常表明存在严重的设计问题。

如果你担心公平,那你就做错了。你的代码应该只做你想做的工作。如果您的代码执行了错误的工作,那么您就没有编写代码来完成您最想完成的工作。解决这个问题。一般来说,实现的工作是让你的代码尽可能多地向前推进,而你的工作就是让你的代码向前推进。

这里是公平锁的一个快速但肮脏的实现,它检查另一个线程是否已经在等待锁并给它一个获取锁的机会:

#include <mutex>
#include <thread>
#include <condition_variable>
#include <iostream>

class fair_lock
{
    private:

    std::mutex m;
    std::condition_variable cv;
    int locked = 0;
    int waiter_count = 0;

    public:

    void lock()
    {
        std::unique_lock<std::mutex> lk(m);

        ++waiter_count;

        // if someone was already waiting, give them a turn
        if (waiter_count > 1)
            cv.wait(lk);

        // wait for lock to be unlocked
        while (locked != 0)
            cv.wait(lk);

        --waiter_count;
        locked = 1;
    }

    void unlock()
    {
        std::unique_lock<std::mutex> lk(m);
        locked = 0;
        cv.notify_all();
    }
};

int main ()
{
    fair_lock m;

    std::thread t ([&] ()
    {
        while (true)
        {
            std::unique_lock<fair_lock> lk(m);
            std::cerr << "#";
            std::cerr.flush ();
        }
    });

    while (true)
    {
        std::unique_lock<fair_lock> lk(m);
        std::cerr << ".";
        std::cerr.flush ();
    }
}

..#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.#.

注意到开头的两个点了吗?在另一个线程开始之前,一个线程能够 运行 两次。如果没有其他线程在等待,此 "fair" 锁允许一个线程继续前进。

此实现仅在获取或释放公平锁时持有互斥锁,因此对互斥锁的争用最小。条件变量用于允许一个线程等待另一个线程取得进展。代码本身确保所需的线程向前推进(通过阻塞我们不想向前推进的线程),适当地让实现专注于让代码尽可能多地向前推进。