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