Книга: Основы программирования в Linux

Синхронизация с помощью мьютексов

Синхронизация с помощью мьютексов

Другой способ синхронизации доступа в многопоточных программах — применение мьютексов (сокращение от mutual exclusions — взаимные исключения) или исключающих семафоров, которые разрешают программистам "запирать" объект так, что только один поток может обратиться к нему.

Базовые функции, необходимые для использования мьютексов, очень похожи на функции семафоров. Они объявляются следующим образом:

#include <рthread.h>
int pthread_mutex_init(pthread_mutex_t* mutex,
 const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t* mutex);
int pthread_mutex_unlock(pthread mutex_t* mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

Как обычно, в случае успешного завершения возвращается 0 и код ошибки в случае аварийного завершения, но переменная errno не задается, вам придется использовать код возврата.

Как и функции семафоров, функции мьютексов принимают указатель на предварительно объявленный объект, в данном случае типа pthread_mutex_t. Дополнительный параметр атрибутов в функции pthread_mutex_init позволяет задать атрибуты мьютекса, управляющие его поведением. По умолчанию тип атрибута — "fast". У него есть небольшой недостаток: если ваша программа попытается вызвать функцию pthread_mutex_lock для мьютекса, который уже заблокирован, программа блокируется. Поскольку поток, удерживающий блокировку, в данный момент заблокирован, мьютекс никогда не будет открыт, и программа попадает в тупиковую ситуацию. Есть возможность изменить атрибуты мьютекса так, чтобы он либо проверял наличие такой ситуации и возвращал ошибку, либо действовал рекурсивно и разрешал множественные блокировки тем же самым потоком, если будет такое же количество разблокировок в дальнейшем.

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

Выполните упражнение 12.4. 

Упражнение 12.4. Мьютекс потока

Далее приводится еще одна модификация исходной программы thread1.с, но значительно измененная. На этот раз вы уделите особое внимание доступу к вашим важным переменным и примените мьютекс для того, чтобы быть уверенными в том, что они доступны в любой момент времени только одному потоку. Для легкости чтения текста примера мы пропустили некоторые проверки ошибок при возвратах из мьютекса, заблокированного и открытого. В рабочем программном коде вы обязательно должны проверять эти возвращаемые значения. Далее приведен текст новой программы thread4.c.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
#include <semaphore.h>
void *thread_function(void *arg);
pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;
int main() {
 int res;
 pthread_t a_thread;
 void *thread_result;
 res = pthread_mutex_init(&work_mutex, NULL);
 if (res != 0) {
  perror("Mutex initialization failed");
  exit(EXIT_FAILURE);
 }
 res pthread_create(&a_thread, NULL, thread_function, NULL);
 if (res != 0) {
  perror("Thread creation failed");
  exit(EXIT_FAILURE);
 }
 pthread_mutex_lock(&work_mutex);
 printf("Input same text. Enter 'end' to finishn");
 while (!time_to_exit) {
  fgets (work_area, WORK_SIZE, stdin);
  pthread_mutex_unlock(&work_mutex);
  while(1) {
   pthread_mutex_lock(&work_mutex);
   if (work_area[0] != '') {
    pthread_mutex_unlock(&work_mutex);
    sleep(1);
   } else {
    break;
   }
  }
 }
 pthread_mutex_unlock(&work_mutex);
 printf("nWaiting for thread to finish...n");
 res = pthread_join(a_thread, &thread_result);
 if (res ! = 0) {
  perror("Thread join failed");
  exit(EXIT_FAILURE);
 }
 printf("Thread joinedn");
 pthread_mutex_destroy(&work_mutex);
 exit(EXIT_SUCCESS);
}
void *thread_function(void *arg) {
 sleep(1);
 pthread_mutex_lock(&work_mutex);
 while(strncmp("end", work_area, 3) ! = 0) {
  printf("You input %d charactersn", strlen(work_area)-1);
  work_area[0] = '';
  pthread_mutex_unlock(&work_mutex);
  sleep(1);
  pthread_mutex_lock(&work_mutex);
  while (work_area[0] == '') {
   pthread_mutex_unlock(&work_mutex);
   sleep(1);
   pthread_mutex_lock(&work_mutex);
  }
 }
 time_to_exit = 1;
 work_area[0] = '';
 pthread_mutex_unlock(&work_mutex);
 pthread_exit(0);
}

После запуска вы получите следующий вывод:

$ cc -D_REENTRANT thread4.с -о thread4 -lpthread
$ ./thread4
Input some text. Enter 'end' to finish
Whit
You input 4 characters
The Crow Road
You input 13 characters
end
Waiting for thread to finish...
Thread joined

Как это работает

Вы начинаете с объявления мьютекса вашей рабочей области и на сей раз дополнительной переменной time_to_exit:

pthread_mutex_t work_mutex; /* защищает work_area и time_to_exit */
#define WORK_SIZE 1024
char work_area[WORK_SIZE];
int time_to_exit = 0;

Далее инициализируется мьютекс:

res = pthread_mutex_init(&work_mutex, NULL);
if (res != 0) {
 perror("Mutex initialization failed");
 exit(EXIT_FAILURE);
}

Затем запускается новый поток. Далее приведен код, выполняемый в функции потока:

pthread_mutex_lock(&work_mutex);
while(strncmp("end", work_area, 3) != 0) {
 printf("You input id charactersn", strlen(work_area)-1);
 work_area[0] = '';
 pthread_mutex_unlock(&work_mutex);
 sleep(1);
 pthread_mutex_lock(&work_mutex);
 while (work_area[0] == '') {
  pthread_mutex_unlock(&work_mutex);
  sleep(1);
  pthread_mutex_lock(&work_mutex);
 }
}
time_to_exit = 1;
work_area[0] = '';
pthread_mutex_unlock(&work_mutex);

Сначала новый поток пытается заблокировать мьютекс. Если он уже заблокирован, вызов задерживается до тех пор, пока мьютекс не освободится. После получения доступа вы проверяете, нет ли к вам запроса на завершение выполнения. Если запрашивается завершение, просто задайте переменную time_to_exit, сотрите первый символ в рабочей области и завершите выполнение. 

Если вы не хотите завершать выполнение, сосчитайте символы и очистите первый символ, сделав его пустым (null). Пустой первый символ применяется как способ информирования считывающей программы о завершении подсчета символов. Далее вы открываете мьютекс и ждете выполнения потока main. Периодически вы пытаетесь заблокировать мьютекс и, когда вам это удается, проверяете, подготовил ли поток main новую работу для вас. Если нет, вы открываете мьютекс и ждете какое-то время. Если работа есть, вы считаете символы и выполняете проход цикла снова.

Далее приведен поток main.

pthread_mutex_lock(&work_mutex)
printf("Input some text. Enter 'end' to finishn");
while (!time_to_exit) {
 fgets(work_area, WORK_SIZE, stdin);
 pthread_mutex_unlock(&work_mutex);
 while(1) {
  pthread_mutex_lock(&work_mutex);
  if (work_area[0] != '') {
   pthread_mutex_unlock(&work_mutex);
   sleep(1);
  } else {
   break;
  }
 }
}
pthread_mutex_unlock(&work_mutex);

Он аналогичен второму потоку. Вы блокируете рабочую область и можете читать в нее текст, а затем вы снимаете блокировку, чтобы открыть доступ другому потоку для подсчета слов. Периодически вы блокируете мьютекс, проверяете, сосчитаны ли слова (элемент work_area[0] равен пустому символу), и освобождаете мьютекс, если нужно продолжить ожидание. Как уже отмечалось ранее, этот вид опроса и получения ответа в основном не слишком удачный прием и в реальной жизни вам, возможно, придется применить семафор для его замены. Тем не менее, программный код справляется с задачей демонстрации примера применения мьютекса.

Оглавление книги


Генерация: 0.057. Запросов К БД/Cache: 0 / 0
поделиться
Вверх Вниз