Главная » Статьи » Образовательные » Программирование |
Изучаем модульное тестирование в сфере мобильного программинга
С появлением многозадачности, в операционных системах остро встал вопрос потокобезопасности и совместного доступа к ресурсам. Большинство современных приложений так или иначе используют треды в своей работе. Помимо организации обработки банальных сообщений Windows, которые отправляются элементам графического интерфейса (окнам, кнопкам, полям ввода и т.д.), многопоточность нужна и в ряде чисто прикладных задач. Часто возникают ситуации, когда нам требуется дождаться корректного завершения запущенного треда, или же обеспечить монопольный доступ в единичный момент времени к какой-нибудь глобальной переменной. Все эти задачи требуют от программиста решить вопрос о межпоточной синхронизации. Для большей наглядности можно рассмотреть следующий код: Ожидаем завершение потока // Запускаем поток HANDLE hWorkerThread = ::CreateThread( ... ); // Перед окончанием работы надо каким-либо образом сообщить рабочему потоку, что пора закачивать ... // Ждем завершения потока DWORD dwWaitResult = ::WaitForSingleObject( hWorkerThread, INFINITE ); if( dwWaitResult != WAIT_OBJECT_0 ) { // обработка ошибки } // Хэндл потока можно закрыть ::CloseHandle( hWorkerThread ); В этом примере мы создаем поток с помощью функции CreateThread, а затем, перед завершением программы, ожидаем, когда наш поток отработает, чтобы закрыть его описатель. Собственно, вся магия заключается тут в API-функции WaitForSingleObject. Но об этом чуть ниже. WaitForSingleObject и WaitForMultipleObjects В нашем примере мы используем функцию WaitForSingleObject для временной приостановки потока, в контексте которого она была вызвана. Первый ее параметр — это хендл объекта, изменение состояния которого мы ожидаем. В приведенном коде мы следим за описателем потока, но есть и другие объекты ядра, о которых мы поговорим чуть позже. Объекты ядра могут находиться в двух состояниях: нейтральном и сигнальном. Так вот функция WaitForSingleObject занимается тем, что ждет, когда объект перейдет в сигнальное состояние, при этом поток, вызвавший ее, совершенно не расходует процессорное время. Сколько времени ждать наступления сигнального состояния, API решает на основе второго параметра — времени в миллисекундах. В случае его истечения функция вернет WAIT_TIMEOUT. Ожидать изменения состояния объекта можно бесконечно, для этого нужно передать во втором параметре значение INFINITE. Или же можно вообще не приостанавливать поток, а просто проверить состояния кернел обжекта, сделав интервал ожидания равным нулю. Когда объект переходит в сигнальное состояние, функция завершается, возвращая значение WAIT_OBJECT_0. Но из названия WaitForSingleObject должно быть понятно, что это всего лишь частный случай API-функции WaitForMultipleObjects, которая предоставляет гораздо больше возможностей. Давай взглянем на ее прототип. Описание WaitForMultipleObjects DWORD WINAPI WaitForMultipleObjects( __in DWORD nCount, __in const HANDLE *lpHandles, __in BOOL bWaitAll, __in DWORD dwMilliseconds ); Основное отличие этой API-функции от WaitForSingleObject в том, что она может ожидать изменения состояния сразу нескольких объектов. Описатели этих объектов передаются в массиве в параметре const HANDLE *lpHandles. Количество элементов массива задается с помощью DWORD nCount, а BOOL bWaitAll позволяет приостанавливать поток до тех пор, пока все объекты ядра не перейдут в сигнальное состояние. Возвращаемые значения практически идентичны значениям WaitForSingleObject, с той лишь разницей, что если bWaitAll == FALSE, то в случае установки одного из объектов в сигнальное состояние мы получаем на выходе WAIT_OBJECT_0 + object_index_in_array. То есть, если из результата вычесть WAIT_OBJECT_0, то мы узнаем индекс объекта в массиве, который перешел в сигнальное состояние. Теперь, когда мы поняли, как работают функции WaitFor***, можно подробнее рассмотреть, чем же таким являются объекты ядра. WaitForMultipleObjects и иже с ней могут мониторить объекты событий (Events), мьютексы, семафоры, процессы, потоки, таймеры ожидания, консольный ввод и некоторые другие виды объектов. Мьютексы, события и семафоры служат специально для целей синхронизации, поэтому на них стоит остановиться подробнее. Объект ядра Событие Событие или event — это самый примитивный объект синхронизации. По сути, это просто флаг, который может находиться либо в нейтральном состоянии, либо в сигнальном. Создать ивент можно с помощью функции CreateEvent. Описание CreateEvent HANDLE WINAPI CreateEvent( __in_opt LPSECURITY_ATTRIBUTES lpEventAttributes, __in BOOL bManualReset, __in BOOL bInitialState, __in_opt LPCTSTR lpName ); События имеют возможность автосброса. Для этого надо установить параметр BOOL bManualReset в значение FALSE. В этом случае при обработке ивента функцией WaitFor*** объект автоматически перейдет в нейтральное состояние. Если же bManualReset == TRUE, то сброс флага осуществляется функцией ResetEvent. События могут быть именованными и не именованными. В первом случае к ивенту можно получить доступ из другого процесса, открыв объект с помощью API OpenEvent. Такие events удобно использовать при межпроцессорной синхронизации. Также событию можно задать начальное состояние с помощью параметра BOOL bInitialState. Если он равен TRUE, то объект будет создан сразу в сигнальном состоянии. Events в связке с WaitFor***-функциями обеспечивает простой и удобный способ синхронизации потоков. Объект ядра Мьютекс Мьютекс (mutex) очень похож на event с автосбросом, но, в отличие от последнего, он имеет привязку к конкретному потоку. Если мьютекс находится в сигнальном состоянии, то считается, что он свободен. Как только какой-либо тред дожидается сигнального состояния mutex с помощью функции WaitFor***, объект сбрасывается в нейтральное состояние и считается захваченным этим потоком. В отличие от события, мьютекс имеет разные состояния для разных тредов. Так, для потока, который захватил mutex, он выглядит как свободный, и все последующие вызовы WaitFor*** вернут результат, говорящий о том, что объект находится в сигнальном состоянии. Все же другие потоки будут видеть его как захваченный объект, то есть в нейтральном состоянии и WaitFor*** API будут ждать, пока он освободится. Перевести мьютекс обратно в сигнальное состояние можно с помощью функции ReleaseMutex. Кроме того mutex имеет счетчик ссылок. Это означает, что ReleaseMutex надо вызывать столько же раз, сколько были вызваны WaitFor***. Если понимание того, чем мьютекс отличается от события до сих пор не пришло, то советую взглянуть на пример ниже. Пример работы Mutex HANDLE hMutex; void Func() { ::WaitForSingleObject(hMutex, INFINITE); … ::ReleaseMutex(hMutex); } DWORD WINAPI thread1(LPVOID param) { ::WaitForSingleObject(hMutex, INFINITE); Func(); ::ReleaseMutex(hMutex); } DWORD WINAPI thread2(LPVOID param) { ::WaitForSingleObject(hMutex, INFINITE); ... ::ReleaseMutex(hMutex); } int main(...) { hMutex = ::CreateMutex(NULL, FALSE, NULL); HANDLE hThread1 = ::CreateThread(NULL, 0, thread1, ...); HANDLE hThread2 = ::CreateThread(NULL, 0, thread2, ...); } В этом коде поток thread1 дважды вызывает WaitForSingleObject для нашего мьютекса. Причем вызовы эти вложены друг в друга, то есть сначала два раза вызывается WaitForSingleObject, а затем — два раза ReleaseMutex. Оба вызова проходят WaitFor*** проходят гладко, так как для thread1 наш мьютекс находится в сигнальном состоянии, несмотря на автосброс. Но этот автосброс влияет на thread2, который ожидает, пока объект станет свободным. Если бы мы использовали ивенты, то вызов WaitForSingleObject в функции Func в первом потоке привел бы к его полному зависанию, но благодаря свойству мьютекса привязываться к контексту потока, этого не произошло. Объект ядра Семафор (semaphore) Поведение семафора сложнее, чем у других объектов синхронизации. Несмотря на то, что у него нет привязки к контексту потока, как у мьютекса, но зато semaphore обладает внутренним счетчиком. Каждый раз, когда WaitFor***-функция определяет семафор в сигнальном состоянии, этот счетчик уменьшается на единицу. Как только счетчик достигнет нуля, семафор переходит в нейтральное состояние. Создать semaphore можно с помощью API-функции CreateSemaphore. Описание CreateSemaphore HANDLE WINAPI CreateSemaphore( __in_opt LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, __in LONG lInitialCount, __in LONG lMaximumCount, __in_opt LPCTSTR lpName ); Как видно из прототипа функции, здесь можно задать максимальное число счетчика (параметр LONG lMaximumCount) и сразу инициализировать этот счетчик некоторым числом (LONG lInitialCount). ReleaseSemaphore может увеличить этот счетчик, причем не обязательно на единицу, а на необходимое заданное значение. Объект семафор может использоваться для ограничения числа активных потоков в приложении или для других более сложных задач. Критические секции Еще одним синхронизационным примитивом являются critical section. Для работы с ними есть свой набор API, и их нельзя использовать в функциях ожидания WaitFor***. По своим свойствам критические секции очень похожи на мьютексы — в их случае тоже отсутствует угроза взаимоблокировки в контексте одного потока. Перед использованием critical section следует сначала ее инициализировать. Для этого потребуется API-функция InitializeCriticalSection. Единственный параметр, который она принимает — это указатель на объект типа CRITICAL_SECTION (память под этот объект следует выделить заранее). После инициализации с критической секцией можно работать. Функция EnterCriticalSection переводит объект CRITICAL_SECTION в состояние «занято», после чего ни один другой поток не сможет выполнить код критической секции. LeaveCriticalSection освобождает объект. Обе API в качестве параметра принимают лишь указатель на инициализированный объект секции, нельзя задать ни время ожидания, ни других дополнительных опций. То есть, функционально, критическая секция является урезанным клоном мьютекса, но в отличие от последнего, работа с ней происходит почти в 100 раз быстрее. Мы теряем в гибкости, зато прибавляем в скорости. Кстати, насчет времени ожидания функцией EnterCriticalSection я немного соврал. Задать его можно, но только для всех критических секций сразу и только в реестре (ключ HKEY_LOCAL_ MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\ CriticalSectionTimeout). По истечении этого времени WINAPI генерирует исключение EXCEPTION_POSSIBLE_DEADLOCK, которое вполне может означать, что в приложении случилась взаимоблокировка потоков — deadlock. Но об этом чуть позже, пока скажу только, что не стоит эти исключения заворачивать в null. Помимо EnterCriticalSection есть еще TryEnterCriticalSection, которая не ожидает освобождения объекта критической секции, а просто возвращает FALSE, если потоку не удалось войти в critical section. Если все нормально, и объект захвачен, то функция вернет TRUE. И не забываем удалять объекты секций, когда они больше не нужны, с помощью DeleteCriticalSection. Атомарные операции Атомарные операции — это еще один вид синхронизации в многопоточных приложениях. Но их главное отличие от всего прочего заключается в том, что они выполняют лишь простые действия. Для каждой атомарной операции есть своя API-функция. Все эти API начинаются со слова Interlocked, и количество их потрясает воображение :). Например, можно атомарно увеличить или уменьшить на единицу какую-либо переменную. Это будет полезно для реализации счетчика ссылок в мультипоточных приложениях. Скорость выполнения атомарных операций выше, чем у критической секции. Дело в том, что все эти команды поддерживаются на уровне процессора, то есть компилятор генерирует единственную команду в машинном коде, которая выполняется CPU в один заход. Deadlock Вернемся к дедлокам. Для того чтобы лучше понять суть этого явления, рассмотрим небольшой пример. Тут возможен deadlock DWORD WINAPI thread1(LPVOID param) { ::WaitForSingleObject(hEventA, INFINITE); ... ::WaitForSingleObject(hEventB, INFINITE); ... } DWORD WINAPI thread2(LPVOID param) { ::WaitForSingleObject(hEventB, INFINITE); ... ::WaitForSingleObject(hEventA, INFINITE); ... } У нас есть два потока, которые поочередно ожидают два ивента: A и B. Возможна такая ситуация, при которой первый поток захватит событие A и будет ожидать освобождения события B, а второй поток, наоборот, захватит event B и будет ждать A. В этом случае мы получим взаимоблокировку, которая приведет к полному зависанию этих двух тредов. Для того чтобы этого избежать, следует либо использовать WaitForMultipleObjects с параметром bWaitAll, равным TRUE, либо ожидать события A и B в жестко оговоренным порядке. И то и другое достаточно сложно сделать в больших системах. Плюс дедлокам подвержены и критические секции, где мы не можем переложить заботу о них на плечи ОС, используя WaitForMultipleObjects. Для решения этой проблемы используются разнообразные паттерны проектирования, которые помогают избежать таких неприятностей даже в самых запутанных ситуациях. Но, к сожалению, рассказ о таких приемах требует отдельной статьи, поэтому здесь я ограничусь предупреждением насчет того, чего стоит опасаться при работе с объектами синхронизации. Заключение Все, что было описано выше, — лишь верхушка айсберга. В рамках одной статьи нельзя полностью раскрыть тему синхронизации потоков и совместного доступа к ресурсам. Тем не менее, все, что мы рассмотрели в этой статье, вполне может составить хорошую основу для правильного написания многопоточных приложений, а конкретные нюансы — они ведь все равно познаются на практике. | |
Просмотров: 1095 | Рейтинг: 0.0/0 |
Всего комментариев: 0 | |