Мьютексы

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

Иногда в потоках используются некоторые разделяемые ресурсы, общие для всей программы. Это могут быть общие переменные, файлы, другие ресурсы. Для разграничения доступа потоков к общим ресурсам могут применяться мюьтексы (mutex - сокращение от "mutual exclusion"). Мьютекс представляет объект, который может находиться в двух состояниях: заблокированном и разблокированном. На уровне языка Си мьютекс представлен типом pthread_mutex_t.

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

  • pthread_mutex_lock(): переводит мьютекс из разблокированного на заблокированное. Если мьютекс заблокирован, то поток, который хочет получить этот мьютекс, должен ждать, когда другой поток освободит его (разблокирует).

  • pthread_mutex_unlock(): переводит мьютекс из заблокированного на рабзлокированное. После этого мьютекс свободен для использования другими потоками.

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

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

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

int value = 0;  // общий ресурс


void* do_work(void* thread_id)
{
    value = 1;
    int id = *(int*)thread_id;     // получаем id потока
    for (int n = 0; n < 5; n++) 
    {
        printf("Thread %d: %d\n", id, value );
        value += 1;
        sleep(1);
    }
    return NULL;
}
int main(void) 
{
    pthread_t t1, t2;       // два потока для демонстрации
    int t1_id = 1, t2_id = 2;  // идентификаторы потоков
    pthread_create(&t1, NULL, do_work, &t1_id);
    pthread_create(&t2, NULL, do_work, &t2_id);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    
    return 0;
}

Здесь у нас запускаются два потока, которые вызывают функцию do_work и которые работают с общей переменной value. Чтобы идентифицировать каждый поток, в функцию do_work передается числовой идентификатор потока. И мы предполагаем, что функция do_work выведет все значения value от 1 до 5. И так для каждого потока. Однако в реальности в процессе работы будет происходить переключение между потоками, и значение переменной value становится непредсказуемым. Например, в моем случае я получил следующий консольный вывод (он может в каждом конкретном случае различаться):

eugene@Eugene:~/Documents/metanit$ ./main
Thread 1: 1
Thread 2: 1
Thread 2: 3
Thread 1: 4
Thread 2: 5
Thread 1: 6
Thread 2: 7
Thread 1: 8
Thread 2: 9
Thread 1: 10
eugene@Eugene:~/Documents/metanit$ 

Изменим программу, применив мьютексы:

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

int value = 0;  // общий ресурс

pthread_mutex_t m; // мьютекс для разграничения доступа

void* do_work(void* thread_id)
{
    int id = *(int*)thread_id;
    pthread_mutex_lock( &m );   // поток блокирует мьютекс
    value = 1;                  // начало работы с обшим ресурсом
    for (int n = 0; n < 5; n++) 
    {
        printf("Thread %d: %d\n", id, value );
        value += 1;
        sleep(1);
    }
    pthread_mutex_unlock( &m );     // поток освобождает мьютекс
    return NULL;
}
int main(void) 
{
    pthread_t t1, t2;
    int t1_id = 1, t2_id = 2; 
    pthread_mutex_init(&m, NULL); //  инициализация мьютекса
    pthread_create(&t1, NULL, do_work, &t1_id);
    pthread_create(&t2, NULL, do_work, &t2_id);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    
    pthread_mutex_destroy(&m);  // удаление мьютекса
    return 0;
}

Итак, здесь в принципе та же программа за исключением применения мьютекса. Сам мьютекс определен как глобальная переменная:

pthread_mutex_t m;

Для инициализации мьютекса в функции main применяется функция pthread_mutex_init(). Первый параметр функции - адрес мьютекса, а второй - атрибуты (в данном случае они нам не важны, поэтому передаем NULL):

pthread_mutex_init(&m, NULL);

Каждый поток также выполняет функцию do_work, только теперь та область кода, где используется общий ресурс - переменная value блокируется мьютексом:

void* do_work(void* thread_id)
{
    int id = *(int*)thread_id;
    pthread_mutex_lock(&m);   // поток блокирует мьютекс
    
    // критическая секция - работа с ресурсом value

    pthread_mutex_unlock(&m);     // поток освобождает мьютекс
    return NULL;
}

Сначала один поток блокирует мьютекс, вызвав функцию pthread_mutex_lock(&m) и начинает работать с переменной value. В это время второй поток ожидает разблокировки мьютекс.

После того, как первый поток закончил работу, он вызывает функцию pthread_mutex_unlock(&m) и тем самым освобождает мьютекс. И второй поток блокирует мьютекс и начинает работать с переменной value.

После завершения работы с мьютексом он удаляется функцией pthread_mutex_destroy()

pthread_mutex_destroy(&m);  // удаление мьютекса

Консольный вывод:

eugene@Eugene:~/Documents/metanit$ ./main
Thread 1: 1
Thread 1: 2
Thread 1: 3
Thread 1: 4
Thread 1: 5
Thread 2: 1
Thread 2: 2
Thread 2: 3
Thread 2: 4
Thread 2: 5
eugene@Eugene:~/Documents/metanit$ 

Взаимоблокировка

Хотя мьютексы очень простой и удобный инструмент, при ненадлежащем применении нескольких мьютексов мы можем столкнуться с ситуацией взаимоблокировки (deadlock). Например, возьмем следующую ситуацию:

pthread_mutex_t A;
pthread_mutex_t B;
void* thread1(void* arg)
{
    pthread_mutex_lock(&A);
    pthread_mutex_lock(&B);
    // ........................
    pthread_mutex_unlock(&B);
    pthread_mutex_unlock(&A);
    return NULL;
}

void* thread2(void* arg)
{
    pthread_mutex_lock(&B);
    pthread_mutex_lock(&A);
    // ........................
    pthread_mutex_unlock(&A);
    pthread_mutex_unlock(&B);
    return NULL;
}

Если предположим, что первый поток запускает функцию thread1, а второй поток запускает функцию thread2, то мы можем столкннуться с ситуацией взаимоблокировки. Потому что первый поток блокирует мьютекс A, а для продолжения ему требуется мьютекс B. Тогда как второй поток блокирует мьютекс B и для продолжения ему требуется мьютекс A. Чтобы избежать подобной ситуации во всех потоках мьютексы следует блокировать и освобождать в одном и том же порядке.

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