Pthreads: pthread_cond_signal() из критической секции

У меня есть следующий фрагмент кода в потоке A, который блокирует использование pthread_cond_wait()

pthread_mutex_lock(&my_lock);     
if ( false == testCondition )        
    pthread_cond_wait(&my_wait,&my_lock); 
pthread_mutex_unlock(&my_lock);

У меня есть следующий фрагмент кода в потоке B, который сигнализирует поток A

pthread_mutex_lock(&my_lock);  
testCondition = true;
pthread_cond_signal(&my_wait);
pthread_mutex_unlock(&my_lock);

Если нет других потоков, не имеет значения, если pthread_cond_signal(&my_wait) перемещается из блока критического раздела, как показано ниже?

pthread_mutex_lock(&my_lock);  
testCondition = true;
pthread_mutex_unlock(&my_lock);
pthread_cond_signal(&my_wait);

Ответы

Ответ 1

Моя рекомендация обычно заключается в том, чтобы держать вызов pthread_cond_signal() внутри заблокированной области, но, вероятно, не по причинам, которые вы думаете.

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

Но, все это прошло мимо того, что должно быть вашей главной задачей, которая является читабельностью и правильностью вашего кода. Вероятно, вы вряд ли увидите какую-либо реальную выгоду от такого рода микро-оптимизации (помните первое правило оптимизации: сначала профиль, оптимизация второго). Однако легче думать о потоке управления, если вы знаете, что набор ожидающих потоков не может измениться между точкой, в которой вы устанавливаете условие, и посылаете сигнал. В противном случае вам нужно подумать о таких вещах, как "что если поток A устанавливает testCondition=TRUE и освобождает блокировку, а затем поток B запускается и видит, что testCondition истинно, поэтому он пропускает pthread_cond_wait() и переходит на reset testCondition до FALSE, а затем, наконец, поток A запускается и вызывает pthread_cond_signal(), который пробуждает поток C, потому что поток B фактически не ожидал, но testCondition больше не верен". Это сбивает с толку и может привести к жесткому определению условий гонки в вашем коде. По этой причине я думаю, что лучше сигнализировать, удерживая замок; Таким образом, вы знаете, что настройка условия и отправка сигнала являются атомарными относительно друг друга.

В связи с тем, что вы вызываете pthread_cond_wait(), неверно. Это возможно (хотя и редко) для pthread_cond_wait() для возврата без фактической сигнализации о состоянии, и есть другие случаи (например, гонка, описанная выше), где сигнал может в конечном итоге пробудить поток, хотя условие isn ' t истинно. Чтобы быть в безопасности, вам нужно поместить вызов pthread_cond_wait() внутри цикла while(), который проверяет условие, чтобы вы перезвонили в pthread_cond_wait(), если условие не выполняется после повторной блокировки. В вашем примере это будет выглядеть так:

pthread_mutex_lock(&my_lock);     
while ( false == testCondition ) {
    pthread_cond_wait(&my_wait,&my_lock);
}
pthread_mutex_unlock(&my_lock);

(Я также исправил, что, вероятно, опечатка в вашем исходном примере, использование my_mutex для вызова pthread_cond_wait() вместо my_lock.)

Ответ 2

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

Обычно переменные условий обычно используются следующим образом:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int go = 0;

void *threadproc(void *data) {
    printf("Sending go signal\n");
    pthread_mutex_lock(&lock);
    go = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&lock);
}

int main(int argc, char *argv[]) {
    pthread_t thread;
    pthread_mutex_lock(&lock);
    printf("Waiting for signal to go\n");
    pthread_create(&thread, NULL, &threadproc, NULL);
    while(!go) {
        pthread_cond_wait(&cond, &lock);
    }
    printf("We're allowed to go now!\n");
    pthread_mutex_unlock(&lock);
    pthread_join(thread, NULL);
    return 0;
}

Это действительно:

void *threadproc(void *data) {
    printf("Sending go signal\n");
    go = 1;
    pthread_cond_signal(&cond);
}

Однако рассмотрим, что происходит в main

while(!go) {
    /* Suppose a long delay happens here, during which the signal is sent */
    pthread_cond_wait(&cond, &lock);
}

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

Ответ 3

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

Ответ 4

Вот хорошая запись об условных переменных: Методы повышения масштабируемости приложений с использованием переменных состояния условий POSIX (см. раздел "Избегание раздел" Контекст Mutex" и пункт 7)

В нем говорится, что вторая версия может иметь некоторые преимущества в производительности. Потому что это позволяет потоку с pthread_cond_wait ждать реже.

Ответ 5

Открытые базовые спецификации Group Issue 7 IEEE Std 1003.1, выпуск 2013 года (что, насколько я могу судить, является официальной спецификацией pthread) это по этому вопросу:

Функции pthread_cond_broadcast() или pthread_cond_signal() могут быть вызываемый потоком независимо от того, владеет ли он в настоящее время мьютезом, потоки, вызывающие pthread_cond_wait() или pthread_cond_timedwait(), имеют связанные с переменной условия во время их ожидания; однако, если требуется предсказуемое поведение при планировании, тогда мьютекс должен быть заблокирован потоком, вызывающим pthread_cond_broadcast() или pthread_cond_signal().

Чтобы добавить свой личный опыт, я работал над приложением с кодом, в котором условная переменная была уничтожена (и память, содержащая ее освобожденная) потоком, который был разбужен. Мы обнаружили, что на многоядерном устройстве (iPad Air 2) pthread_cond_signal() может иногда иногда сбой, если он находился вне блокировки мьютекса, когда официант проснулся и уничтожил условную переменную до завершения pthread_cond_signal. Это было неожиданно.

Таким образом, я определенно повернулся бы к версии "сигнал внутри замка", это кажется более безопасным.