Подтвердить что ты не робот

С++ 11 std:: condition_variable: можем ли мы передать наш замок непосредственно в уведомляемый поток?

Я изучаю С++ 11 concurrency, где мой единственный предыдущий опыт с примитивами concurrency был в классе Operating Systems шесть лет назад, поэтому будьте осторожны, если сможете.

В С++ 11 мы можем написать

std::mutex m;
std::condition_variable cv;
std::queue<int> q;

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    cv.notify_one();
}

void consumer_thread() {
    std::unique_lock<std::mutex> lock(m);
    while (q.empty()) {
        cv.wait(lock);
    }
    q.pop();
}

Это отлично работает, но я оскорблен необходимостью обернуть cv.wait в цикле. Причина, по которой нам нужен цикл, мне понятна:

Consumer (inside wait())       Producer            Vulture

release the lock
sleep until notified
                               acquire the lock
                               I MADE YOU A COOKIE
                               notify Consumer
                               release the lock
                                                   acquire the lock
                                                   NOM NOM NOM
                                                   release the lock
acquire the lock
return from wait()
HEY WHERE MY COOKIE                              I EATED IT

Теперь я считаю, что одна из интересных вещей о unique_lock заключается в том, что мы можем передать ее, верно? Поэтому было бы очень элегантно, если бы мы могли это сделать:

Consumer (inside wait())       Producer

release the lock
sleep until notified
                               acquire the lock
                               I MADE YOU A COOKIE
                               notify and yield(passing the lock)
wake(receiving the lock)
return from wait()
YUM
release the lock

Теперь нет возможности вкрутить поток Vulture, потому что мьютекс остается заблокированным полностью от I MADE YOU A COOKIE до YUM. Кроме того, если notify() требует, чтобы вы передали блокировку, это хороший способ убедиться, что люди фактически блокируют мьютекс перед вызовом notify() (см. Сигнализация переменной условия (pthreads)).

Я уверен, что С++ 11 не имеет стандартной реализации этой идиомы. Какая историческая причина для этого (разве это просто так, что pthreads этого не делали, и почему это так)? Есть ли техническая причина, по которой авантюрный С++-кодер не смог реализовать эту идиому в стандартном С++ 11, назвав ее, возможно, my_better_condition_variable?

У меня также есть смутное чувство, что, возможно, я изобретаю семафоры, но я недостаточно помню из школы, чтобы узнать, насколько это точно или нет.

4b9b3361

Ответ 1

Окончательный ответ заключается в том, что pthreads этого не делали. С++ - это язык, который инкапсулирует функциональность операционной системы. С++ не является операционной системой или платформой. И поэтому он инкапсулирует существующую функциональность операционных систем, таких как linux, unix и windows.

Однако pthreads также имеет хорошее обоснование для этого поведения. Из базовых спецификаций Open Group:

Эффект заключается в том, что более чем один поток может вернуться от своего вызова к pthread_cond_wait() или pthread_cond_timedwait() в результате одного вызовите pthread_cond_signal(). Этот эффект называется "ложным" пробуждение ". Обратите внимание, что ситуация самокорректируется тем, что число потоков, которые так пробуждены, конечно; например, следующий поток для вызова pthread_cond_wait() после последовательности событий выше блоки.

Хотя эта проблема может быть решена, потеря эффективности для которое возникает редко, является неприемлемым, особенно учитывая, что нужно проверить предикат, связанный с условием переменная в любом случае. Исправление этой проблемы излишне сократило бы степень concurrency в этом базовом строительном блоке для всех операции синхронизации более высокого уровня.

Дополнительное преимущество разрешения побочных пробуждений заключается в том, что приложения принудительно закодировать цикл предикат-тестирование вокруг условия ожидания. Это также заставляет приложение переносить лишнее состояние трансляции или сигналы на той же переменной условия, которая может быть закодирована в какой-то другой части приложения. Результирующие приложения таким образом, более надежным. Поэтому IEEE Std 1003.1-2001 явно документирует что могут возникнуть побочные пробуждения.

Таким образом, в основном утверждение состоит в том, что вы можете легко построить my_better_condition_variable поверх переменной условия pthreads (или std::condition_variable) довольно легко и без ущерба для производительности. Однако, если мы разместим my_better_condition_variable на базовом уровне, то тем клиентам, которым не нужны функции my_better_condition_variable, все равно придется заплатить за них.

Эта философия поместить самый быстрый, самый примитивный дизайн в нижней части стека, с намерением, чтобы над ним можно было создавать более лучшие/медленные вещи, работает по всей С++ lib. И где С++ lib не соблюдает эту философию, клиенты часто (и справедливо) раздражаются.

Ответ 2

Если вы не хотите писать цикл, вы можете использовать перегрузку которая вместо этого использует предикат:

cv.wait(lock, [&q]{ return !q.is_empty(); });

Он определен как эквивалентный циклу, поэтому он работает так же, как и исходный код.

Ответ 3

Даже если вы можете это сделать, спецификация С++ 11 позволяет cv.wait() разблокировать ложно (для учета платформ, которые имеют такое поведение). Таким образом, даже если нет потоков стервятников (исключая аргумент о том, должны ли они существовать), потребительский поток не может ожидать, что там будет ждать куки файл и все еще нужно проверить.

Ответ 4

Я думаю, что это не безопасно:

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    cv.notify_one();
}

Вы все еще удерживаете блокировку, когда вы уведомляете другой поток, ожидающий блокировки. Таким образом, может случиться так, что другой поток сразу просыпается и пытается получить блокировку до того, как деструктор, вызванный после cv.notify_one(), освободит блокировку. Это означает, что другой поток возвращается, чтобы ждать в конце концов навсегда.

Поэтому я думаю, что это должно быть закодировано как:

void producer_thread() {
    std::unique_lock<std::mutex> lock(m);
    q.push(42);
    lock.unlock();
    cv.notify_one();
}

или если вам не нравится разблокировать вручную как

void producer_thread() {
    {
        std::unique_lock<std::mutex> lock(m);
        q.push(42);
    }
    cv.notify_one();
}