Когда мьютекс преобразуется в условную переменную? - PullRequest
0 голосов
/ 30 октября 2018

I обнаружил проблему с мьютексным голодом , и предлагаемый ответ - вместо этого использовать условные переменные

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 патологически истощает один мьютекс при нормальных обстоятельствах , может показаться, что использование мьютекса никогда не является разумным вариантом в условия реального мира.

Когда, если вообще, было бы лучше использовать мьютекс вместо условной переменной?

Ответы [ 2 ]

0 голосов
/ 30 октября 2018

Вы должны почти всегда использовать мьютекс. Это должно быть вашим средством синхронизации по умолчанию. Это легко и эффективно. В двух случаях вы должны использовать условную переменную:

  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 ();
    }
}

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

Обратите внимание на две точки в начале? Один поток мог запускаться дважды до начала другого потока. Эта «справедливая» блокировка позволяет одному потоку продолжать продвижение вперед, если другой поток не ожидает.

Эта реализация удерживает мьютекс только во время получения или освобождения справедливой блокировки, поэтому конкуренция с мьютексом минимальна. Условная переменная используется для того, чтобы один поток мог ожидать другого, чтобы продвинуться вперед. Сам код гарантирует, что требуемый поток продвигается вперед (блокируя поток, который мы не хотим продвигать вперед), должным образом позволяя реализации сфокусироваться на том, чтобы позволить коду сделать максимально возможный прогресс вперед.

0 голосов
/ 30 октября 2018

Переменные условия и взаимные исключения служат двум различным целям.

Мьютекс обычно используется для предотвращения одновременного манипулирования одними и теми же данными двумя разными потоками. Примером может служить случай, когда вам необходимо обновить несколько связанных элементов общей структуры данных атомарным способом. Мьютекс также можно использовать для защиты фрагментов кода, которые не являются потокобезопасными (например, все контейнеры STL), что на самом деле является еще одним способом сказать то же самое.

Условная переменная полезна, когда поток A хочет «передать» работу потоку B. В реальной жизни поток A может поместить некоторый вид рабочего элемента в очередь и затем сигнализировать об условной переменной. Затем поток B активируется и обрабатывает все элементы, находящиеся в очереди, а затем снова ожидает условную переменную.

Я нахожу пример в вопросе довольно надуманным. Мне никогда не нужно было «пинг-понг» между двумя такими занятыми петлями. Скорее, обычно существуют отношения поставщик / потребитель, например, при обработке аудио, когда входящее аудио может поступать в поток с высоким приоритетом и должно быть буферизовано и затем передано потоку с более низким приоритетом для (скажем) записи на диск.

...