Вы должны почти всегда использовать мьютекс. Это должно быть вашим средством синхронизации по умолчанию. Это легко и эффективно. В двух случаях вы должны использовать условную переменную:
Очень редкий случай, когда мьютекс будет заблокирован большую часть времени. Как правило, большая часть работы, выполняемой потоками, не влияет на общие данные, поэтому большинство потоков выполняет большую часть своей работы без мьютексов. В редком случае, когда блокировка будет проводиться большую часть времени, мьютекс не подходит. Ваш пример относится к этому случаю.
Менее редкий случай, когда вам нужен один поток, чтобы дождаться завершения другого потока. Например, скажем, у вас есть кеш, который содержит некоторые данные. Поток может получить блокировку кеша и посмотреть, есть ли в кеше некоторые данные. Если это так, он будет использовать кэшированную копию. Если нет, то он обработает сами данные и затем поместит их в кеш. Но что, если новый поток ищет данные в кеше, а другой обрабатывает их? Этот поток должен просто подождать, пока другой поток поместит данные в кэш. Мьютекс плохо подходит для того, чтобы заставить один поток ждать другого.
В типичном случае, когда у вас есть общие данные, к которым требуется краткий доступ более чем одному потоку, мьютекс является наилучшим выбором. В подавляющем большинстве случаев мьютекс будет недоступен, когда поток попытается его получить, поэтому, будет ли он честным или нет, не будет иметь никакого значения.
Ожидание должно быть редким. Если один поток тратит большую часть своего времени на ожидание другого потока, обычно это не должен быть его собственный поток. В вашем примере у вас есть два потока, в которых ни один из них не может добиться какого-либо прогресса, если другой не остановлен, и один из них может достичь бесконечного прогресса, пока другой остановлен. Это почти никогда не происходит в любой реальной ситуации и обычно указывает на серьезную проблему проектирования.
Если вы беспокоитесь о справедливости, вы делаете что-то не так. Ваш код должен выполнять только ту работу, которую вы хотите. Если ваш код выполняет неправильную работу, то вы не создавали код для выполнения той работы, которую вы больше всего хотели выполнить. Исправь это. Вообще говоря, задача реализации состоит в том, чтобы ваш код достиг максимально возможного прогресса, а ваша задача - сделать так, чтобы ваш код развивался правильно.
Вот быстрая и грязная реализация справедливой блокировки, которая проверяет, ожидает ли блокировка другой поток, и дает ему возможность получить блокировку:
#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 ();
}
}
.. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. # . #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. #. # . #.
Обратите внимание на две точки в начале? Один поток мог запускаться дважды до начала другого потока. Эта «справедливая» блокировка позволяет одному потоку продолжать продвижение вперед, если другой поток не ожидает.
Эта реализация удерживает мьютекс только во время получения или освобождения справедливой блокировки, поэтому конкуренция с мьютексом минимальна. Условная переменная используется для того, чтобы один поток мог ожидать другого, чтобы продвинуться вперед. Сам код гарантирует, что требуемый поток продвигается вперед (блокируя поток, который мы не хотим продвигать вперед), должным образом позволяя реализации сфокусироваться на том, чтобы позволить коду сделать максимально возможный прогресс вперед.