Почему уведомление необходимо в критическом разделе? - PullRequest
1 голос
/ 08 февраля 2020

Я читаю эту книгу здесь (официальная ссылка, это бесплатно) для понимания потоков и параллельного программирования.

Вот вопрос.

  1. Почему в книге сказано, что pthread_cond_signal должно быть сделано с блокировкой, чтобы предотвратить гонку данных? Я не был уверен, поэтому я сослался на этот вопрос этот вопрос тоже), который в основном сказал "нет, это не обязательно". Почему возникает состояние гонки?
  2. Что и где описывается условие гонки?

Код и отрывок, о которых идет речь, следующие.

...
Код для пробуждения потока, который будет выполняться в каком-либо другом потоке, выглядит следующим образом:
pthread_mutex_lock(&lock);
ready = 1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&lock);
Несколько замечаний по поводу этой последовательности кода. Во-первых, при передаче сигналов (а также при изменении глобальной переменной ready) мы всегда должны держать блокировку удерживаемой. Это гарантирует, что мы не случайно введем условие гонки в наш код. ...

(пожалуйста, обратитесь к бесплатному официальному pdf для получения контекста.)

Я не смог прокомментировать небольшой вопрос в ссылке-2, так вот полный вопрос.

Редактировать 1: Я понимаю, что блокировка заключается в управлении доступом к переменной ready. Мне интересно, почему с сигналом связано состояние гонки. В частности,

Во-первых, когда мы сигнализируем [...], мы всегда проверяем, удерживается ли блокировка. Это гарантирует, что мы не случайно введем условие гонки в наш код

Edit 2: Я видел ресурсы и комментарии (по ссылкам, прокомментированным ниже, и во время моего собственного исследования), иногда на той же странице, где написано , не имеет значения или , вы должны поместить его в замок для предсказуемого поведения TM (было бы неплохо, если бы к нему можно было прикоснуться на тоже, если поведение может отличаться от ложных пробуждений). Что я должен следовать?

Редактировать 3: Я ищу больше «теоретического» ответа, а не спецификации реализации c, чтобы я мог понять основную идею. Я понимаю, что ответы на них могут быть спецификацией платформы c, но ответом, который фокусируется на основных идеях lock, mutex, condition variable, поскольку все реализации должны следовать этой семантике, возможно, добавляя свои собственные маленькие причуды. Например, wait() может внезапно просыпаться, и при плохой синхронизации передачи сигналов может происходить и на «чистых» реализациях. Упоминание об этом помогло бы.

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

Любое понимание было бы очень полезно, спасибо. Кроме того, пожалуйста, не стесняйтесь указывать мне на книги, где я могу прочитать эти понятия в detail , и где я также могу изучать C ++ с этими понятиями. Спасибо.

Ответы [ 2 ]

3 голосов
/ 09 февраля 2020
  1. Почему в книге говорится, что pthread_cond_signal должен выполняться с блокировкой, удерживаемой для предотвращения гонки данных? Я не был уверен, поэтому я сослался на этот вопрос (и этот вопрос тоже), который в основном сказал: «Нет, это не обязательно». Почему возникает состояние гонки?

В книге, не содержащей полного примера, я думаю, что предполагаемый смысл в том, что может быть гонка данных с самим резюме, если оно сигнализируется без удержания соответствующего мьютекса. Это может иметь место в некоторых реализациях CV, но в книге конкретно говорится о pthreads, и CV pthreads не подвержены такому ограничению. Также как и C ++ std::condition_variable, о котором говорят два других SO вопроса, о которых вы говорили. Таким образом, в этом смысле, книга просто неправильна .

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

Что и где описывается состояние расы?

Можно только догадываться, что имел в виду автор.


Для протокола: Правильное использование условных переменных включает в себя, прежде всего, определение того, какое условие нужно обеспечить до выполнения выполнения. Это условие обязательно будет включать общие переменные, иначе нет никаких оснований ожидать, что что-то, что делает другой поток, может измениться, если условие выполнено. В таком случае весь доступ к совместно используемым переменным должен быть защищен мьютексом, если более одного потока живы.

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

// BAD
int temp;

result = pthread_mutex_lock(m);
// handle failure results ...

temp = shared;

result = pthread_mutex_unlock(m);
// handle failure results ...

if (temp == 0) {
    result = pthread_cond_wait(cv, m);
    // handle failure results ...
}

// do something ...

Предположим, что ему было разрешено ждать резюме без удержания мьютекса, как это делает этот код. Этот код предполагает, что в какой-то момент в будущем другой поток (T2) обновит shared (под защитой мьютекса) и затем подаст сигнал CV, чтобы сообщить ожидающему (T1), что он может продолжить работу. Но что, если T2 делает это между тем, когда T1 разблокирует мьютекс и когда он начинает свое ожидание? Не имеет значения, сигнализирует ли T2 CV под защитой мьютекса или нет - T1 начнет ждать сигнала, который уже был доставлен. И сигналы CV не ставятся в очередь.

Итак, предположим, что T1 ожидает только под защитой мьютекса, как это фактически требуется. Этого не достаточно. Примите во внимание следующее:

// ALSO BAD

result = pthread_mutex_lock(m);
// handle failure results ...

if (shared == 0) {
    result = pthread_cond_wait(cv, m);
    // handle failure results ...
}

result = pthread_mutex_unlock(m);
// handle failure results ...

// do something ...

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

  • сигнала, который был законно отправлен и получен, даже если конкретное условие, представляющее интерес для T1, не выполняется
  • из сигнала, который был законно отправлен и получен, и условие выполняется при отправке сигнала, но T2 или другой поток снова изменяет общую переменную, прежде чем T1 возвращается из своего ожидания.
  • ложное возвращение из ожидания, что очень редко, но иногда случается во многих реальных реализации -world.

Ничто из этого не зависит от того, отправляет ли T2 сигнал без защиты мьютекса.

правильный способ ожидания на переменная условия должна проверять состояние интереса перед ожиданием, а затем до l oop back и check снова перед продолжением:

// OK

result = pthread_mutex_lock(m);
// handle failure results ...

while (shared == 0) {  // <-- 'while', not 'if'
    result = pthread_cond_wait(cv, m);
    // handle failure results ...
}
// typically, shared = 0 at this point

result = pthread_mutex_unlock(m);
// handle failure results ...

// do something ...

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

0 голосов
/ 09 февраля 2020
  1. Почему в книге говорится, что pthread_cond_signal должен выполняться с блокировкой, удерживаемой для предотвращения гонки данных? Я не был уверен, поэтому я сослался на этот вопрос (и этот вопрос тоже), который в основном сказал: «Нет, это не обязательно». Почему возникает состояние гонки?

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

Рассмотрим следующий фрагмент кода:

std::queue< int > events;

std::mutex mutex;
std::condition_variable cond;

// Thread 1
void consume_events()
{
    std::unique_lock< std::mutex > lock(mutex); // #1
    while (true)
    {
        if (events.empty())                     // #2
        {
            cond.wait(lock);                    // #3
            continue;
        }

        // Process an event
        events.pop();
    }
}

// Thread 2
void produce_event(int event)
{
    {
        std::unique_lock< std::mutex > lock(mutex); // #4
        events.push(event);                         // #5
    }                                               // #6

    cond.notify_one();                              // #7
}

Это классический пример одного производителя. / одна очередь данных потребителя.

В строке # 1 потребитель (поток 1) блокирует мьютекс. Затем в строке # 2 он проверяет, есть ли какие-либо события в очереди и, если их нет, в строке # 3 разблокирует mutex и блокирует. Когда происходит уведомление о переменной условия, поток разблокируется, сразу же блокирует mutex и продолжает выполнение после строки # 3 (то есть до go до строки # 2 снова).

В строке # 4 производитель (поток 2) блокирует мьютекс и в строке # 5 ставит в очередь новое событие. Поскольку мьютекс заблокирован, модификация очереди событий безопасна (строка № 5 не может выполняться одновременно со строкой № 2), поэтому гонки данных не происходит. Затем в строке № 6 мьютекс разблокируется, а в строке № 7 уведомляется переменная условия.

Возможно, что произойдет следующее:

  1. Поток 2 получает мьютекс в строке # 4.
  2. Поток 1 пытается получить мьютекс в строке # 1 или # 3 (после разблокирования предыдущим уведомлением). Поскольку мьютекс заблокирован потоком 2, поток 1 блокируется.
  3. Поток 2 помещает событие в очередь в строке № 5 и разблокирует мьютекс в строке № 6.
  4. Поток 1 разблокирует и получает мьютекс , В строке # 2 он видит, что очередь событий не пуста, и обрабатывает событие. На следующей итерации l oop очередь пуста, и поток блокируется в строке # 3.
  5. Поток 2 уведомляет поток 1 в строке # 7. Но нет событий в очереди, и поток 1 просыпается напрасно.

Хотя в этом конкретном примере дополнительное пробуждение является доброкачественным, в зависимости от содержимого l oop, оно может быть вредным , Правильный код должен вызывать notify_one перед разблокировкой мьютекса.

Другой пример - когда один поток используется для запуска какой-либо работы в другом потоке без явной очереди событий:

std::mutex mutex;
std::condition_variable cond;

// Thread 1
void process_work()
{
    std::unique_lock< std::mutex > lock(mutex); // #1
    while (true)
    {
        cond.wait(lock);                        // #2

        // Do some processing                   // #3
    }
}

// Thread 2
void initiate_work_processing()
{
    cond.notify_one();                          // #4
}

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

Проблема состоит в том, что переменная условия не буферизует уведомления и действует только на потоки, которые фактически заблокированы в нем в точке уведомления. Если нет заблокированных потоков, то уведомление ничего не делает. Это означает, что возможна следующая последовательность событий:

  1. Поток 1 получает мьютекс в строке # 1 и блоки в строке # 2.
  2. Поток 2 решает, что пора выполнять periodi c активность и уведомляет Поток 1 в строке # 4.
  3. Поток 1 разблокирует и переходит к выполнению действий (например, рендеринг кадра).
  4. Оказывается, этот кадр много работы, и когда поток 2 уведомляет поток 1 о следующем кадре в строке # 2, поток 1 все еще занят предыдущим кадром. Это уведомление пропускается.
  5. Поток 1 наконец-то завершен с кадром и блоками в строке # 2. Пользователь наблюдает, как пропущен кадр.

Выше не могло бы произойти, если бы поток 2 заблокировал mutex до того, как уведомить поток 1 в строке # 4. Если поток 1 все еще занят рендерингом кадра, поток 2 будет блокироваться, пока поток 1 не будет завершен, и только после этого выдаст уведомление.

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

Что и где описывается состояние гонки?

Определение гонки данных зависит от модели памяти, используемой в конкретной среде. Это означает, в первую очередь, вашу модель памяти на языке программирования и может включать базовую модель аппаратной памяти (если язык программирования основан на модели аппаратной памяти, как, например, в Assembler).

C ++ определяет данные обрабатываются следующим образом:

Когда оценка выражения записывает данные в ячейку памяти, а другая оценка считывает или изменяет эту же ячейку памяти, считается, что выражения конфликтуют. Программа с двумя конфликтующими оценками имеет гонку данных, если только

  • обе оценки не выполняются в одном потоке или в одном и том же обработчике сигналов, или
  • обе конфликтующие оценки являются атомами c операции (см. std::atomic) или
  • одна из конфликтующих оценок происходит раньше другой (см. std::memory_order)

Если происходит гонка данных, поведение программы не определено

Таким образом, в основном, когда несколько потоков одновременно обращаются к одной и той же ячейке памяти (с помощью средств, отличных от std::atomic), и хотя бы один из потоков изменяет данные в этой ячейке, то есть гонки данных .

...