Особенности:
- volatile BOOL g_fFinished = FALSE;
/*volatile – загружаем значение из памяти при каждом обращении к переменной (отключает оптимизации компилятора).*/
int main() {
CreateThread (…, CalcFunc,…);
While (g_fFinished = = FALSE);
}
DWORD WINAPI CalcFunc (PVOID) {
…
g_fFinished = TRUE;
}
Если не будем использовать ключевое слово volatile, то возникнет риск того, что значения флага загрузятся в регистр процессора перед вычислением while, и в дальнейшем проверяться будет значение из регистра, т.е. ждущий процесс никогда не увидит окончания вычислений.
- Thr1 Thr2
При реализации спин-блокировки возможна ситуация, когда поток длительное время опрашивает условие входа E, периодически это условие оказывается истинным, тем не менее, поток не может войти в критический участок. Происходит «отталкивание» (starvation, голодание). Можно решить, добавив Sleep(0) в конец Thr1.
- Влияние КЭШ-линий.
volatile int x = 0; // Thread1() x++
volatile int y = 0; // Thread2() y++
Для ускорения работы с ОЗУ каждый из процессоров использует локальный КЭШ, однако подгрузка в КЭШ производится не побайтно, а загружается целиком КЭШ-линия (участок памяти, выровненный по 32-байтной границе). Если две переменные попадают в одну КЭШ-линию, то эффект от использования КЭШа пропадает: нужно обновлять данные и в ОЗУ и во втором КЭШе. Поэтому следует выровнять переменные по границам КЭШ-линии или вводить фиктивные переменные, чтобы гарантировать, что переменные не попадут в одну КЭШ-линию. Для того чтобы избежать этих проблем, в Windows реализован такой объект, как критическая секция.
|
|
Критическая Секция Windows
void InitializeCriticalSection (PCritical_Section);
void DeleteCriticalSection();
void EnterCriticalSection();
bool TryEnterCriticalSection();
void LeaveCriticalSection();
bool InitializeCriticalSectionAndCount();
setCriticalSectionSpinCount();
Достоинство: высокое быстродействие.
Недостатки: interlocked функции не переводят поток в режиме ожидания, нельзя указать тайм-аут, нельзя использовать при межпроцессном взаимодействии.
Синхронизация в режиме ядра
Для такой синхронизации используются объекты ядра, которые могут находиться в сигнальном (свободном) и несигнальном (занятом) состоянии.
Это – процессы, потоки, задания, файлы, консольный ввод, уведомления об изменении файлов, события, ожидаемые таймеры, мьютексы, семафоры.
WaitForSingleObject(HANDLE, DWORD dwMultiSec);
WaitForMultipleObject(…, BOOL fWaitAll, …);
Если происходит переход объект в сигнальное состояние в момент блокировки, то обычно происходит снятие блокировки, и выполняются некоторые действия, зависящие от объекта ядра.
Возвращают:
- В случае завершения тайм-аута: WAIT_TIMEOUT;
- В случае ошибки: WAIT_FAILED;
- “Указатель” на следующий в порядке ожидания объект: WAIT_OBJECT_0;
Чтобы узнать, какой именно объект перешел в сигнальное состояние, проверяем, что значение не равно WAIT_FAILED и WAIT_TIMEOUT и вычитаем из него WAIT_OBJECT_0 и получаем индекс в массиве, переданном в WaitForMultipleObject().
|
|
Управление объектами ядра
Создание CreateMutex()
Закрытие BOOL CloseHandle(HANDLE)
Каждый процесс имеет таблицу описателей:
При закрытии HANDLE мы удаляем описатель из таблицы и декрементируем счетчик i. Если i = 0 – уничтожение объекта.
Наследование: SECURITY_ATTRIBUTE sa;
sa.nlength = sizeof(sa);
sa.lpSecurityDescriptor=NULL; /*NULL – защита по умолчанию; определяет, кто может пользоваться объектом (при NULL – создатель процесса и администраторы) */
sa.bInHeritHandle=TRUE; /*наследовать HANDLE*/
HANDLE hMutex = CreateMutex (&sa, FALSE, NULL);
Для передачи описателя в дочерний процесс обычно используется командная строка или переменные окружения.
Если объект создается уже после создания дочернего процесса, то дочерний процесс не содержит наследованного описателя.