Иногда в потоках используются некоторые разделяемые ресурсы, общие для всей программы. Это могут быть общие переменные, файлы, другие ресурсы. Для разграничения доступа потоков к общим ресурсам могут применяться мюьтексы (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. Чтобы избежать подобной ситуации во всех потоках мьютексы следует блокировать и освобождать в одном и том же порядке.