Перейти к содержимому

Введение в многопоточность

    Введение

    В статье рассматриваются методы синхронизации потоков одного или нескольких процессов. Все методы основаны на создании специальных объектов синхронизации. Эти объекты характеризуются состоянием. Различают сигнальное и несигнальное состояние. В зависимости от состояния объекта синхронизации один поток может узнать об изменении состояния других потоков или общих (разделяемых) ресурсов.

    Небольшое замечание: функция _beginthread, используемая в примерах, может быть заменена соответствующим эквивалентом MFC (AfxBeginThread) или аналогичной в других диалектах языка С.

    Несинхронизированные потоки

    Первый пример иллюстрирует работу с несинхронизированными потоками. Основной цикл, который является основным потоком процесса, выводит на экран содержимое глобального массива целых чисел. Поток, названный \\\»Thread\\\», непрерывно заполняет глобальный массив целых чисел.

    #include #include int a[ 5 ]; void Thread( void* pParams ) { int i, num = 0; while ( 1 ) { for ( i = 0; i < 5; i++ ) a[ i ] = num; num++; } } int main( void ) { _beginthread( Thread, 0, NULL ); while( 1 ) printf(\\\"%d %d %d %d %d\\\\n\\\", a[ 0 ], a[ 1 ], a[ 2 ], a[ 3 ], a[ 4 ] ); return 0; }

    Как видно из результата работы процесса, основной поток (сама программа) и поток Thread действительно работают параллельно (красным цветом обозначено состояние, когда основной поток выводит массив во время его заполнения потоком Thread):

    81751652 81751652

     81751651 81751651 81751651
    81751652 81751652

     81751651 81751651 81751651
    83348630 83348630 83348630

     83348629 83348629
    83348630 83348630 83348630

     83348629 83348629
    83348630 83348630 83348630

     83348629 83348629

    Запустите программу, затем нажмите \\\»Pause\\\» для остановки вывода на дисплей (т.е. приостанавливаются операции ввода/вывода основного потока, но поток Thread продолжает свое выполнение в фоновом режиме) и любую другую клавишу для возобновления выполнения.

    Критические секции

    А что делать, если основной поток должен читать данные из массива после его обработки в параллельном процессе? Одно из решений этой проблемы — использование критических секций.

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

    #include #include #include CRITICAL_SECTION cs; int a[ 5 ]; void Thread( void* pParams ) { int i, num = 0; while ( TRUE ) { EnterCriticalSection( &cs; ); for ( i = 0; i < 5; i++ ) a[ i ] = num; LeaveCriticalSection( &cs; ); num++; } } int main( void )
    { InitializeCriticalSection( &cs; ); _beginthread( Thread, 0, NULL ); while( TRUE ) { EnterCriticalSection( &cs; ); printf( \\\"%d %d %d %d %d\\\\n\\\", a[ 0 ], a[ 1 ], a[ 2 ], a[ 3 ], a[ 4 ] ); LeaveCriticalSection( &cs; ); } return 0; }

    Мьютексы (взаимоисключения)

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

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

    Несколько процессов могут получить хэндл одного и того же мьютекса, что делает возможным взаимодействие между процессами. Вы можете использовать следующие механизмы такого подхода:

    • Дочерний процесс, созданный при помощи функции CreateProcess может наследовать хэндл мьютекса в случае, если при его (мьютекса) создании функией CreateMutex был указан параметр lpMutexAttributes.
    • Процесс может получить дубликат существующего мьютекса с помощью функции DuplicateHandle.
    • Процесс может указать имя существующего мьютекса при вызове функций OpenMutex или CreateMutex.

    Вообще говоря, если вы синхронизируете потоки одного процесса, более эффективным подходом является использование критических секций.

    #include #include #include HANDLE hMutex; int a[ 5 ]; void Thread( void* pParams ) { int i, num = 0; while ( TRUE ) { WaitForSingleObject( hMutex, INFINITE ); for ( i = 0; i < 5; i++ ) a[ i ] = num; ReleaseMutex( hMutex ); num++; } } int main( void ) { hMutex = CreateMutex( NULL, FALSE, NULL ); _beginthread( Thread, 0, NULL ); while( TRUE )
    { WaitForSingleObject( hMutex, INFINITE ); printf( \\\"%d %d %d %d %d\\\\n\\\", a[ 0 ], a[ 1 ], a[ 2 ], a[ 3 ], a[ 4 ] ); ReleaseMutex( hMutex ); } return 0; }

    События

    А что, если мы хотим, чтобы в предыдущем примере второй поток запускался каждый раз после того, как основной поток закончит печать содержимого массива, т.е. значения двух последующих строк будут отличаться строго на 1?

    Событие — это объект синхронизации, состояние которого может быть установлено в сигнальное путем вызова функций SetEvent или PulseEvent. Существует два типа событий:

    Тип объектаОписание
    Событие с ручным сбросомЭто объект, сигнальное состояние которого сохраняется до ручного сброса функцией ResetEvent. Как только состояние объекта установлено в сигнальное, все находящиеся в цикле ожидания этого объекта потоки продолжают свое выполнение (освобождаются).
    Событие с автоматическим сбросомОбъект, сигнальное состояние которого сохраняется до тех пор, пока не будет освобожден единственный поток, после чего система автоматически устанавливает несигнальное состояние события. Если нет потоков, ожидающих этого события, объект остается в сигнальном состоянии.

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

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

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

    #include #include #include HANDLE hEvent1, hEvent2; int a[ 5 ]; void Thread( void* pParams ) { int i, num = 0; while ( TRUE ) { WaitForSingleObject( hEvent2, INFINITE ); for ( i = 0; i < 5; i++ ) a[ i ] = num; SetEvent( hEvent1 ); num++; } } int main( void ) { hEvent1 = CreateEvent( NULL, FALSE, TRUE, NULL ); hEvent2 = CreateEvent( NULL, FALSE, FALSE, NULL ); _beginthread( Thread, 0, NULL ); while( TRUE ) { WaitForSingleObject( hEvent1, INFINITE ); printf( \\\"%d %d %d %d %d\\\\n\\\", a[ 0 ], a[ 1 ], a[ 2 ], a[ 3 ], a[ 4 ] ); SetEvent( hEvent2 ); } return 0; }

    Сравнение объектов синхронизации

    В MSDN News за июль/август 1998г. есть статья об объектах синхронизации. Следующая таблица взята из этой статьи:

    ОбъектОтносительная скоростьДоступ нескольких процессовПодсчет числа обращений к ресурсуПлатформы
    Критическая секциябыстроНетНет (эксклюзивный доступ)9x/NT/CE
    МьютексмедленноДаНет (эксклюзивный доступ)9x/NT/CE
    СемаформедленноДаАвтоматически9x/NT
    СобытиемедленноДаДа9x/NT/CE

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *