Организация межпотокового взаимодействия с использованием объектов ядра операционной системы [окончание]
Просмотров: 2814
Август 2012 года
А.А. Огинский, А.М. Набатчиков, Е.А. Бурлак. Организация межпотокового взаимодействия с использованием объектов ядра операционной системы // Вестник компьютерных и информационных технологий. – 2012. – №8 (98). – С. 52-56.
ВАК
УДК: 004.032.3:004.272.4
Ключевые слова: многопоточные приложения; межпоточное взаимодействие; объекты ядра; синхронный и асинхронный опрос устройства
"ВЕСТНИК КОМПЬЮТЕРНЫХ И ИНФОРМАЦИОННЫХ ТЕХНОЛОГИЙ"
Ключевые слова: многопоточные приложения; межпоточное взаимодействие; объекты ядра; синхронный и асинхронный опрос устройства
"ВЕСТНИК КОМПЬЮТЕРНЫХ И ИНФОРМАЦИОННЫХ ТЕХНОЛОГИЙ"
Рассмотрены проблемы реализации многопоточных приложений в операционной системе Windows. Показаны основные трудности разработки и пути их решения. Определены основные моменты проектирования и продемонстрированы преимущества использования потоков. Представлены некоторые специфические и трудно диагностируемые ошибки и приведены методы их устранения. Проиллюстрированы основные моменты примером решения актуальной практической задачи разработки приложения, взаимодействующего с некоторым устройством ввода данных.
Использование потоков
Несмотря на существование интерфейса, во многом абстрагирующего пользователя от работы с потоками, необходимость в интерфейсе может возникнуть, например, в следующих случаях:
- разработка оболочки библиотеки [1, 2], обеспечивающей более удобную работу с API (Application Programming Interface) устройства, в том числе на языках, не обладающих необходимым инструментарием для работы с потоками. В этом случае оболочка может быть реализована на C++, например, в виде динамически подключаемой библиотеки для использования в приложении, написанном на другом языке [3]. Такая необходимость возникла при исследованиях, представленных в [4];
- если помимо регистрации данных на каждой итерации осуществляются операции, бо́льшую часть времени не зависящие от состояния устройства, вследствие чего их можно выполнять параллельно с опросом. Например, получаемые с устройства данные подлежат длительной обработке, в ходе которой текущее состояние источника информации игнорируется. При этом актуальность данных определяется разницей во времени между их получением и началом операций над ними.
Для решения перечисленных задач требуются некоторые манипуляции с потоками. На рисунке 6 приведена схема работы варианта реализации оболочки, позволяющей выделить процесс получения данных в отдельный поток.
Асинхронный опрос источника данных, например органа управления летательного аппарата в авиатренажёре, вынесен в отдельный поток: постановка запроса устройству происходит за время t1, устройство формирует ответ за время t2, поток записывает в некоторый участок памяти (буфер) результат опроса, затрачивая на эту операцию время t3. Поток приложения, использующий оболочку, выполняет длительную обработку информации (расчет параметров динамики летательного аппарата, визуализацию закабинной обстановки и т.д.) за время t4, затем тратит время t5 на чтение актуальных (на сколько позволяет скорость работы устройства) данных из буфера.
Таким образом, устройство опрашивается постоянно: каждый новый блок данных заменяет собой предыдущий. Данные запрашиваются по мере необходимости (на рис. 6 блоки A и C) за время t5 < t1 + t2, при этом невостребованные данные перезаписываются на более поздние (на рис. 6 блок B).
При использовании данной организации взаимодействия необходимо рассмотреть два случая:
- Данные не готовы: поток приложения запрашивает данные до конца первого опроса устройства. Необходимо оговорить значение неопределённого состояния устройства или дополнительный параметр, индицирующий отсутствие готовых для обработки данных.
- Одновременный доступ: при совпадении начала процедуры записи и чтения буфера возможно искажение данных. Обработка этой ситуации не всегда критична, однако если данные внутри блока взаимосвязаны между собой (сумма элементов нормирована или первый элемент кодирует количество доступных для чтения элементов в блоке и т.д.), это может привести к ошибке в логике программы или её краху. Подобная проблема возникла при разработке диспетчера динамически подключаемых модулей программного обеспечения тренажно-моделирующего комплекса тактической подготовки лётных экипажей «Репитер-М».
Для программиста, взаимодействующего с оболочкой, асинхронный опрос превратится в синхронный, а все детали реализации, связанные с многопоточностью, будут скрыты новым интерфейсом.
Реализация многопоточного приложения
В общем случае интерфейс разрабатываемой библиотеки имеет три экспортируемых функции:
- запуск опроса;
- получение данных;
- остановка опроса.
С буфером, хранящим актуальный блок информации с устройства, взаимодействуют две функции, монопольно захватывающие ресурс:
- запись в буфер;
- чтение из буфера (она же соответствует функции получения данных).
Необходимо запрограммировать поведение потока, создав отдельную функцию.
Таким образом, в библиотеке содержатся пять функций: запуск, останов, чтение из буфера (получение данных), запись в буфер, функция опроса устройства (выполняемая отдельным потоком).
Как показывает практика, многие ошибки совершаются уже на этапе создания потока. Часто в примерах реализаций можно встретить использование Windows API функции
CreateThread
LPDWORD lpThreadId
NULL
_beginthread
_beginthreadex
process.h
Листинг № 2. Создание потока
12345678910111213141516 | void Start(){ InitializeCriticalSection(&CrSc); // инициализация критической секции (см. ниже) STOP=0; // сброс флага останова BUFF=new double[size]; // буфер for(short i=0;i<size;i++) BUFF[i]=-2; // неопределённое (начальное) значение элементов буфера unsigned lpThreadId; //создание потока Thr=(HANDLE)_beginthreadex(NULL, // дескриптор защиты 0, // начальный размер стека ThFunc, // функция потока NULL, // параметр потока 0, // опции создания &lpThreadId); // идентификатор потока } |
Созданный поток сразу начинает выполнять функцию
ThFunc
NULL
Листинг № 3. Запись в буфер
12345678 | void Write(double *FROM){ EnterCriticalSection(&CrSc); //поток захватывает ресурс или ждёт его освобождения memcpy(BUFF,FROM, size*sizeof(double)); //копирование данных в буфер, из области памяти, заданной указателем FROM LeaveCriticalSection(&CrSc); //освобождение ресурса } |
Проблема монопольного доступа к буферу решается в данной реализации при помощи так называемых «критических секций». Участок исходного кода программы, заключённый между командами
EnterCriticalSection
LeaveCriticalSection
EnterCriticalSection
Листинг № 4. Чтение буфера
12345678 | void Read(double *TO){ EnterCriticalSection(&CrSc); //поток захватывает ресурс или ждёт его освобождения memcpy(TO,BUFF,size*sizeof(double)); //копирование данных из буфера, в область памяти, заданную указателем TO LeaveCriticalSection(&CrSc); //освобождение ресурса } |
Критическая секция реализуется с использованием структуры
CRITICAL_SECTION
InitializeCriticalSection
DeleteCriticalSection
TryEnterCriticalSection
InitalizeCriticalSectionAndSpinCount
Для реализации монопольного доступа к ресурсу можно использовать так называемые мьютексы. Данные объекты ядра гарантируют потокам взаимоисключающий доступ к единственному ресурсу. Объект создаётся функцией
CreateMutex
WaitForSingleObject
Мьютексы и критические секции одинаковы в том, как они влияют на планирование ждущих потоков, но различны по некоторым другим характеристикам [1]. Преимущество критических секций – простота в использовании и очень быстрое выполнение, так как реализованы на основе Interlocked-функций. Главный недостаток – нельзя синхронизировать потоки в разных процессах.
При использовании механизмов монопольного доступа необходимо тщательно спланировать программу таким образом, чтобы исключить возможность взаимной блокировки (deadlock).
Завершение потока можно реализовать четырьмя способами:
- Поток самоуничтожается вызовом функции (аналогично ситуации с функцией
ExitThread
, необходима замена наCreateThread
)._endthreadex
- Один из потоков данного или стороннего процесса вызывает функцию (поток не уведомляется о своём уничтожении и не выполняет очистку своих ресурсов, которая может быть необходима).
TerminateThread
- Завершается процесс, содержащий данный поток.
- Функция потока возвращает управление.
Все способы, кроме последнего, являются нежелательными. Для сигнализации потоку о необходимости прекращения работы, в данной реализации используется переменная, атомарный доступ к которой осуществляется при помощи
InterlockedExchange
Листинг № 5. Завершение потока
1234567 | void Stop(){ InterlockedExchange(&STOP,1);// флаг «стоп» поднят WaitForSingleObject(Thr,INFINITE);// ждём завершения потока CloseHandle(Thr);// закрываем дескриптор DeleteCriticalSection(&CrSc);// удаляем критическую секцию delete []BUFF;// удаляем буфер } |
В цикле опроса устройства (внутри функции
ThFunc
STOP
Если значение переменной равно единице, происходит выход из цикла, уничтожение используемых в опросе объектов и возврат кода завершения при помощи
return
Так как поток не завершается мгновенно, необходимо приостановить выполнение дальнейших операций - уничтожение ресурсов, которые поток может использовать, до его полного завершения. Корректной реализацией такого ожидания является не задержка на фиксированный большой, достаточный для завершения потока, интервал времени, а вызов функции
WaitForSingleObject
Дополнительные аспекты
При разработке многопоточного приложения придётся отказаться от паттерна проектирования «одиночка» (singleton, [6]): так называемый синглтон Майерса не является потокобезопасным. Полностью переносимое решение проблемы в рамках существующего стандарта языка отсутствует [7].
Поток приложения (даже однопоточного) может последовательно выполняться на разных ядрах. По умолчанию Windows использует нежесткую привязку (soft affinity) потоков к процессорам для повторного использования данных в кэше процессора, программист может дополнительно реализовать жёсткую привязку (hard affinity) при помощи функции
SetProcessAffinityMask
На некоторых многоядерных процессорах при работе в ОС Windows Server 2000, Windows Server 2003 и Windows XP описанное выше на разных ядрах приводит к сложной ситуации при взаимодействии со счётчиком производительности ядра [8], вызванной разной, отличной от номинальной, частотой работы ядер. Такая ситуация вызвана применением технологий автоматического понижения (AMD Cool'n'Quiet и Intel SpeedStep) или повышения (AMD Turbo Core и Intel Turbo Boost) частоты работы ядер. Таким образом, функция
QueryPerformanceCounter
В зависимости от конкретной решаемой задачи возможно использование механизмов пула потоков [5], упрощающих создание, уничтожение и контроль за потоками.
При разработке высокоэффективных приложений стоит уделить внимание следующим аспектам - приоритеты потоков, кэш-линии процессора, привязка потоков к процессорам и прочее.
Заключение
- Асинхронный опрос позволяет повысить частоту обработки сигнала и снизить время ожидания ответа за счёт распараллеливания задачи с надлежащим использованием механизмов межпроцессного взаимодействия.
- Атомарный доступ к отдельной переменной в пользовательском режиме реализуется посредством Interlocked-функций.
- Спин-блокировка – не всегда лучший примитив синхронизации. Затраты на переход в режим ядра для использования его объектов могут не оправдаться.
- Целесообразность применения мьютексов или критических секций определяется конкретной решаемой задачей.
- Завершение потока вызовами (самоуничтожение),
ExitThread
(завершение по запросу от другого потока) или окончанием процесса, содержащего данный поток, некорректно.TerminateThread
- Корректный алгоритм определения факта завершения потока – вызов функции с передачей в неё дескриптора потока.
WaitForSingleObject
- Вместо функций Windows API и
CreateThread
должны использоваться эквивалентные функции из библиотеки применяемого компилятора.ExitThread
- На работоспособность многопоточного приложения может повлиять версия операционной системы: некоторые функции могут быть определены, но не реализованы, или не поддерживать некоторые значения аргументов. Windows 98 при работе с многоядерными процессорами использует только одно ядро, игнорируя остальные.
- При разработке многопоточного приложения во многих случаях целесообразно отказаться от паттерна проектирования «одиночка».
- Если счётчик производительности ядра возвращает некорректные значения, следует, в первую очередь, убедиться, что эта ситуация не является следствием особенностей взаимодействия аппаратной части ПЭВМ и операционной системы.
Библиографический список
- Таненбаум Э. Современные операционные системы. 3-е изд. СПб: Питер, 2010. 1120 с.
- Страуструп Б. Язык программирования С++. М.: Бином, 2011. 1136 с.
- Бей И. Взаимодействие разноязыковых программ. Руководство программиста. М.: Вильямс, 2005. 880 с.
- Экспериментальные исследования работы человека-оператора в режиме слежения при установке на максимальное быстродействие // Тр. ГосНИИАС. Вопросы авионики. 2010. Вып. 1(19). С. 22 - 32.
- Рихтер Дж. Windows для профессионалов: создание эффективных Win32 приложений с учетом специфики 64-разрядной версии Windows: пер. c англ. 4-е изд. СПб.: Питер, 2001. 752 с.
- Приемы объектно-ориентированного проектирования. Паттерны проектирования / Э. Гамма и др. СПб: Питер, 2007. 366 с.
- Meyers S., Alexandrescu A. C++ and the Perils of Double-Checked Locking // Dr. Dobbs Journal. 2004. July – August. С. 46–61.
- Programs that Use the QueryPerformanceCounter Function May Perform Poorly in Windows Server 2000, in Windows Server 2003, and in Windows XP [Электронный ресурс] // Microsoft Technical support. [сайт]. [2011]. URL: http://support.microsoft.com/kb/895980/en-us?fr=1 (дата обращения: 24.05.2012).
Комментарии