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

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

Критические секции

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

Вот пример кода, который демонстрирует, что может произойти без критической секции:

const int MAX_TIMES = 1000,

int g_nIndex - 0,

DWORD g_dwTimes[MAX_TIMES];

DWORD WINAPI FirstThread(PVOID pvParam)

{

while (g_nIndex < MAX_TIMES)

{

g_dwTimes[g__nIndex] = GetTickCount(); g_nIndex++;

}

return(0),

}

DWORD WINAPI SecondThread(PVOID pvParam)

{

while Cg_nIndex < MAX_TIMES)

{

g_nIndex++;

g_dwTimes[g_nIndex - 1] = GetTickCount();

}

return(0);

}

Здесь предполагается, что функции обоих потоков дают одинаковый результат, хоть они и закодированы с небольшими различиями. Если бы исполнялась только функция FirstThread, она заполнила бы массив g_dwTimes набором чисел с возраста ющими значениями. Это верно и в отношении SecondThread - если бы она тоже ис полнялась независимо. В идеале обе функции даже при одновременном выполнении должны бы по-

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

Допустим, мы только что начали исполнение обоих потоков в системе с одним процессором Первым включился в работу второй поток, т e функция SecondThread (что вполне вероятно), и только она успела увеличить счетчик g_nIndex 1, как си

стема вытеснила ее поток и перешла к исполнению FtrstThread Та заносит в g_dwTi mes[1] показания системного времени, и процессор вновь переключается на испол нение второго потока. SecondThread теперь присваивает элементу g_dwTtmes[1 - 1] новые показания системного времени Поскольку эта операция выполняется позже, новые показания, естественно, выше, чем записанные в элемент g_dwTimes[1]фyнк цией FirstThread Отметьте также, что сначала заполняется первый элемент массива и только потом нулевой. Таким образом, данные в массиве оказываются ошибочными.

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

Теперь, когда Вы видите все «подводные камни», попробуем исправить этот фраг мент кода с помощью критической секции:

const int MAX_TIMES = 1000; int g_nIndex = 0;

DWORD g_dwTimes[MAX_TIMES]; CRITICAL_SECTION g_cs;

DWORD WINAPI FirstThread(PVOID pvParam)

{

for (BOOL fContinue = TRUE; fContinue; )

{

EnterCriticalSection(&g_cs); if (g_nIndex < MAX_TIMES)

{

g_dwTimes[g_nlndex] = GetTickCount(); g_nIndex++;

}

else

fContinue = FALSE; LeaveCriticalSection(&g_cs);

}

return(0);

}

DWORD WINAPI SecondThread(PVOID pvParam)

{

for (BOOL fContinue = TRUE; fContinue; )

{

EnterCriticalSection(&g_cs); if (g__nIndex < MAX_TIMES)

{

g_nIndex++;

g_dwTimes[g_nIndex - 1] = GetTickCount();

}

else

fContinue = FALSE; LeaveCriticalSecLion(&g_cs);

}

return(0);

}

Я создал экземпляр структуры данных CRITICAL_SECTION — g_cs, а потом «обер нул" весь код, работающий с разделяемым ресурсом (в нашем примере это строки с g_nIndex и g_dwTimes), вызовами EnterCriticalSection и LeaveCriticalSection. Заметьте, что при вызовах этих функций я передаю адрес g_cs.

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

Если у Вас есть ресурсы, всегда используемые вместе, Вы можете поместить их в одну кабинку — единственная структура CRITICAL_SECTION будет охранять их всех. Но если ресурсы не всегда используются вместе (например, потоки 1 и 2 работают с одним ресурсом, а потоки 1 и 3 — с другим), Вам придется создать им по отдельной кабинке, или структуре CRITICAL_SECTION.

Теперь в каждом участке кода, где Вы обращаетесь к разделяемому ресурсу, вы зывайте EnterCriticaSection, передавая ей адрес структуры CRITICAL_SECTION, кото рая выделена для этого ресурса. Иными словами, поток, желая обратиться к ресурсу, должен сначала убедиться, нет ли на двери кабинки знака «занято». Структура CRITI CAL_SECTION идентифицирует кабинку, в которую хочет войти поток, а функция EnterCriticalSection — тот инструмент, с помощью которого он узнает, свободна или занята кабинка. EnterCriticalSection допустит вызвавший ее поток в кабинку, если оп ределит, что та свободна. В ином случае (кабинка занята) EnterCriticalSection заставит его ждать, пока она не освободится.

Поток, покидая участок кода, где он работал с защищенным ресурсом, должен вызвать функцию LeaveCriticalSection. Тем самым он уведомляет систему о том, что кабинка с данным ресурсом освободилась. Если Вы забудете это сделать, система бу дет считать, что ресурс все еще занят, и не позволит обратиться к нему другим жду щим потокам, То есть Вы вышли из кабинки и оставили на двери знак "занято".

NOTE:

Самое сложное — запомнить, что любой участок кода, работающего с разде ляемым ресурсом, нужно заключить в вызовы функций EnterCrtticalSection и LeaveCriticalSection. Если Вы забудете сделать это хотя бы в одном месте, ре сурс может быть поврежден Так, если в FirstThread убрать вызовы EnterCritical Section и LeaveCriticalSection, содержимое переменных g_nIndex и g_dwTimes станет некорректным — даже несмотря на то что в SecondThread функции

EnterCriticalSection и LeaveCriticalSection вызываются правильно.

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

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

Критические секции: важное дополнение

Теперь, когда у Вяс появилось общее представление о критических секциях (зачем они нужны и как с их помощью можно монопольно распоряжаться разделяемым ресур сом), давайте повнимательнее приглядимся ктому, как они устроены Начнем со струк туры CRITICAL_SECTION. Вы не найдете ее в Platform SDK — о ней нет даже упомина ния. В чсм дело?

Хотя CRITICAL_SECTION не относится к недокументированным структурам, Micro soft полагает, что Вам незачем знать, как она устроена И это правильно. Для нас она нвляется своего рода черным ящиком - сама структура известна, а ее элементы — нет. Конечно, поскольку CRITICAL_SECTION — не более чем одна из структур, мы можем сказать, из чего она состоит, изучив заголовочные файлы. (CRITICAT,_SECTlON опреде лена в файле WinNT.h как RTL_CRITICAL_SECTION, а тип структуры RTL_CRITICAL_SEC TION определен в файле WinBase.h,) Но никогда не пишите код, прямо ссылающийся на ее элементы.

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

Обычно структуры CRITICAL_SECTION создаются как глобальные переменные, доступные всем потокам процесса. Но ничто не мешает нам создавать их как локаль ные переменные или переменные, динамически размещаемые в куче, Есть только два условия, которые надо соблюдать Во-первых, все потоки, которым может понадобить ся ресурс, должны знать адрес структуры CRITICAL_SECTION, которая защищает этот ресурс. Вы можете получить ее адрес, используя любой из существующих механиз мов. Во-вторых, элементы структуры CRITICAL_SECTION следует инициализировать до обращения какого-либо потока к защищенному ресурсу. Структура инициализи руется ВЫЗОВОМ:

VOID InitializeCriticalSection(PCRITICAL_SECTION pcs);

Эта функция инициализирует элементы структуры CRITICAL_SECTION, на которую указывает параметр pcs. Поскольку вся работа данной функции заключается в иници ализации нескольких переменных-членов, она не дает сбоев и поэтому ничего не возвращает (void). InitializeCriticalSection должна быть вызвана до того, как один иэ потоков обратится к EnterCriticalSection. В документации Platform SDK недвусмыслен но сказано, что попытка воспользоваться неинициализированной критической сек цией даст непредсказуемые результаты.

Если Вы знаете, что структура CRITICAL_SECTION больше не понадобится ни од-. ному потоку, удалите ее, вызвав DeleteCriticalSection:

VOID DeleteCriticalSection(PCRITICAL__SECTION pcs);

Она сбрасывает все переменные-члены внутри этой структуры. Естественно, нельзя удалять критическую секцию в тот момент, когда ею все еще пользуется ка кой-либо поток. Об этом нас предупреждают и в документации Platform SDK.

Участок кода, работающий с разделяемым ресурсом, предваряется вызовом:

VOID EnterCriticalSection(PCRITICAL_SECTION pcs);

Первое, что делает EnterCriticalSection, — исследует значения элементов структу ры CRITICAL_SECTION. Если ресурс занят, в них содержатся сведения о том, какой поток пользуется ресурсом. EnterCriticalSection выполняет следующие действия.

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

Если значения элементов структуры свидетельствуют, что ресурс уже захвачен вызывающим потоком, EnterCriticalSection обновляет их, отмечая тем самым, сколько раз подряд этот поток захватил ресурс, и немедленно возвращает уп равление. Такая ситуация бывает нечасто — лишь тогда, когда поток два раза подряд вызывает EnterCriticalSection без промежуточного вызова LeaweCritical

Section.

Если значения элементов структуры указывают на то, что ресурс занял другим потоком, EnterCriticalSection переводит вызывающий поток в режим ожидания. Это потрясающее свойство критических секций: поток, пребывая в ожидании, не тратит ни кванта процессорного времени Система запоминает, что данный поток хочет получить доступ к ресурсу, и - как только поток, занимавший этот ресурс, вызывает LeaveCriticalSection — вновь начинает выделять нашему пото ку процессорное время При этом она передает ему ресурс, автоматически обновляя элементы структуры CRITICAL_SECTION

Внутреннее устройство EnterCriticalSection не слишком сложно; она выполняет лишь несколько простых операций. Чем она действительно ценна, так это способно стью выполнять их на уровне атомарного доступа. Даже если два потока на много процессорной машине одновременно вызовут EnterCriticalSection, функция все равно корректно справится со своей задачей: один пошк получит ресурс, другой — перей дет в ожидание.

Поток, переведенный EnterCriticalSection в ожидание, может надолго лишиться доступа к процессору, а в плохо написанной программе — даже вообще не получить его. Когда именно так и происходит, говорят, что поток "голодает".

WINDOWS 2000

В действительности потоки, ожидающие освобождения критической секции, никогда не блокируются «навечно» EnterCriticalSection устроена так, что по истечении определенного времени, генерирует исключение. После этого Вы можете подключить к своей программе отладчик и посмотреть, что в ней слу чилось. Длительность времени ожидания функцией EnterCriticaiSection опреде ляется значением параметра CriticalSectionTimeout, который хранится в следу ющем разделе системного реестра:

HKEY_LOCAL_MACHlNE\System\CurrentControlSet\Control\S ession Manager

Длительность времени ожидания измеряется в секундах и по умолчанию равна 2 592 000 сскунд (что составляет ровно 30 суток). Не устанавливайте слишком малое значение этого параметра (например, менее 3 секунд), так как иначе Вы нарушиге работу других потоков и приложений, которые обычно ждут освобождения критической секции дольше трех секунд.

Вместо EnterCriticalSection Вы можете воспользоваться;

BOOL TryEnterCriticalSection(PCRITICAL_SECTIQN pcs);

Эта функция никогда не приостанавливает выполнение вызывающего потока. Но возвращаемое ею значение сообщает, получил ли этот поток доступ к ресурсу. Если при ее вызове указанный ресурс занят другим потоком, она возвращает FALSE.

TryEnterCriticalSection позволяет потоку быстро проверить, доступен ли ресурс, и ссли нет, папяться чем-нибудь другим. Если функция возвращает TRUE, значит, она обновила элементы структуры CRITICAL_SECTION так, чтобы они сообщали о захва те ресурса вызывающим потоком. Отсюда следует, что для каждого вызова функции TryEnterCriticalScction, где она возвращает TRUE, надо предусмотреть парный вызов

LeaveCriticalSection.

WINDOWS 2000

В Windows 98 функция TryEnterCriticalSection определена, но не реализована. При

еевызове всегда возвращается FALSE.

Вконце участка кода, использующего разделяемый ресурс, должен присутствовать вызов.

VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);

Эта функция просматривает элементы структуры CRITICAL_SECTION и уменьша ет счетчик числа захватов ресурса вызывающим потоком на 1. Если его значение боль ше 0, LeaveCriticalSection ничего не делает и просто возвращает управление.

Если значение счетчика достигло 0, LeaveCnitcalSection сначала выясняет, есть ли в системе другие потоки, ждущие данный ресурс в вызове EnlerCriticalSection Если есть хотя бы один такой поток, функция настраивает значения элементов структуры, что бы они сигнализировали о занятости ресурса, и отдает его одному из ждущих пото ков (поток выбирается «по справедливости») Если же ресурс никому не нужен, Leave CriticalSection соответственно сбрасывает элементы структуры.