Лекция №8. Особенности: volatile BOOL g_fFinished = FALSE

Особенности:

  • 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);

Для передачи описателя в дочерний процесс обычно используется командная строка или переменные окружения.

Если объект создается уже после создания дочернего процесса, то дочерний процесс не содержит наследованного описателя.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: