26.7. Взаимные исключения

26.7. Взаимные исключения

Обратите внимание на то, что в листинге 26.8 при завершении выполнения очередного потока в главном цикле уменьшаются на единицу и nconn, и nlefttoread. Мы могли бы поместить оба эти оператора уменьшения в одну функцию do_get_read, что позволило бы каждому потоку уменьшать эти счетчики непосредственно перед тем, как выполнение потока завершается. Но это привело бы к возникновению трудноуловимой серьезной ошибки параллельного программирования.

Проблема, возникающая при помещении определенного кода в функцию, которая выполняется каждым потоком, заключается в том, что обе эти переменные являются глобальными, а не собственными переменными потока. Если один поток в данный момент уменьшает значение переменной и это действие приостанавливается, чтобы выполнился другой поток, который также станет уменьшать на единицу эту переменную, может произойти ошибка. Предположим, например, что компилятор С осуществляет уменьшение переменной на единицу в три этапа: загружает информацию из памяти в регистр, уменьшает значение регистра, а затем сохраняет значение регистра в памяти. Рассмотрим возможный сценарий.

1. Выполняется поток А, который загружает в регистр значение переменной nconn (равное 3).

2. Система переключается с выполнения потока А на выполнение потока В. Регистры потока А сохранены, регистры потока В восстановлены.

3. Поток В выполняет три действия, составляющие оператор декремента в языке С (nconn--), сохраняя новое значение переменной nconn, равное 2.

4. Впоследствии в некоторый момент времени система переключается на выполнение потока А. Восстанавливаются регистры потока А, и он продолжает выполняться с того места, на котором остановился, а именно начиная со второго этапа из трех, составляющих оператор декремента. Значение регистра уменьшается с 3 до 2, и значение 2 записывается в переменную nconn.

Окончательный результат таков: значение nconn равно 2, в то время как оно должно быть равным 1. Это ошибка.

Подобные ошибки параллельного программирования трудно обнаружить по многим причинам. Во-первых, они возникают нечасто. Тем не менее это ошибки, которые по закону Мэрфи вызывают сбои в работе программ. Во-вторых, ошибки такого типа возникают не систематически, так как зависят от недетерминированного совпадения нескольких событий. Наконец, в некоторых системах аппаратные команды могут быть атомарными. Это значит, что имеется аппаратная команда уменьшения значения целого числа на единицу (вместо трехступенчатой последовательности, которую мы предположили выше), а аппаратная команда не может быть прервана до окончания своего выполнения. Но это не гарантировано для всех систем, так что код может работать в одной системе и не работать в другой.

Программирование с использованием потоков является параллельным (parallel), или одновременным (concurrent), программированием, так как несколько потоков могут выполняться параллельно (одновременно), получая доступ к одним и тем же переменным. Хотя ошибочный сценарий, рассмотренный нами далее, предполагает систему с одним центральным процессором, вероятность ошибки также присутствует, если потоки А и В выполняются в одно и то же время на разных процессорах в многопроцессорной системе. В обычном программировании под Unix мы не сталкиваемся с подобными ошибками, так как при использовании функции fork родительский и дочерний процессы не используют совместно ничего, кроме дескрипторов. Тем не менее мы столкнемся с ошибками этого типа при обсуждении совместного использовании памяти несколькими процессами.

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

Мы повысили вероятность ошибки за счет того, что потребовали от программы получить текущее значение переменной counter, вывести это значение и записать его. Если мы запустим эту программу, то получим результат, представленный в листинге 26.10.

Листинг 26.10. Результат выполнения программы, приведенной в листинге 26.11

4: 1

4: 2

4: 3

4: 4

 продолжение выполнения потока номер 4

4: 517

4: 518

5: 518 теперь выполняется поток номер 5

5: 519

5: 520

 продолжение выполнения потока номер 5

5: 926

5: 927

4: 519 теперь выполняется поток номер 4, записывая неверные значения

4: 520

Листинг 26.11. Два потока, которые неверно увеличивают значение глобальной переменной

//threads/example01.c

 1 #include "unpthread.h"

 2 #define NLOOP 5000

 3 int counter; /* потоки должны увеличивать значение этой переменной */

 4 void *doit(void*);

 5 int

 6 main(int argc, char **argv)

 7 {

 8  pthread_t tidA, tidB;

 9  Pthread_create(&tidA, NULL, &doit, NULL);

10  Pthread_create(&tidB, NULL, &doit, NULL);

11  /* ожидание завершения обоих потоков */

12  Pthread_join(tidA, NULL);

13  Pthread_join(tidB, NULL);

14  exit(0);

15 }

16 void*

17 doit(void *vptr)

18 {

19  int i, val;

20  /* Каждый поток получает, выводит и увеличивает на

21   * единицу переменную counter NLOOP раз. Значение

22   * переменной должно увеличиваться монотонно.

23   */

24  for (i = 0; i < NLOOP; i++) {

25   val = counter;

26   printf("%d: %d ", pthread_self(), val + 1);

27   counter = val + 1;

28  }

29  return (NULL);

30 }

Обратите внимание на то, что в первый раз ошибка происходит при переключении системы с выполнения потока номер 4 на выполнение потока номер 5: каждый поток в итоге записывает значение 518. Это происходит множество раз на протяжении 10 000 строк вывода.

Недетерминированная природа ошибок такого типа также будет очевидна, если мы запустим программу несколько раз: каждый раз результат выполнения программы будет отличаться от предыдущего. Также, если мы переадресуем вывод результатов в файл на диске, эта ошибка иногда не будет возникать, так как программа станет работать быстрее, что приведет к уменьшению вероятности переключения системы между потоками. Наибольшее количество ошибок возникнет в случае, если программа будет работать интерактивно, записывая результат на медленный терминал, но при этом также сохраняя результат в файл при помощи программы Unix script (которая описана в главе 19 книги [110]).

Только что описанная проблема, возникающая, когда несколько потоков изменяют значение одной переменной, является самой простой из проблем параллельного программирования. Для решения этой проблемы используются так называемые взаимные исключения (mutex — mutual exclusion), с помощью которых контролируется доступ к переменной. В терминах Pthreads взаимное исключение — это переменная типа pthread_mutex_t, которая может быть заблокирована и разблокирована с помощью следующих двух функций:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mptr);

int pthread_mutex_unlock(pthread_mutex_t *mptr);

Обе функции возвращают: 0 в случае успешного выполнения, положительное значение Exxx в случае ошибки

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

Если переменная-исключение размещена в памяти статически, следует инициализировать ее константой PTHREAD_MUTEX_INITIALIZER. В разделе 30.8 мы увидим, что если мы размещаем исключение в совместно используемой (разделяемой) памяти, мы должны инициализировать его во время выполнения программы путем вызова функции pthread_mutex_init.

ПРИМЕЧАНИЕ

Некоторые системы (например, Solaris) определяют константу PTHREAD_MUTEX_INITIALIZER как 0. Если данная инициализация будет опущена, это ни на что не повлияет, так как статически размещаемые переменные все равно автоматически инициализируются нулем. Но для других систем такой гарантии дать нельзя — например, в Digital Unix константа инициализации ненулевая.

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

Листинг 26.12. Исправленная версия листинга 26.11, использующая взаимное исключение для защиты совместно используемой переменной

//threads/examplе01.с

 1 #include "unpthread.h"

 2 #define NLOOP 5000

 3 int counter; /* увеличивается потоками */

 4 pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;

 5 void *doit(void*);

 6 int

 7 main(int argc, char **argv)

 8 {

 9  pthread_t tidA, tidB;

10  Pthread_create(&tidA, NULL, &doit, NULL);

11  Pthread_create(&tidB, NULL, &doit, NULL);

12  /* ожидание завершения обоих потоков */

13  Pthread_join(tidA, NULL);

14  Pthread_join(tidB, NULL);

15  exit(0);

16 }

17 void*

18 doit(void *vptr)

19 {

20  int i, val;

21  /*

22   * Каждый поток считывает, выводит и увеличивает счетчик NLOOP раз.

23   * Значение счетчика должно возрастать монотонно.

24   */

25  for (i = 0; i < NLOOP; i++) {

26   Pthread_mutex_lock(&counter_mutex);

27   val = counter;

28   printf(%d: %d ", pthread_self(), val + 1);

29   counter = val + 1;

30   Pthread_mutex_unlock(&counter_mutex);

31  }

32  return(NULL);

33 }

Мы объявляем взаимное исключение с именем counter_mutex. Это исключение должно быть заблокировано потоком на то время, когда он манипулирует переменной counter. Когда мы запускали эту программу, результат всегда был правильным: значение переменной увеличивалось монотонно, а ее окончательное значение всегда оказывалось равным 10 000.

Насколько серьезной является дополнительная нагрузка, связанная с использованием взаимных исключений? Мы изменили программы, приведенные в листингах 26.11 и 26.12, заменив значение NLOOP на 50 000 (вместо исходного значения 5000), и засекли время, направив вывод на устройство /dev/null. Время работы центрального процессора в случае корректной версии, использующей взаимное исключение, увеличилось относительно времени работы некорректной версии без взаимного исключения на 10 %. Это означает, что использование взаимного исключения не связано со значительными издержками.

Поделитесь на страничке

Следующая глава >

Похожие главы из других книг

Исключения

Из книги Сущность технологии СОМ. Библиотека программиста автора Бокс Дональд

Исключения СОМ имеет специфическую поддержку выбрасывания (throwing) исключительных ситуаций из реализации методов. Поскольку в языке C++ не существует двоичного стандарта для исключений, СОМ предлагает явные API-функции для выбрасывания и перехвата объектов СОМ-исключений://


Внутренние исключения

Из книги Язык программирования С# 2005 и платформа .NET 2.0. [3-е издание] автора Троелсен Эндрю

Внутренние исключения Вы можете догадываться, что вполне возможно генерировать исключения и во время обработки другого исключения. Например, предположим, что вы обрабатываете CarIsDeadException в рамках конкретного блока catch и в процессе обработки пытаетесь записать след стека


Тип исключения

Из книги Microsoft Visual C++ и MFC. Программирование для Windows 95 и Windows NT автора Фролов Александр Вячеславович

Тип исключения Если вызывается исключение, для которого отсутствует обработчик и не определен универсальный обработчик исключений всех типов, тогда вызывается функция terminate из стандартной библиотеки. Она вызывает функцию abort, завершающую работу программы.Вы можете


Исключения

Из книги Основы объектно-ориентированного программирования автора Мейер Бертран

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


ГЛАВА 7 Взаимные исключения и условные переменные

Из книги UNIX: взаимодействие процессов автора Стивенс Уильям Ричард

ГЛАВА 7 Взаимные исключения и условные переменные 7.1. Введение Эта глава начинается с обсуждения синхронизации — принципов синхронизации действий нескольких программных потоков или процессов. Обычно это требуется для предоставления нескольким потокам или процессам


7.2. Взаимные исключения: установка и снятие блокировки

Из книги Программирование на языке Ruby [Идеология языка, теория и практика применения] автора Фултон Хэл

7.2. Взаимные исключения: установка и снятие блокировки Взаимное исключение (mutex) является простейшей формой синхронизации. Оно используется для защиты критической области (critical region), предотвращая одновременное выполнение участка кода несколькими потоками (если взаимное


Взаимные исключения Posix

Из книги Firebird РУКОВОДСТВО РАЗРАБОТЧИКА БАЗ ДАННЫХ автора Борри Хелен

Взаимные исключения Posix В листинге А.19 приведены глобальные переменные и функция main пpoгрaммы, измеряющей быстродействие взаимных исключений Posix.Листинг А.19. Глобальные переменные и функция main для взаимных исключений Posix//bench/incr_pxmutex1.с1  #include "unpipc.h"2  #define MAXNTHREADS 1003  int


Взаимные исключения Posix между процессами

Из книги Мир InterBase. Архитектура, администрирование и разработка приложений баз данных в InterBase/FireBird/Yaffil автора Ковязин Алексей Николаевич

Взаимные исключения Posix между процессами Функция main первой программы использует взаимное исключение Posix для обеспечения синхронизации. Текст ее приведен в листинге А.32.Листинг А.32. Функция main для измерения быстродействия взаимных исключений между


7.19. Взаимные преобразования объектов Date, Time и DateTime

Из книги автора

7.19. Взаимные преобразования объектов Date, Time и DateTime В Ruby есть три основных класса для работы с датами и временем: Time, Date и DateTime. Опишем их особенности:• Класс Time преимущественно обертывает соответствующие функции из стандартной библиотеки языка С. Они, как правило,


Исключения

Из книги автора

Исключения Обработчики исключений могут быть написаны, чтобы "съесть" ошибку, обрабатывая ее разными способами. Например, в итеративной подпрограмме входная строка, вызывающая исключение, необязательно должна приводить к остановке всего процесса. Обработка исключения


11.1. Возбуждение исключения

Из книги автора

11.1. Возбуждение исключения Исключение – это аномальное поведение во время выполнения, которое программа может обнаружить, например: деление на 0, выход за границы массива или истощение свободной памяти. Такие исключения нарушают нормальный ход работы программы, и на них


19.2. Исключения и наследование

Из книги автора

19.2. Исключения и наследование Обработка исключений – это стандартное языковое средство для реакции на аномальное поведение программы во время выполнения. C++ поддерживает единообразный синтаксис и стиль обработки исключений, а также способы тонкой настройки этого