Использование мьютекса после уведомления условной переменной - PullRequest
1 голос
/ 24 января 2020

Какова причина того, что уведомленная переменная условия повторно блокирует мьютекс после получения уведомления.

Следующий фрагмент кода тупик, если unique_lock не ограничен или если мьютекс явно не разблокирован

#include <future>
#include <mutex>
#include <iostream>

using namespace std;

int main()
{
    std::mutex mtx;
    std::condition_variable cv;

    //simulate another working thread sending notification
    auto as = std::async([&cv](){   std::this_thread::sleep_for(std::chrono::seconds(2));
                                    cv.notify_all();});

    //uncomment scoping (or unlock below) to prevent deadlock 
    //{

    std::unique_lock<std::mutex> lk(mtx);

    //Spurious Wake-Up Prevention not adressed in this short sample
    //UNLESS it is part of the answer / reason to lock again
    cv.wait(lk);

    //}

    std::cout << "CV notified\n" << std::flush;

    //uncomment unlock (or scoping  above) to prevent deadlock 
    //mtx.unlock();

    mtx.lock();
    //do something
    mtx.unlock();

    std::cout << "End may never be reached\n" << std::flush;

    return 0;
}

Даже перечитывая некоторую документацию и примеры Я до сих пор не нахожу это очевидным.

Большинство примеров, которые можно найти над net, представляют собой небольшие примеры кода, которые имеют внутренняя область действия unique_lock.

Должны ли мы использовать разные мьютексы для работы с критическими секциями (мьютекс 1) и условными переменными wait и notify (мьютекс 2)?

Примечание: отладка показывает, что после завершения на этапе ожидания «внутренний» «счетчик мьютексов» (я думаю, поле __count структуры __pthread_mutex_s) переходит с 1 на 2. Он достигает 0 после разблокировки

Ответы [ 3 ]

1 голос
/ 24 января 2020

Поскольку ожидание условия может вернуться по причинам, помимо уведомления, например, сигнала, или просто потому, что кто-то другой записал в ту же строку 64-байтового кэша. Или оно могло быть уведомлено, но условие больше не выполняется, потому что его обрабатывал другой поток.

Таким образом, мьютекс заблокирован, так что ваш код может проверить его переменную условия , удерживая мьютекс. Возможно, это просто логическое значение, говорящее о готовности go.

Do NOT пропустить эту часть. Если вы это сделаете, вы будете сожалеть об этом.

1 голос
/ 24 января 2020

Давайте временно представим, что mutex не заблокирован при возврате из wait:

Поток 1:

Блокирует mutex, проверяет предикат ( что бы это ни было), и, обнаружив предикат в неприемлемой форме, ждет, пока какой-нибудь другой поток вернет его в приемлемую форму. Ожидание атомарно переводит поток 1 в спящий режим и разблокирует mutex. При разблокированном mutex некоторый другой поток получит разрешение перевести предикат в допустимое состояние (предикат не является естественно безопасным для потока).

Поток 2:

Одновременно этот поток пытается заблокировать mutex и поместить предикат в состояние, приемлемое для потока 1, чтобы продолжить его ожидание. Он должен сделать это с заблокированным mutex. mutex защищает предикат от доступа (либо чтения, либо записи) более чем к одному потоку за раз.

Как только поток 2 переводит mutex в приемлемое состояние, он уведомляет condition_variable и разблокирует mutex (порядок этих двух действий не имеет отношения к этому аргументу).

Thread 1:

Теперь поток 1 был уведомлен, и мы предполагаем гипотетическое, что mutex не блокируется при возврате с wait. Первое, что нужно сделать потоку 1, это проверить предикат, чтобы увидеть, является ли он действительно приемлемым (это может быть ложным пробуждением). Но он не должен проверять предикат без блокировки mutex. В противном случае другой поток может изменить предикат сразу после того, как этот поток проверит его, аннулировав результат этой проверки.

Так что очень первое, что этот поток должен сделать при пробуждении, это заблокировать mutex и , затем проверьте предикат.

Так что действительно удобнее то, что mutex блокируется по возвращении из wait. В противном случае ожидающий поток должен был бы вручную заблокировать его 100% времени.


Давайте еще раз посмотрим на события, когда поток 1 входит в wait: я сказал, что происходит сон и разблокировка атомарно . Это очень важно. Представьте, что поток 1 должен вручную разблокировать mutex и , а затем вызвать wait: в этом гипотетическом сценарии поток 1 может разблокировать mutex, а затем прерваться, пока другой поток получит mutex, изменяет предикат, разблокирует mutex и сигнализирует condition_variable, все до того, как поток 1 вызовет wait. Теперь поток 1 спит вечно, потому что ни один поток не увидит, что предикат нуждается в изменении, а condition_variable нужна сигнализация.

Так что обязательно , что unlock / enter - wait случается атомно . И это облегчает использование API, если lock / exit- wait также происходит атомарно.

1 голос
/ 24 января 2020

Вы пытаетесь заблокировать мьютекс дважды. Один раз с unique_lock и снова с явным вызовом mutex.lock(). Для нерекурсивного мьютекса он блокируется при попытке повторной блокировки, чтобы сообщить вам, что у вас есть ошибка.

std::unique_lock<std::mutex> lk(mtx);   // This locks for the lifetime of the unique_lock object

cv.wait(lk);  // this will unlock while waiting, but relock on return

std::cout << "CV notified\n" << std::flush;

mtx.lock();  // This attempts to lock the mutex again, but will deadlock since unique_lock has already invoked mutex.lock() in its constructor.

Исправление довольно близко к тому, что вы имеете с этими фигурными скобками без комментариев. Просто убедитесь, что на мьютексе одновременно активна только одна блокировка.

Кроме того, ваш код подвержен ложному пробуждению. Вот некоторые корректировки для вас. Вы всегда должны оставаться в ожидании до тех пор, пока условие или состояние (обычно охраняемое самим мьютексом) фактически не возникнет. Для простого уведомления подходит bool.

int main()
{
    std::mutex mtx;
    std::condition_variable cv;
    bool conditon = false;

    //simulate another working thread sending notification
    auto as = std::async([&cv, &mtx, &condition](){   
                                    std::this_thread::sleep_for(std::chrono::seconds(2));
                                    mtx.lock();
                                    condition = true;
                                    mtx.unlock();
                                    cv.notify_all();});

    std::unique_lock<std::mutex> lk(mtx); // acquire the mutex lock
    while (!condition)
    {
        cv.wait(lk);
    }

    std::cout << "CV notified\n" << std::flush;

    //do something - while still under the lock

    return 0;
}
...