
- •Лабораторная работа № 3. Синхронизация потоков с использованием мfс
- •Задача синхронизации потоков
- •Создание и синхронизация потоков с использованием mfc
- •1. Синхронизация с использованием глобальной переменной
- •3. Взаимодействие с помощью объектов событий
- •1. Критические секции.
- •2. Мьютексы.
- •3. Семафор
- •Создание и выполнение потоков
- •Функция WaitForSingleObject
- •Cинхронизация в mfc
- •Cинхронизация в cMutex
- •Задания для самостоятельного выполнения.
Лабораторная работа № 3. Синхронизация потоков с использованием мfс
Задача синхронизации потоков
Основная проблема, с которой сталкиваются программисты при реализации многопоточности в вычислительных процессах, является синхронизация (согласованная работа) одновременно работающих потоков. Например, если один поток пытается изменить значение разделяемых данных в то время, когда другой поток пытается прочитать эти значения, то результатом оказывается неопределенное поведение вычислительного процесса.
Так как практически всегда потоков гораздо больше, чем физических процессоров для их выполнения, то потоки на самом деле выполняются не одновременно, а по очереди. Но переключение между потоками происходит так часто, что пользователю кажется, что они выполняются параллельно. Наиболее распространенными проблемами во взаимодействии потоков являются:
1) cостояние гонки (race condition) - ошибка при разработке многопоточного приложения, при которой результат его работы зависит от того, в каком порядке выполняются части кода,
2) взаимная блокировка (deadlock) — ситуация в многозадачной среде, при которой несколько процессов пребывают в состоянии бесконечно долгого ожидания ресурсов при условии, что эти ресурсы заняты самими этими процессами.
Обычно рассматривается взаимодействие потоков в двух основных случаях:
1) при совместном использовании разделяемого ресурса (чтобы предотвратить его разрушение);
2) при необходимости уведомления других потоков о завершении каких-либо операций.
В зависимости от ситуации потоки могут находиться в трех состояниях.
1. Состояние активности. Поток выполняется, ему выделено процессорное время.
2. Состояние неактивности (готовности). Поток ожидает выделения процессора, т.е. находится в состоянии готовности.
3. Состояние блокировки. Когда поток заблокирован, ему вообще не выделяется время. Обычно блокировка ставится на время ожидания какого-либо события. При возникновении этого события поток автоматически переводится из состояния блокировки в состояние готовности.
По назначению и способу выполнения выделяют два вида потоков:
1) интерактивные, крутящие свой цикл обработки сообщений (такие, как главный поток приложения), обычно прерываются после наступления заданных событий, и
2) рабочие, представляющие собой простую функцию, в этом случае поток завершается по мере завершения выполнения этой функции.
Механизм синхронизации потоков включает набор специальных объектов операционной системы, которые создаются и управляются программно, являются общими для всех потоков в системе (некоторые - для потоков, принадлежащих одному процессу) и используются для координирования доступа к ресурсам. В качестве ресурсов может выступать все, что может быть общим для двух и более потоков - файл на диске, порт, запись в базе данных, объект GDI, и даже глобальная переменная программы, которая может быть доступна из потоков, принадлежащих одному процессу.
Объектов синхронизации существует несколько, самые важные из них – это:
1) взаимоисключение (mutex, сокр. от mutual exclusion - взаимное исклю- чение),
2) критическая секция (critical section),
3) событие (event) и
4) семафор (semaphore).
Каждый из этих объектов реализует свой способ синхронизации. Также в качестве объектов синхронизации фактически используются сами процессы и потоки (когда один поток ждет завершения другого потока или процесса); а также файлы, коммуникационные устройства, консольный ввод и уведомления об изменении.
Мьютекс (mutex, сокр. от mutually exclusive - взаимоисключающий) - объект, который может в любой момент времени принадлежать лишь одному потоку, гарантируя безопасность доступа к связанному с ним ресурсу. Для этого поток должен «заблокировать» мьютекс. Если мьютекс уже заблокирован другим потоком, то эта операция ждет, пока мьютекс не будет освобожден («разблокирован»). Таким образом, гарантируется, что только один поток имеет доступ к разделяемому ресурсу в один момент времени. \
Одной из основных проблем в использовании мьютексов является возникновение тупиков, при которых время ожидания теоретически бесконечно.
Критические секции (critical section), как и мьютексы, используются для предотвращения одновременного доступа к ресурсу со стороны нескольких потоков. Однако если мьютекс может синхронизировать межпроцессные потоки, критическая секция ограничивается потоками одного процесса. Ограничение компенсируется скоростью — критическая секция работает быстрее, чем мьютекс.
Семафоры (semaphore) тоже могут применяться для ограничения доступа к ресурсам, но в отличие от мьютексов или критических секций семафор разрешает одновременный доступ со стороны нескольких потоков. Максимальное количество потоков, одновременно получающих доступ к ресурсу, определяется при создании семафора. Затем доступ предоставляется всем потокам до тех пор, пока их количество не достигнет заданного предела. Все остальные потоки, желающие получить доступ, блокируются до тех пор, пока один или несколько потоков не прекратят работу с ресурсом.
Каждый из объектов синхронизации может находиться в так называемом сигнальном состоянии. Для каждого типа объектов это состояние имеет свой смысл. Потоки могут проверять текущее состояние объекта синхронизации и/или ждать изменения этого состояния и таким образом согласовывать свои действия. Когда поток работает с объектами синхронизации (создает их, изменяет состояние) система не прервет его выполнения, пока он не завершит это действие. Таким образом, все конечные операции с объектами синхронизации являются атомарными (неделимыми), как бы выполняющимися за один такт.
Чтобы создать тот или иной объект синхронизации, производится вызов специальной функции WinAPI типа Create... (напр. CreateMutex). Этот вызов возвращает дескриптор объекта (HANDLE), который может использоваться всеми потоками, принадлежащими данному процессу. Есть возможность получить доступ к объекту синхронизации из другого процесса - либо унаследовав дескриптор этого объекта, либо, что предпочтительнее, воспользовавшись вызовом функции открытия объекта (Open...). После этого вызова процесс получит дескриптор, который в дальнейшем можно использовать для работы с объектом. Объекту, если только он не предназначен для использования внутри одного процесса, обязательно присваивается имя. Имена всех объектов должны быть различны (даже если они разного типа). Нельзя, например, создать событие и семафор с одним и тем же именем.
По имеющемуся дескриптору объекта можно определить его текущее состояние. Это делается с помощью т.н. ожидающих функций. Обычно используется функция WaitForSingleObject:
DWORD WaitForSingleObject(
HANDLE hObject, // идентификатор объекта
DWORD dwTimeout); // время ожидания в миллисекундах
Функция принимает два параметра, первый из которых HANDLE hObject - дескриптор объекта, второй dwTimeout - время ожидания в мсек.
Функция возвращает следующие строковые значения:
1) WAIT_OBJECT_0, в случае перехода объекта в сигнальном состояние,
2) WAIT_TIMEOUT - если истекло время ожидания (объект не получил сигнал), и
3)WAIT_ABANDONED, если объект-взаимоисключение не был освобожден до того, как владеющий им поток завершился.
Если время ожидания указано равным нулю, функция возвращает результат немедленно, в противном случае она ждет в течение указанного промежутка времени. В случае, если состояние объекта станет сигнальным до истечения этого времени, функция вернет WAIT_OBJECT_0, в противном случае функция вернет WAIT_TIMEOUT. Если в качестве времени указана символическая константа INFINITE, то функция будет ждать неограниченно долго, пока состояние объекта не станет сигнальным.
Если необходимо узнавать о состоянии сразу нескольких объектов, следует воспользоваться функцией WaitForMultipleObjects.
Для того, чтобы закончить работу с объектом и освободить дескриптор, вызывается функция CloseHandle.
Обращение к ожидающей функции всегда блокирует текущий поток, т.е. пока поток находится в состоянии ожидания, ему не выделяется процессорного времени.
Для работы с объектами синхронизации можно использовать стандартную библиотеку MFC, можно использовать новую библиотеку Boost, представляющую собой фактически собрание библиотек, расширяющих функциональность C++ - в том числе, в направлении многопоточного программирования.