Визуальное программирование и MFC

       

Использование критических секций


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

int g_count; // глобальная переменная …… UINT MyThread(LPVOID pParam) { g_count=0; while(g_count++<100) { // здесь выполняются какие-то действия } return 0; }

Однако, здесь есть одна проблема, которую можно обнаружить, посмотрев сгенерированный ассемблерный код. Значение g_count загружается в регистр, увеличивается там и переписывается обратно в g_count. Предположим, g_count равно 40 и Windows прерывает выполнение рабочего потока сразу после того, как он загружает это значение в регистр. Пусть теперь управление получает основной поток и присваивает переменной g_count значение 100 (ожидая, что рабочий поток вслед за этим прекрарит свою работу). При возобновлении рабочий поток увеличивает значение регистра и записывает обратно в g_count число 41, стирая при этом предыдущее значение 100. Итог – цикл рабочего потока не завершился. Допустим, процедура потока модифицируется следующим образом:

…. g_count=0; while(g_count<100) { // здесь выполняются какие-то действия g_count++; } ….

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

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

Допустим, программа отслеживает показания времени как часы, минуты и секунды, а каждое из этих значений хранится в отдельной целочисленной переменной. Теперь представим, что значения времени совместно используются двумя потоками. Поток А изменяет значение времени и прерывается потоком Б после обновления часов, но до обновления минут и секунд. Результат: поток Б получает недостоверные показания времени.

Если для данного формата времени создается класс C++, то можно легко управлять доступом к данным, сделав элементы данных закрытыми и предусмотрев открытые функции-члены. Именно таков класс CHMS, рассматриваемый ниже. Следует отметить: в этом классе есть элемент данных типа CRITICAL_SECTION. (Здесь не применяется поддержка из MFC.) Конструктор вызывает Win32-функцию InitializeCriticalSection, а деструктор — DeleteCriticalSection. Таким образом, с каждым объектом CHMS связан объект критическая секция.

#include "stdafx.h" class CHMS { private: int m_nHr, m_nMn, m_nSc; CRITICAL_SECTION in_cs; public: CHMS() : m_nHr(0), m_nMn(0), m_nSc(0) { ::InitializeCriticalSection(&m_cs); } ~CHMS() { ::DeleteCriticalSection(&m_cs); } void SetTime(int nSecs) { ::EnterCriticalSection(&m_cs): m_nSc = nSecs % 60; m_nMn = (nSecs / 60) % 60; m_nHr = nSecs / 3600; ::LeaveCriticalSection(&m_cs); } void GetTotalSecs() { int nTotalSecs; ::EnterCriticalSection(&m_cs); nTotalSecs = m_nHr * 3600 + m_nMn * 60 + m_nSc; ::LeaveCriticalSection(&m_cs); return nTotalSecs; } void IncrementSecs() { ::EnterCriticalSection(&m_cs); SetTime(GetTotalSecs() + 1): ::LeaveCriticalSection(&m_cs); } };

Обратим внимание, что функции-члены вызывают функции EnterCriticalSection и LeaveCriticalSection. Если поток А исполняется в середине SetTime, поток Б будет блокирован вызовом EnterCriticalSection в GetTotalSecs до тех пор, пока поток А не вызовет LeaveCriticalSection. Функция IncrementSecs вызывает SetTime, что означает наличие вложенных критических секций. Это допустимо, так как Windows отслеживает уровни вложения.

Класс CHMS отлично работает, если он используется для конструирования глобальных объектов. Если же потоки программы совместно используют указатели на объекты в куче, то появляется другой спектр проблем. Каждый поток должен определять, не удален ли объект другим потоком, а значит, нужна синхронизация доступа к указателям.


Содержание раздела