Windows для профессионалов

       

Худшее, что можно сделать


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

Суть его в том, что поток синхронизирует себя с завершением какой-либо задачи в другом потоке, постоянно просматривая значение переменной, доступной обоим потокам Возьмем пример:

volatile BOOL q_fFinishedCalculation = FALSE;

int WINAPI WinMain( )
{
CreateThread( , RecalcFunc, );
... // ждем завершения пересчета
while (!g_fFinishedCalculation)
...
}

DWORD WINAPI RecalcFunc(PVOID pvParam)
{ // выполняем пересчет

g_fFinishedCalculation = TRUE;
return(0);
}

Как видите, первичный поток (он исполняет функцию WinMain) при синхронизации по такому событию, как завершение функции RecalcFunc, никогда не впадает в спячку. Поэтому система по-прежнсму выделяет ему процессорное время за счет других потоков, занимающихся чем-то более полезным.

Другая проблема, связанная с подобным методом опроса, в том, что булева переменная g_fFinishedCalculation может не получить значения TRUE — например, если у первичного потока более высокий приоритет, чем у потока, выполняющего функцию RecalcFunc. В этом случае система никогда не предоставит процессорное время потоку RecalcFunc, а он никогда не выполнит оператор, присваивающий значение TRUE переменной g_fFinishedCalculation. Если бы мы не опрашивали поток, выполняющий функцию WinMain, а просто отправили в спячку, это позволило бы системе отдать его долю процессорного времени потокам с более низким приоритетом, в частности потоку RecalcFunc.

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

Прежде всего позвольте обратить Ваше внимание на одну вещь: в начале приведенного выше фрагмента кода я использовал спецификатор volatile - без него работa моей программы просто немыслима. Он сообщает компилятору, что переменная может быть изменена извне приложения — операционной системой, аппаратным устройством или другим потоком. Точнее, спецификатор volatile заставляет компилятор исключить эту переменную из оптимизации и всегда перезагружать ее значение из памяти. Представьте, что компилятор сгенерировал следующий псевдокод для оператора while из предыдущего фрагмента кода:

MOV RegO, [g__fFinishedCalculation] ; копируем значение в регистр

Label TEST RegO, 0 ; равно ли оно нулю9

JMP RegO == 0, Label ; в регистре находится 0, повторяем цикл

... ;в регистре находится ненулевое значение

; (выходим из цикла)

Если бы я не определил булеву переменную как volatile, компилятор мог бы оптимизировать наш код на С именно так. При этом компилятор загружал бы ее значение в регистр процессора только раз, а потом сравнивал бы искомое значение с содержимым регистра. Конечно, такая оптимизация повышает быстродействие, поскольку позволяет избежать постоянного считывания значения из памяти, оптимизирующий компилятор скорее всего сгенерирует код именно так, как я показал. Но тогда наш поток войдет в бесконечный цикл и никогда не проснется. Кстати, если структура определена как volatile, таковыми становятся и все ее элементы, т e. при каждом обращении они считываются из памяти.

Вас, наверное, заинтересовало, а не следует ли объявить как volatile и мою переменную g_fResourcelnUse в примере со спин-блокировкой. Отвечаю: нет, потому что она передается Interlocked-функции по ссылке, а не по значению. Передача перемен ной по ссылке всегда заставляет функцию считывать ее значение из памяти, и оптимизатор никак не влияет на это.


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