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

Критические секции и обработка ошибок

Вероятность того, что lnitializeCriticalSection потерпит неудачу, крайне мала, но все же существует. В свое время Microsoft не учла этого при разработке функции и опреде лила ее возвращаемое значение как VOID, т. e. она ничего не возвращает. Однако функция может потерпеть неудачу, так как выделяет блок памяти для внутрисистем ной отладочной информации. Если выделить память не удается, генерируется исклю чение STATUS_NO_MEMORY. Вы можете перехватить его, используя структурную об работку исключений (см. главы 23, 24 и 25).

Есть и другой, более простой способ решить эту проблему — перейти на новую функцию

InitializeCriticalSectionAndSpinCount. Она, тоже выделяя блок памяти для от ладочной информации, возвращает FALSE, если выделить память не удается.

В работе с критическими секциями может возникнуть ещс одна проблема. Когда за доступ к критической секции конкурирует два и более потоков, она использует объект ядра "событие" (Я покажу, как работать с этим объектом при описании C++ класса COptex в главе 10.) Поскольку такая конкуренция маловероятна, система не создает объект ядра «событие" до тех пор, пока он действительно не потребуется. Это экономит массу системных ресурсов — в большинстве критических секций конкурен ция потоков никогда не возникает.

Но если потоки все же будут конкурировать за критическую секцию в условиях нехватки памяти, система не сможет создать нужный объект ядра И тогда Enter CriticalSection возбудит исключение EXCEPTION_INVALID_HANDLE. Большинство раз работчиков просто игнорирует вероятность такой ошибки и не предусматривает для нее никакой обработки, поскольку она случается действительно очень редко Но если Вы хотите заранее подготовиться к такой ситуации, у Вас есть две возможности.v

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

EnterCriticalSection.

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

InitializeCriticalSectionAndSpinCount, передавая параметр dwSpinGount с уста

новленным старшим битом Тогда функция создает объект «событие" и сопоставляет его с критической секцией. Если создать объект не удается, она возвращает FALSE, и это позволяет корректнее обрабатывать такие ситуации. Но успешно созданный объ ект ядра «событие" гарантирует Вам, что EnterCriticalSection выполнит свою задачу при любых обстоятельствах и никогда не вызовет исключение. (Всегда выделяя память под объекты ядра «событие», Вы неэкономно расходуете системные ресурсы. Поэтому делать так следует лишь в нескольких случаях, а именно: если программа может рух нуть из-за неудачного завершения функции EnterCriticatlSection, если Вы уверены в конкуренции потоков при обращении к критической секции или если программа будет работать в условиях нехватки памяти.)

Несколько полезных приемов

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

На каждый разделяемый ресурс используйте отдельную структуру CRITICAL_SECTION

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

int g_nNums[100]; // один разделяемый ресурс

TCHAR g_cChars[100]; // Другой разделяемый ресурс

CRITICAL_SECTION g_cs, // защищает оба ресурса

DWORD WINAPI ThreadFunc(PVOID pvParam) { EnterCriticalSection(&g_cs);

for (int x = 0; x < 100: x++)

{

g_nNums[x] = 0; g_cChars|x] - TEXT('X');

}

LeaveCriticalSection(&g_cs); return(0);

}

Здесь создана единственная критическая секция, защищающая оба массива — g_nNums и g_cChars — в период их инициализации. Но эти массивы совершенно раз личны. И при выполнении данного цикла ни один из потоков нс получит доступ ни к одному массиву. Теперь посмотрим, что будет, если ThreadFunc реализовать так:

DWORD WINAPI ThreadFunc(PVOID pvParam)

{

EnterCriticalSection(&g_cs); for (int x = 0; x < 100; x++)

g_nNums[x] = 0;

for (x = 0; x < 100; x++)

g_cChars[x] = TEXT('X'); LeaveCriticalSection(&g_cs); return(0);

}

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

int g_nNum[100]; // разделяемый ресурс

CRITICAL_SECTION g_csNums; // защищает g_nNums

TCHAR g_cChars[100]; // другой разделяемый ресурс

CRITICAL_SECTION g_csChars; // защищает g_cChars

DWORD WTNAPT ThreadFunc(PVOTD pvParam)

{

EnterCriticalSection(&g_csNums); for (int x = 0; x < 100; x++) g_nNums[x] = 0; LeaveCriticalSection(&g_csNums); EnterCriticalSection(&g_csChars); for (x = 0; x < 100; x++) g_cChars[x] = TEXT('X'); LeaveCriticalSection(&g_ csChars); return(0);

}

Теперь другой поток сможет работать с массивом g_nNums, как только ThreadFunc закончит его инициализацию. Можно сделать и так, чтобы один поток инициализи ровал массив g_nNums, я другой — gcChars.

Одновременный доступ к нескольким ресурсам

Иногда нужен одновременный доступ сразу к двум структурам данных. Тогда Thread Func следует реализовать так:

DWORD WINAPI ThreadFunc(PVOID pvParam)

{

EnterCriticalSection(&g_csNums);

EnterCriticalSection(&g_csChars);

// в этом цикле нужен одновременный доступ к обоим ресурсам

for (int x = 0; x < 100; x++) g_nNums[x] = g_cChars[x];

LeaveCriticalSection(&g_csChars);

LeaveCrilicalSection(&g_csNums};

return(0);

}

Предположим, доступ к обоим массивам требуется и другому потоку в данном процессе; при этом его функция написана следующим образом:

DWORD WINAPI OtherThreadFunc(PVOID pvParam)

{

EnterCriticalSection(&g_csChars);

EnterCriticalSection(&g_csNums);

for (int x = 0; x < 100; x++) g_nNums[x] = g_cChars[x]; LeaveCriticalSection(&g_csNums); LeaveCriticalSection(&g_csChars); return(0);

}

Я лишь поменял порядок вызовов EnterCriticalSection и LeaveCriticalSection, Но из за того,

что функции ThreadFunc и OtherThreadFunc написаны именно так, существу

ет вероятность взаимной блокировки (deadlock) Допустим, ThreadFunc начинает ис полнение и занимает критическую секцию g_csNums Получив от системы процессор ное время, поток с функцией OtherThreadFunc захватывает критическую секцию g_csChars Тут-то и происходит взаимная блокировка потоков Какая бы из функций — ThreadFunc или OtherThreadFunc — ни пыталась продолжить исполнение, она не су меет занять другую, необходимую ей критическую секцию

Эту ситуацию легко исправить, написав код обеих функций так, чтобы они вызы вали EnterCriticalSection в одинаковом порядке Заметьте, что порядок вызовов Leave CrititalSection несуществен, поскольку эта функция никогда не приостанавливает поток

Не занимайте критические секции надолго

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

SOMESTRUCT g, s;

CRITICAL_SECTION g_cs;

DWORD WINAPI SomeThread(PVOID pvParam)

{

EnterCriticalSection(&g_cs);

// посылаем в окно сообщение

SendMessage(hwndSomeWnd, WM_SOMEMSG, &g_s, 0); LeaveCriticalSection(&g_cs);

return(0);

}

Трудно сказать, сколько времени уйдет на обработку WM_SOMEMSG оконной про цедурой — может, несколько миллисекунд, а может, и несколько лет В течение этого времени никакой другой поток не получит доступ к структуре g_s Поэтому лучше составить код иначе

SOMESTRUCT g_s;

CRITICAL_SECTION g_cs;

DWORO WINAPI SomeThread(PVOID pvParam)

{

EnterCriticalSection(&g_cs); SOMESTRUCT sTemp = g_s; LeaveCriticalSection(&g_cs);

// посылаем в окно сообщение

SendMessage(hwndSompWnd, WM_SOMEMSG, &sTemp, 0); return(0);

}