Сигналы и условные переменные синхронизации

Последнее обновление: 06.02.2024

С помощью специальных переменных мы можем добавить к синхронизации потоков дополнительные условия. Подобные переменные представляют тип pthread_cond_t. Условные переменные используются вместе с мьютексами. Принцип работы условний синхронизации состоит в следующем. Изначально один является активным, а все остальные потоки находятся в ожидающем состоянии и ждут, когда будет выполняться некоторое условие. Потом активный поток посылает сигнал. Этот сигнал получает ждущий поток, который возобновляет свое выполнение.

Для отправки и ожидания сигнала применяются две функции:

  • pthread_cond_signal(): отправляет сигнал с помощью условной переменной

  • pthread_cond_wait(): переводит поток в ожидающее состояние, пока не будет получен сигнал с помощью условной переменной

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

Рассмотрим применение условных переменных на примере следующей программы:

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

pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;  // условная переменная
pthread_mutex_t mutex;      // мьютекс

// функция первого потока
void* thread1_work(void* arg) 
{
    pthread_mutex_lock(&mutex);             // Поток 1 блокирует мьютекс
    puts("Thread1 starts and waits for signal");

    pthread_cond_wait(&condvar, &mutex);    // Поток 1 ожидает сигнала

    puts("Thread1 received a signal");
    pthread_mutex_unlock(&mutex);         // Поток 1 освобождает мьютекс
    return NULL;
}
// функция второго потока
void* thread2_work(void* arg) 
{
    pthread_mutex_lock(&mutex);                 // Поток 2 блокирует мьютекс
    puts("Thread2 starts and does some work"); // Поток 2 выполняет условную работу
    puts("Thread2 is sending a signal");

    pthread_cond_signal(&condvar);      // ПОток 2 посылает сигнал
    puts("Thread2 sent a signal");
    pthread_mutex_unlock( &mutex );         // Поток 2 освобождает мьютекс
    return NULL;
}
int main(void) 
{
    pthread_t thread1, thread2;
    pthread_mutex_init(&mutex, NULL);

    pthread_create(&thread1, NULL, thread1_work, NULL);
    sleep(2);   // ждем, чтобы Поток 1 гарантировано начал выполняться первым
    pthread_create(&thread2, NULL, thread2_work, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    pthread_mutex_destroy(&mutex);
    return 0;
}

Данная программа покажет следующий консольный вывод:

eugene@Eugene:~/Documents/metanit$ ./main
Thread1 starts and waits for signal
Thread2 starts and does some work
Thread2 is sending a signal
Thread2 sent a signal
Thread1 received a signal
eugene@Eugene:~/Documents/metanit$

Разберем программу. Середи глобальных переменных определяем условную переменную:

pthread_cond_t condvar = PTHREAD_COND_INITIALIZER;  // условная переменная

Чтобы использовать условную переменную, ее ндо инициализировать. Это можно сделать двумя способами: присвоить ей встроенный макрос PTHREAD_COND_INITIALIZER, либо вызвать функцию pthread_cond_init(). В примере выше применяется первый вариант. Второй вариант выглядел бы так:

pthread_cond_t condvar;
....................
// в функции main
pthread_cond_init(&condvar, NULL);  // второй параметр - атрибуты условной переменной

В программе выше мы тестируем два потока. ПЕрвый поток запускат функцию thread1_work, а второй - функцию thread2_work. Первый поток блокирует мьютекс mutex. Затем он вызывает функцию pthread_cond_wait() и тем самым начинает ожидать сигнал, который передается через условную переменную condvar. Важно, что в функцию pthread_cond_wait также принимает адрес заблокированного выше мьютекса:

void* thread1_work(void* arg) 
{
    pthread_mutex_lock(&mutex);             // Поток 1 блокирует мьютекс
    puts("Thread1 starts and waits for signal");

    pthread_cond_wait(&condvar, &mutex);    // Поток 1 ожидает сигнала

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

Второй поток блокирует тот же мьютекс mutex, выполняет некоторую работу (здесь просто выводим строку на консоль) и отправляет сигнал через condvar с помощью функции pthread_cond_signal():

void* thread2_work(void* arg) 
{
    pthread_mutex_lock(&mutex);             // Поток 2 блокирует мьютекс
    puts("Thread2 starts and does some work"); // Поток 2 выполняет условную работу
    puts("Thread2 is sending a signal");

    pthread_cond_signal(&condvar);      // ПОток 2 посылает сигнал

Отправка сигнала приведет к тому, что будет разблокирован как минимум один из потоков, заблокированных по условной переменной condvar (в нашем случае это первый поток)

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

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

int signal_sent = 0;   // условие - общий ресурс

// функция первого потока
void* thread1_work(void* arg) 
{
    pthread_mutex_lock(&mutex); 
    puts("Thread1 starts and waits for signal");

    
    while (!signal_sent)       // пока не будет удовлетворять условие
    { 
        pthread_cond_wait(&condvar, &mutex);    // Поток 1 ожидает сигнала
    }
    // pthread_cond_wait(&condvar, &mutex);
    puts("Thread1 received a signal");
    pthread_mutex_unlock(&mutex); 
    return NULL;
}
// функция второго потока
void* thread2_work(void* arg) 
{
    pthread_mutex_lock(&mutex); 
    puts("Thread2 starts and does some work");
    puts("Thread2 is sending a signal");
    signal_sent = 1;                           // Поток 2 изменяет общую переменную

    pthread_cond_signal(&condvar); 
    puts("Thread2 sent a signal");
    pthread_mutex_unlock( &mutex ); 
    return NULL;
}

Здесь первый поток дополнительно проверяет значение общей переменной signal_sent:

while (!signal_sent)  { ....... }

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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850