Самый дешевый способ пробудить несколько ожидающих потоков без блокировки - PullRequest
2 голосов
/ 20 марта 2012

Я использую boost :: thread для управления потоками.В моей программе у меня есть пул потоков (рабочих), которые иногда активируются для выполнения некоторой работы одновременно.

Теперь я использую boost :: condition_variable: и все потоки ожидают внутри boost :: condition_variable :: wait ()вызовите их собственные объекты conditional_variableS.

Могу ли я ИЗБЕГАТЬ, используя мьютексы в классической схеме, когда я работаю с условными переменными?Я хочу пробуждать потоки, но не нужно передавать им некоторые данные, поэтому не нужно блокировать / разблокировать мьютекс во время процесса пробуждения, зачем мне тратить на это процессор (но да, я должен помнить оложные пробуждения)?

Вызов boost :: condition_variable :: wait () пытается REACQUIRE блокирующий объект, когда CV получает уведомление.Но мне не нужно это точное средство.

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

Ответы [ 4 ]

3 голосов
/ 20 марта 2012

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

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

Одним из распространенных шаблонов является использование мьютекса, переменной условия и целого числа состояния. Чтобы заблокировать, сделайте это:

  1. Получить мьютекс.

  2. Скопировать значение целого числа состояния.

  3. Блокировать переменную условия, освобождая мьютекс.

  4. Если целое число состояния такое же, как и при копировании, перейдите к шагу 3.

  5. Отключить мьютекс.

Чтобы разблокировать все темы, сделайте следующее:

  1. Получить мьютекс.

  2. Увеличение целого числа состояния.

  3. Трансляция переменной условия.

  4. Отключить мьютекс.

Обратите внимание, как шаг 4 алгоритма блокировки проверяет, завершен ли поток ожидания? Обратите внимание, как этот код отслеживает, была ли разблокировка с тех пор, как поток решил заблокировать? Вы должны сделать это, потому что переменные условия не делают это сами. (И именно поэтому вам нужно повторно захватить объект блокировки.)

Если вы попытаетесь удалить целочисленное состояние, ваш код будет работать непредсказуемо. Иногда вы будете блокировать слишком долго из-за пропущенных пробуждений, а иногда вы не будете блокировать достаточно долго из-за ложных пробуждений. Только целое состояние (или аналогичный предикат), защищенный мьютексом, сообщает потокам, когда ждать, а когда прекратить ожидание.

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

0 голосов
/ 07 ноября 2013

boost :: condition_variable :: notify _ * (блокировка) НЕ требует, чтобы вызывающая сторона удерживала блокировку мьютекса.Это хорошее улучшение по сравнению с моделью Java в том, что она отделяет уведомление о потоках с удержанием блокировки.

Строго говоря, это означает, что следующий бессмысленный код ДОЛЖЕН СДЕЛАТЬ то, что вы просите:

lock_guard lock(mutex);
// Do something
cv.wait(lock);
// Do something else


unique_lock otherLock(mutex);
//do something
otherLock.unlock();
cv.notify_one();

Не думаю, что вам нужно сначала вызывать otherLock.lock ().

0 голосов
/ 21 марта 2012

Вообще говоря, вы не можете.

Предполагается, что алгоритм выглядит примерно так:

ConditionVariable cv;

void WorkerThread()
{
  for (;;)
  {
    cv.wait();
    DoWork();
  }
}

void MainThread()
{
  for (;;)
  {
    ScheduleWork();
    cv.notify_all();
  }
}

ПРИМЕЧАНИЕ. В этом псевдокоде я намеренно опустил любую ссылку на мьютексы. Для целей этого примера мы предположим, что ConditionVariable не требует мьютекса.

В первый раз через MainTnread () работа ставится в очередь, а затем уведомляет WorkerThread (), что она должна выполнить свою работу. В этот момент могут произойти две вещи:

  1. WorkerThread () завершает DoWork (), прежде чем MainThread () сможет завершить ScheduleWork ().
  2. MainThread () завершает ScheduleWork (), прежде чем WorkerThread () сможет завершить DoWork ().

В случае # 1 WorkerThread () снова засыпает на резюме и вызывается следующим cv.notify (), и все хорошо.

В случае # 2 MainThread () возвращается и уведомляет ... никто и продолжает работу. Между тем, WorkerThread () в конечном итоге возвращается в свой цикл и ожидает CV, но теперь это одна или несколько итераций позади MainThread ().

Это известно как "потерянное пробуждение". Это похоже на пресловутое «ложное пробуждение» в том, что два потока теперь имеют разные представления о том, сколько было выполнено notify (). Если вы ожидаете, что два потока будут поддерживать синхронизацию (и обычно это так), вам нужен какой-то общий примитив синхронизации для управления им. Это где мьютекс приходит. Он помогает избежать потерянных пробуждений, которые, возможно, являются более серьезной проблемой, чем ложное разнообразие. В любом случае, последствия могут быть серьезными.

ОБНОВЛЕНИЕ: Для дальнейшего обоснования этого дизайна, см. Этот комментарий одного из оригинальных авторов POSIX: https://groups.google.com/d/msg/comp.programming.threads/cpJxTPu3acc/Hw3sbptsY4sJ

Ложные пробуждения - это две вещи:

  • Тщательно напишите свою программу и убедитесь, что она работает, даже если вы что-то пропустил.
  • Поддержка эффективных реализаций SMP

Могут быть редкие случаи, когда «абсолютно, параноикально правильно» выполнение условия пробуждения при одновременном ожидании и сигнал / трансляция на разных процессорах, потребует дополнительных синхронизация, которая замедлит ВСЕ операции с условными переменными при этом не принося пользы в 99,99999% всех звонков. Стоит ли накладные расходы? Ни за что!

Но, на самом деле, это оправдание, потому что мы хотели заставить людей написать безопасный код. (Да, это правда.)

0 голосов
/ 20 марта 2012

Для управления потоками, выполняющими параллельные задания, есть хороший примитив, называемый барьером.

Барьер инициализируется некоторым положительным целым значением N, представляющим, сколько потоков он содержит.Барьер имеет только одну операцию: wait.Когда N потоки вызывают ожидание, барьер освобождает их всех.Кроме того, одному из потоков дается специальное возвращаемое значение, указывающее, что это «последовательный поток»;этот поток будет выполнять какую-то особую работу, например, интегрировать результаты вычислений из других потоков.

Ограничение состоит в том, что данному барьеру необходимо знать точное количество потоков.Он действительно подходит для ситуаций параллельной обработки.

POSIX добавил барьеры в 2003 году. Поиск в Интернете показывает, что Boost также имеет их.

http://www.boost.org/doc/libs/1_33_1/doc/html/barrier.html

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...