Предотвращение потерянного пробуждения, когда обновление условия является блокирующей функцией

Я пишу цикл событий, который переходит в спящий режим, когда нет работы, ожидая переменной условия «работа для выполнения» (work_to_do). Эта условная переменная может быть уведомлена разными потоками на основе различных событий. Когда событие происходит в другом потоке, оно уведомляет переменную условия, пробуждая цикл событий, который затем проверяет условия, которые могли вызвать уведомление, зацикливается до тех пор, пока не закончится работа, а затем снова ждет. Одно из условий задается блокирующей функцией (WaitForMessage()).

Поток цикла событий:

std::lock_guard<std::mutex> lock(work_to_do_lock);
for (;;) {
  if (condition1) {
    // Act on condition 1.
  } else if (condition2) {
    // Act on condition 2.
  } else if (HasMessage()) {
    // Act on receiving message.
  } else {
    work_to_do.wait(lock);
  }
}

Поток, который обрабатывает уведомление от блокирующей функции:

for (;;) {
  // Wait for message to be received (blocking). Once it returns you are
  // guaranteed that HasMessage() will return true.
  WaitForMessage();

  // Wake-up the main event loop.
  work_to_do.notify_one();
}

Основной поток блокирует мьютекс, защищающий условную переменную (work_to_do_lock), перед входом в цикл событий и передает ее в вызов wait(), когда нет работы. Чтобы избежать потери пробуждения, общий совет заключается в том, что все уведомители должны удерживать блокировку при обновлении своих состояний состояния. Однако, если бы вы защищали вызов WaitForMessage() с помощью work_to_do_lock, вы могли бы предотвратить пробуждение цикла событий другими сигналами.

Решение, которое я придумал, состоит в том, чтобы получить и снять блокировку после WaitForMessage(), но до notify_one():

for (;;) {
  // Wait for message to be received (blocking). Once it returns you are
  // guaranteed that HasMessage() will return true.
  WaitForMessage();

  {
    std::lock_guard<std::mutex> lock(work_to_do_lock);
  }

  // Wake-up the main event loop.
  work_to_do.notify_one();
}

Это должно избежать проблемы с потерянным пробуждением, поскольку больше невозможно, чтобы условие стало истинным (WaitForMessage() для возврата) и notify_one() произошло между проверкой условия (HasMessage()) и wait().

Альтернативный подход — не полагаться на HasMessage() и просто обновить общую переменную, которую мы могли бы защитить с помощью блокировки:

for (;;) {
  // Wait for message to be received (blocking). Once it returns you are
  // guaranteed that HasMessage() will return true.
  WaitForMessage();

  {
    std::lock_guard<std::mutex> lock(work_to_do_lock);
    has_message = true;
  }

  // Wake-up the main event loop.
  work_to_do.notify_one();
}

Соответствующий цикл обработки событий, проверяющий новый предикат условия:

std::lock_guard<std::mutex> lock(work_to_do_lock);
for (;;) {
  if (condition1) {
    // Act on condition 1.
  } else if (condition2) {
    // Act on condition 2.
  } else if (has_message) {
    has_message = false;
    // Act on receiving message.
  } else {
    work_to_do.wait(lock);
  }
}

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


person JDN    schedule 14.01.2020    source источник
comment
Недостаток вашего дизайна в том, что вы condition_variable::wait без предиката (и, возможно, без тайм-аута). Здесь — это какая-то статья, объясняющая эту проблему. Тогда вашим предикатом может быть какой-то атомарный флаг, установленный рабочими процессами, и у вас не должно возникнуть никаких проблем.   -  person pptaszni    schedule 14.01.2020
comment
@pptaszni Сообщение, на которое вы ссылаетесь, указывает на две потенциальные проблемы: ложные пробуждения и потерянные пробуждения. В своем посте я описываю, почему я думаю, что этот дизайн предотвращает последнее, а что касается первого, то проверка предиката является явной в проверках if/else if, которые продолжают else, где ожидается условная переменная (внутри безусловного цикла for, поэтому они всегда будут проверяться до и после ожидания). Пока эти два условия избегаются, вызов wait() безопасен (даже без предиката или тайм-аута).   -  person JDN    schedule 14.01.2020
comment
Что ж, ваш пример, кажется, имеет дело с ложным пробуждением, но не рекомендуемым способом, что означает, что вы все равно повторно получаете блокировку, выполняете все условия и затем снова вызываете ожидание. Сможете ли вы потерять пробуждение или нет, зависит от других ваших воркеров, установили ли они правильные флаги под блокировкой или нет. Рабочий с WaitForMessage выглядит нормально. Я предпочитаю установить (большой) тайм-аут, чтобы увидеть сообщение об ошибке, а не видеть, что мое приложение заблокировано в случае, если я совершу ошибку. В любом случае, просто подождите с предикатом и избавьте себя от неприятностей.   -  person pptaszni    schedule 14.01.2020
comment
@pptaszni Я не согласен с тем, что это не рекомендуемый способ борьбы с ложными пробуждениями. Если вы прочтете исходный код реализации предикатной версии wait(), вы увидите, что он просто оборачивает обычное ожидание в цикл while, который проверяет функцию предиката. Ваше утверждение, в котором говорится, что в моем коде вы все еще повторно получаете блокировку, выполняете все условия, а затем снова вызываете ожидание, также является именно тем, что будет делать wait() с предикатом, нет никакой разницы в производительности. На самом деле, использование предиката приведет к пустой трате работы, так как мне в любом случае нужно явно проверить каждое условие, чтобы выполнить отправку.   -  person JDN    schedule 14.01.2020


Ответы (1)


Ваш подход работает, но он менее эффективен, чем тот, который повторно использует любую синхронизацию, позволяющую безопасно вызывать WaitForMessage и HasMessage одновременно (или, другими словами, использует ваш work_to_do_lock для обновления значения HasMessage, а не (скажем) с использованием атом для него). Конечно, если это недоступно для этого кода, это лучшее, что вы можете сделать, поскольку вам нужно взаимное исключение для других условий.

person Davis Herring    schedule 15.01.2020