Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
385
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

//для чтения и записи

};

Аэто усовершенствованная версия той же структуры.

//определяем размер кэш-линии используемого процессора

#ifdef _X86_

#define CACHE_ALIGN 32 #endif

#ifdef _ALPHA_

#define CACHE_ALIGN 64 #endif

#ifdef _IA64_

#define CACHE_ALIGN ?? #endif

#define CACHE_PAD(Name, BytesSoFar) BYTE Name[CACHE_ALIGN - ((BytesSoFar) % CACHE_ALIGN)]

struct CUSTINFO

{

DWORD dwCustomerID;

//в осноеном "только для чтения" char szName[100];

//в основном "только для чтения"

//принудительно помещаем следующие элементы в другую кэш-линию

CACHE_PAD(bPad1, sizeof(DWORD) + 100);

int nBalanceDue;

//для чтения и записи

FILETIME ftLastOrderDate;

//для чтения и записи

//принудительно помещаем следующую структуру в другую кэш-линию

CACHE_PAD(bPad2, sizeof(int) + sizeof(FILETIME));

};

Макрос CACHE_ALIGN неплох, но не идеален. Проблема в том, что байтовый раз мер каждого элемента придется вводить в макрос вручную, а при добавлении, пере мещении или удалении элемента структуры — еще и модифицировать вызов макроса CACHE_PAD. В следующих версиях компилятор Microsoft C/C++ будет поддерживать новый синтаксис, упрощающий выравнивание элементов структур. Это будет что-то вроде

__declepec(align(32)).

NOTE:

Лучше всего, когда данные используются единственным потоком (самый про стой способ добиться этого — применять параметры функций и локальные пе ременные) или одним процессором (это реализуется привязкой потока к оп ределенному процессору). Если Бы пойдете по такому пути, можете вообще забыть о проблемах, связанных с кэш-линиями.

Более сложные методы синхронизации потоков

Interlocked-функции хороши, когда требуется монопольно изменить всего одну пере менную С них и надо начинать Но реальные программы имеют дело со структурами данных, которые гораздо сложнее единственной 32или 64-битной переменной Что бы получить доступ на атомарном уровне к таким структурам данных, забудьте об Interlocked-функциях и используйте другие механизмы, предлагаемые Windows

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

Когда поток хочет обратиться к разделяемому ресурсу или получить уведомление о некоем "особом событии", он должен вызвать определенную функцию операцион ной системы и передать ей параметры, сообщающие, чего именно он ждет Как толь ко операционная система обнаружит, что ресурс освободился или что "особое собы тие» произошло, эта функция вернет управление потоку, и тот снова будет включен в число планируемых (Это не значит, что поток тут же начнет выполняться, система подключит его к процессору по правилам, описанным в предыдущей главе )

Пока ресурс занят или пока не произошло "особое событие", система переводит поток в ждущий режим, исключая его из числа планируемых, и берет на себя роль агента, действующего в интересах спящего потока Она выведет его из ждущего ре жима, когда освободится нужный ресурс или произойдет "oco6oc событие"

Большинство потоков почти постоянно находится в ждущем режиме И когда си стема обнаруживает, чю все потоки уже несколько минут спят, срабатывает механизм управления электоропитанием

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

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

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

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. при каждом об ращении они считываются из памяти.