pthreads: pthread_cond_signal () из критической секции - PullRequest
14 голосов
/ 29 октября 2009

У меня есть следующий фрагмент кода в потоке 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);

Ответы [ 5 ]

17 голосов
/ 07 ноября 2009

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

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

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

На связанной ноте способ, которым вы звоните pthread_cond_wait(), неверен. Возможно (хотя и редко) для pthread_cond_wait() возврата без фактической сигнализации переменной условия, и есть другие случаи (например, гонка, которую я описал выше), когда сигнал может закончить пробуждение потока, даже если условие не ' правда. Чтобы быть в безопасности, вам нужно поместить вызов 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 голосов
/ 19 ноября 2009

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

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

#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 останется в ожидании - возможно, навсегда. Вот почему вы хотите сигнализировать с заблокированным мьютексом.

1 голос
/ 29 октября 2009

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

0 голосов
/ 21 апреля 2016

Базовые спецификации Open 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. Это было довольно неожиданно.

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

0 голосов
/ 20 декабря 2012

Вот хорошее описание условных переменных: Методы улучшения масштабируемости приложений, использующих переменные условия потока POSIX (см. Раздел «Предотвращение разногласий по Mutex» и пункт 7)

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

...