
- •1.Архитектура операционных систем
- •1.1Общие вопросы архитектуры операционных систем
- •1.2Архитектура Windows
- •1.2.1История возникновения Windows
- •1.2.2Архитектура ос Windows
- •1.2.3История возникновения ос Linux
- •1.2.4Архитектура Linux
- •1.2.5Интерфейсы системы unix
- •1.2.6Файловая система unix
- •1.2.7Аутентификация в unix
- •1.2.8Сценарии командной оболочки unix
- •1.3Операционная система qnx
- •1.3.1 Архитектура qnx
- •1.4Выводы
- •1.5Вопросы для самоконтроля
- •2.Типы и алгоритмы работы с оперативной памятью
- •2.1Общие принципы функционирования подсистемы памяти в ос
- •2.1.1Обобщённые принципы управления памятью
- •2.1.2Однозадачная система без подкачки на диск
- •2.1.3Многозадачность с фиксированными разделами
- •2.1.4Подкачка
- •2.1.5Управление памятью с помощью битовых массивов
- •2.1.6Управление памятью с помощью связанных списков
- •2.1.7Виртуальная память
- •2.1.8Многоуровневые таблицы страниц
- •2.1.9Алгоритмы замещения страниц
- •2.2Виртуальная память ос Windows
- •2.2.1Архитектура памяти в ос Windows
- •2.2.2Работа с виртуальной памятью в ос Windows
- •2.2.3Использование виртуальной памяти в приложениях
- •2.3Пример организации страничной памяти на примере linux
- •2.3.1Страничная организация памяти в Linux
- •2.3.2Права доступа к области памяти
- •2.3.3Работа с областями памяти в Linux
- •3.Процессы и потоки
- •3.1Процессы
- •3.1.1Модель процесса
- •3.1.2Создание процесса
- •3.1.3Завершение процесса
- •3.1.4Состояния процессов
- •3.1.5Реализация процессов
- •3.2Потоки
- •3.2.1Реализация потоков
- •3.2.2Реализация потоков на уровне ядра
- •3.2.3Смешанная реализация
- •3.2.4 Метод управления «Активация планировщика»
- •3.2.5Всплывающие потоки
- •3.3Межпроцессное взаимодействие
- •3.3.1Состояние состязания
- •3.3.2Критические секции (Критические области)
- •3.3.3Взаимное исключение с активным ожиданием
- •3.3.4Примитивы межпроцессного взаимодействия
- •3.4Семафоры
- •3.5Мьютексы
- •3.6Организация многопоточной обработки в среде Windows
- •3.6.1Объекты ядра Windows
- •3.6.2Потоки Windows
- •3.6.3Синхронизация потоков в Windows
- •3.6.4Синхронизация потоков с помощью объектов ядра
- •3.6.5Сравнение объектов, используемых для синхронизации потоков
- •3.7Организация процессов и потоков в Linux
- •3.7.1Среда окружения в Linux
- •3.7.2Создание нового процесса. Системный вызов exec.
- •3.7.3Потоки unix. Функции потоков стандарта posix.
- •3.8Синхронизация потоков в unix
- •3.8.1Мьютексы
- •3.8.2Семафоры
- •0,0,0, //Ожидать обнуления семафора
- •0,1,0 // Затем увеличить значение семафора на 1};
- •0,1, 0 // Увеличитьзначение семафора на 1};
3.6.3Синхронизация потоков в Windows
В Windows существуют два основных способа синхронизации потоков: в пользовательском режиме и с использованием объектов ядра. Рассмотрим оба подхода к синхронизации потоков, начав с первого из них – синхронизации потоков в пользовательском режиме
Все потоки в системе должны иметь доступ к системным ресурсам — последовательным портам, файлам, окнам и т.д. Если один из потоков запросит монопольный доступ к какому-либо ресурсу, другим потокам, которым тоже нужен этот ресурс, не удастся выполнить свои задачи. А с другой стороны, просто недопустимо, чтобы потоки бесконтрольно пользовались ресурсами. Иначе может получиться так, что один поток пишет в блок памяти, из которого другой что-то считывает.
Потоки должны взаимодействовать друг с другом в двух основных случаях:
• совместно используя разделяемый ресурс (чтобы не разрушить его);
• когда нужно уведомлять другие потоки о завершении каких-либо операций.
Большая часть синхронизации потоков связана с атомарным доступом (atomic access) — монопольным захватом ресурса, обращающимся к нему потоком. Возьмем простой пример.
// определяем глобальную переменную
long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
g_x++;
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvParam}
{
g_x++;
return(0);
}
Я объявил глобальную переменную g_n и инициализировал ее нулевым значением. Создано два потока: один выполняет ThreadFunc1, другой — ThreadFunc2. Код этих функций идентичен: обе увеличивают значение глобальной переменной g_x на 1. Поэтому лочигно предположить, что когда оба потока завершат свою работу, значение g_x будет равно 2. Так ли это? Может быть. При таком коде заранее сказать, каким будет конечное значенис g_x, нельзя. И вот почему. Допустим, компилятор сгенерировал для строки, увеличивающей g_x на 1, следующий код:
MOV EAX, [g_x] ; значение из g_x помещается в регистр
INC EAX ; значение регистра увеличивается на 1
MOV [g_x], EAX ; значение из регистра помещается обратно в g_x
Вряд ли оба потока будут выполнять этот код в одно и то же время. Если они будут делать это по очереди — сначала один, потом другой, тогда мы получим такую картину:
MOV EAX, [g_x] ; поток 1 в регистр помещается 0
INC EAX ; поток 1. значение регистра увеличивается на 1
MOV [g_x], EAX ; поток 1. значение 1 помещается в g_x
MOV EAX, [g_x] ; поток 2. в регистр помещается 1
INC EAX ; поток 2. значение регистра увеличивается до 2
MOV [g_x], EAX ; поток 2. значение 2 помещается в g_x
После выполнения обоих потооков значение g_x будет равно 2. Это просто замечательно и как раз то, что мы ожидали: взяв переменную с нулевым значением, дважды увеличили ее на 1 и получили в результате 2. Прекрасно. Но Windows — это среда, которая поддерживает многопоточность и вытесняющую многозадачность. Значит, процессорное время в любой момент может быть отнято у одного потока и передано другому. Тогда код, приведенный мной выше, может выполняться и таким образом:
MOV EAX, [g_x] ; поток 1. в регистр помещается 0
INC EAX ; поток 1. значение регистра увеличивается на 1
MOV EAX, [g_x] ; поток 2 в регистр помещается 0
INC EAX ; поток 2. значение регистра увеличивается на 1
MOV [g_x], EAX ; поток 2. значение 1 помещается в g_x
MOV [g_x], EAX ; поток 1. значение 1 помещается в g_x
А если код будет выполняться именно так, конечное значение g_x окажется равным 1, а не 2, как мы думали! Очевидно, что в таких условиях работать просто нельзя. Мы вправе ожидать, что, дважды увеличив 0 на 1, при любых обстоятельствах получим 2. Кстати, результаты могут зависеть оттого, как именно компилятор генерирует машинный код, а также от того, как процессор выполняет этот код и сколько процессоров установлено в машине. Однако в Windows есть ряд функций, которые (при правильном их использовании) гарантируют корректные результаты выполнения кода.
Решение этой проблемы должно быть простым. Все, что нам нужно, — это способ, гарантирующий приращение значения переменной на уровне атомарного доступа, т.e. без прерывания другими потоками. Семейство Interlocked-функций как раз и дает нам ключ к решению подобных проблем. Все функции из этого семейства манипулируют переменными на уровне атомарного доступа.
LONG InterlockedExchangeAdd( PLONG plAddend, LONG lncrement);
Что может быть проще? Вы вызываете эту функцию, передавая адрес переменной типа LONG и указываете добавляемое значение. InterlockedExchangeAdd гарантирует, что операция будет выполнена атомарно. Перепишем вышеприведенный код вот так:
// определяем глобальную переменную
long g_x = 0;
DWORD WINAPI ThreadFunc1(PVOID pvParam)
{
InterlockedExchangeAdd(&g_x, 1);
return(0);
}
DWORD WINAPI ThreadFunc2(PVOID pvPararr)
{
InterlockedExchangeAdd(&g_x, 1);
return(0);
}
Теперь можно быть уверенным, что конечное значение g_x будет равно 2. Заметьте: в любом потоке, где нужно модифицировать значение разделяемой (общей) переменной типа LONG, следует пользоваться лишь Interlocked-функциями и никогда не прибегать к стандартным операторам языка С:
// переменная типа LONG, используемая несколькими потоками
LONG g_x;
// неправильный способ увеличения переменной типа LONG
g_x++;
// правильный способ увеличения переменной типа LONG
InterlockedExchangeAdd(&g_x, 1);
Работа Interlocked-функций серъёзно зависит от аппаратной архитектуры. На компьютерах с процессорами семейства x86 эти функции выдают по шине аппаратный сигнал, не давая другому процессору обратиться по тому же адресу памяти.
Другой важный аспект, связанный с Interlocked-функциями, состоит в том, что они выполняются чрезвычайно быстро. Вызов такой функции обычно требует не более 50 тактов процессора, и при этом не происходит перехода из пользовательского режима в режим ядра (а он отнимает не менее 1000 тактов).
Кстати, InterlockedExchangeAdd позволяет не только увеличить, но и уменьшить значение — просто передайте во втором параметре отрицательную величину. InterlockedExchangeAdd возвращает исходное значение в *plAddend
Вот еще две функции из этого семейства:
LONG InterlockedExchange( PLONG plTarget, LONG IValue);
PVOID InterlockedExchangePointer( PVOID* ppvTarget, PVOID* pvValue);
InterlockedExchange и InterlockedExchangePointer монопольно заменяют текущее значение переменной типа LONG, адрес которой передается в первом параметре, на значение, передаваемое во втором параметре. В 32-разрядпом приложении обе функции работают с 32-разрядными значениями, но в 64-разрядной программе первая оперирует с 32-разрядными значениями, а вторая — с 64-разрядными. Все функции возвращают исходное значение переменной. InterlockedExchange чрезвычайно полезна при реализации спин-блокировки (spinlock):
// глобальная переменная, используемая как индикатор того,
// занят ли разделяемый ресурс
BOOL g_fResourceInUse = FALSE ;
...
void Func1() {
// ожидаем доступа к ресурсу
while (InterlockedExchange(&g_fResourceInUse, TRUE) == TRUE)
Sleep(0);
// получаем ресурс в свое распоряжение
// доступ к ресурсу больше не нужен
InterlockedExchange(&g_fResourceInUse, FALSE);
}
В этой функции постоянно "крутится" цикл while, в котором переменной g_fResourceInUse присваивается значение TRUE и проверяется ее предыдущее значение. Если оно было равно FALSE, значит, ресурс не был занят, но вызывающий поток только что занял его, на этом цикл завершается. В ином случае (значение было равно TRUE) ресурс занимал другой поток, и цикл повторяется.
Если бы подобный код выполнялся и другим потоком, его цикл while работал бы до тех пор, пока значение переменной g_fResourceInUse вновь не изменилось бы на FALSE. Вызов InterlockedExchange в конце функции показывает, как вернуть переменной g_fResourceInUse значение FALSE.
Избегайте спин-блокировки на однопроцессорных машинах. "Крутясь" в цикле, поток впустую транжирит драгоценное процессорное время, не давая другому потоку изменить значение переменной. Применение функции Sleep в цикле while несколько улучшает ситуацию. С ее помощью Вы можете отправлять свой поток в сон на некий случайный отрезок времени и после каждой безуспешной попытки обратиться к ресурсу увеличивать этот отрезок. Тогда потоки не будут зря отнимать процессорное время.
Спин-блокировка предполагает, что защищенный ресурс не бывает занят надолго. И тогда эффективнее делать так: выполнять цикл, переходить в режим ядра и ждать. Многие разработчики повторяют цикл некоторое число раз (скажем, 4000) и, если ресурс к тому времени не освободился, переводят поток в режим ядра, где он спит, ожидая освобождения ресурса (и не расходуя процессорное время). По такой схеме реализуются критические секции (critical sections).
Последняя пара Interlocked-функций выглядит так:
PVOID InterlockedCompareExchange( PLONG pIOestination, LONG lExchange, LONG lComparand);
PVOID InterlockedCompareExchangePointer( PVOID* ppvDestination, PVOID pvExchange, PVOID pvComparand);
Они выполняют операцию сравнения и присвоения на уровне атомарного доступа. В 32-разрядном приложении обе функции работают с 32-разрядными значениями, но в 64-разрядном приложении InterlockedCompareExchange используется для 32 разрядных значений, a InterlockedCompareExchangePointer - для 64-разрядных. Вот как они действуют, если представить это в псевдокоде.
LONG InterlockedCompareExchange(PLONG plDestination, LONG lExchange, LONG lComparand) {
LONG lRet = *plDestination;// исходное значение
if (*plDestination == lComparand) *plDestination = lExchange;
return(lRet); }
Функция сравнивает текущее значение переменной типа LONG (на которую указывает параметр plDestination) со значением, передаваемым в параметре lComparand. Если значения совпадают, *plDestination получает значение параметра lExchange; в ином случае *plDestination остается без изменений. Функция возвращает исходное значение *plDestination. И не забывайте, что все эти действия выполняются как единая атомарная операция.
Обратите внимание на отсутствие Interlocked-функции, позволяющей просто считывать значение какой-то переменной, не меняя его. Она и не нужна. Если один поток модифицирует переменную с помощью какой-либо Interlocked-функции в тот момент, когда другой читает содержимое той же переменной, ее значение, прочитанное вторым потоком, всегда будет достоверным. Он получит либо исходное, либо измененное значение переменной. Поток, конечно, не знает, какое именно значение он считал, но главное, что оно корректно и не является некоей произвольной величиной. В большинстве приложений этого вполне достаточно.
Interlocked-функции можно также использовать в потоках различных процессов для синхронизации доступа к переменной, которая находится в разделяемой области памяти, например в проекции файла.
В Windows есть и другие функции из этого семейства, но ничего нового по сравнению с тем, что мы уже рассмотрели, они не делают. Вот еще две из них.
LONG Interlockedlncrement(PLONG plAddend);
LONG InterlockedDecrement(PLONG plAddend);
InterlockedExchangeAdd полностью заменяет обе эти устаревшие функции. Новая функция умеет добавлять и вычитать произвольные значения, а функции InterlockedIncrement и InterlockedDecrement увеличивают и уменьшают значения только на 1.
Критическая секция (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 (g_nIndex < MAX_TIMES)
{
g_nIndex++;
g_dwTimes[g_nIndex - 1] = GetTickCount();
}
return(0);
}
Здесь предполагается, что функции обоих потоков дают одинаковый результат, хоть они и закодированы с небольшими различиями. Если бы исполнялась только функция FirstThread, она заполнила бы массив g_dwTimes набором чисел с возрастающими значениями. Это верно и в отношении SecondThread - если бы она тоже исполнялась независимо. В идеале обе функции даже при одновременном выполнении должны бы по-прежнему заполнять массив тем же набором чисел. Но в нашем коде возникает проблема: масив g_dwTimes не будет заполнен, как надо, потому что функции обоих потоков одновременно обращаются к одним и тем же глобальным переменным. Вот как это может произойти.
Допустим, мы только что начали исполнение обоих потоков в системе с одним процессором. Первым включился в работу второй поток, т.e. функция SecondThread (что вполне вероятно), и только она успела увеличить счетчик g_nIndex, как система вытеснила ее поток и перешла к исполнению FirstThread. Та заносит в g_dwTimes[1] показания системного времени, и процессор вновь переключается на исполнение второго потока. SecondThread теперь присваивает элементу g_dwTtmes[i - 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_nIndex] = 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. Но если ресурсы не всегда используются вместе (например, потоки 1 и 2 работают с одним ресурсом, а потоки 1 и 3 — с другим), необходимо создать две различные структуры CRITICAL_SECTION.
Теперь в каждом участке кода, где есть обращение к разделяемому ресурсу, необходимо вызывать EnterCriticalSection, передавая ей адрес структуры CRITICAL_SECTION, которая выделена для этого ресурса. Если ресурс не занят – поток войдёт в критическую секцию, в противном случае – будет ожидать. Поток, покидая участок кода, где он работал с защищенным ресурсом, должен вызвать функцию LeaveCriticalSection. Её нужно вызывать обязательно, в противном случае ни один другой поток не получит доступа к ресурсу.
Хотя CRITICAL_SECTION не относится к недокументированным структурам, тем не менее поля данной структуры не дукументированы. Для разработчика она является своего рода черным ящиком - сама структура известна, а ее элементы — нет. Поэтому нельзя писать код, прямо ссылающейся на элементы структуры CRITICAL SECTION.
Работа со структурой CRITICAL_SECTION осузествляется через функции Windows API, принимающих в качестве параметра адрес экземпляра этой структуры. Функции сами знают, как обращаться с ее элементами, и гарантируют, что она всегда будет в согласованном состоянии.
Обычно структуры 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);
Она сбрасывает все переменные-члены внутри этой структуры. Естественно, нельзя удалять критическую секцию в тот момент, когда ею все еще пользуется какой-либо поток.
Участок кода, работающий с разделяемым ресурсом, предваряется вызовом:
VOID EnterCriticalSection(PCRITICAL_SECTION pcs);
Первое, что делает EnterCriticalSection, — исследует значения элементов структуры CRITICAL_SECTION. Если ресурс занят, в них содержатся сведения о том, какой поток пользуется ресурсом. EnterCriticalSection выполняет следующие действия.
Если ресурс свободен, EnterCriticalSection модифицирует элементы структуры, указывая, что вызывающий поток занимает ресурс, после чего немедленно возвращает управление, и поток продолжает свою работу, получив доступ к ресурсу.
Если значения элементов структуры свидетельствуют, что ресурс уже захвачен вызывающим потоком, EnterCriticalSection обновляет их, отмечая тем самым, сколько раз подряд этот поток захватил ресурс, и немедленно возвращает управление. Такая ситуация бывает нечасто — лишь тогда, когда поток два раза подряд вызывает EnterCriticalSection без промежуточного вызова LeaveCriticalSection.
Если значения элементов структуры указывают на то, что ресурс занят другим потоком, EnterCriticalSection переводит вызывающий поток в режим ожидания. Поток, пребывая в ожидании, не тратит ни кванта процессорного времени. Система запоминает, что данный поток хочет получить доступ к ресурсу, и - как только поток, занимавший этот ресурс, вызывает LeaveCriticalSection — вновь начинает выделять нашему потоку процессорное время. При этом она передает ему ресурс, автоматически обновляя элементы структуры CRITICAL_SECTION.
Внутреннее устройство EnterCriticalSection не слишком сложно; она выполняет лишь несколько простых операций. Чем она действительно ценна, так это способностью выполнять их на уровне атомарного доступа. Даже если два потока на много процессорной машине одновременно вызовут EnterCriticalSection, функция все равно корректно справится со своей задачей: один поток получит ресурс, другой — перейдет в ожидание.
Поток, переведенный EnterCriticalSection в ожидание, может надолго лишиться доступа к процессору, а в плохо написанной программе — даже вообще не получить его. Когда так происходит, говорят, что поток "голодает".
В действительности же потоки, ожидающие освобождения критической секции, никогда не блокируются "навечно" EnterCriticalSection устроена так, что по истечении определенного времени, генерирует исключение. После этого можно подключить к программе отладчик и посмотреть, что в ней случилось. Длительность времени ожидания функцией EnterCriticalSection определяется значением параметра CriticalSectionTimeout, который хранится в следующем разделе системного реестра:
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager
Длительность времени ожидания измеряется в секундах и по умолчанию равна 2 592 000 секунд (что составляет ровно 30 суток). Слишком малое значение этого параметра (например, менее 3 секунд) нарушит работу других потоков и приложений, которые обычно ждут освобождения критической секции трёх дольше.
Вместо EnterCriticalSection можно также применить следующую функцию:
BOOL TryEnterCriticalSection(PCRITICAL_SECTION pcs);
Эта функция никогда не приостанавливает выполнение вызывающего потока. Но возвращаемое ею значение сообщает, получил ли этот поток доступ к ресурсу. Если при ее вызове указанный ресурс занят другим потоком, она возвращает FALSE.
TryEnterCriticalSection позволяет потоку быстро проверить, доступен ли ресурс, и если нет, выполнять другие задачи. Если функция возвращает TRUE, значит, она обновила элементы структуры CRITICAL_SECTION так, чтобы они сообщали о захвате ресурса вызывающим потоком. Отсюда следует, что для каждого вызова функции TryEnterCriticalScction, где она возвращает TRUE, надо предусмотреть парный вызов LeaveCriticalSection.
В Windows 98 функция TryEnterCriticalSection определена, но не реализована. При ее вызове всегда возвращается FALSE.
В конце участка кода, использующего разделяемый ресурс, должен присутствовать вызов.
VOID LeaveCriticalSection(PCRITICAL_SECTION pcs);
Эта функция просматривает элементы структуры CRITICAL_SECTION и уменьшает счетчик числа захватов ресурса вызывающим потоком на 1. Если его значение больше 0, LeaveCriticalSection ничего не делает и просто возвращает управление.
Если значение счетчика достигло 0, LeaveCnitcalSection сначала выясняет, есть ли в системе другие потоки, ждущие данный ресурс в вызове EnlerCriticalSection. Если есть хотя бы один такой поток, функция настраивает значения элементов структуры, что бы они сигнализировали о занятости ресурса, и отдает его одному из ждущих потоков (поток выбирается "по справедливости"). Если же ресурс никому не нужен, Leave CriticalSection соответственно сбрасывает элементы структуры.
Как и EnterCriticalSection, функция LeaveCriticalSection выполняет все действия на уровне атомарного доступа. Однако LeaveCriticalSection никогда не приостанавливает поток, а управление возвращает немедленно.
Когда поток пытается войти в критическую секцию, занятую другим потоком, он немедленно приостанавливается. А это значит, что поток переходит из пользовательского режима в режим ядра (на что затрачивается около 1000 тактов процессора). Цена такого перехода чрезвычайно высока. На многопроцессорной машине поток, владеющий ресурсом, может выполняться на другом процессоре и очень быстро освободить ресурс. Тогда появляется вероятность, что ресурс будет освобожден еще до того, как вызывающий поток завершит переход в режим ядра. В итоге уйма процессорного времени будет потрачена впустую.
Microsoft повысила быстродействие критических секций, включив в них спин-блокировку. Когда вызывается EnterCriticalSection, она выполняет заданное число циклов спин-блокировки, пытаясь получить доступ к ресурсу и лишь в том случае, когда все попытки закапчиваются неудачно, функция переводит поток в режим ядра, где он будет находиться в состоянии ожидания.
Для использования спин-блокировки в критической секции нужно инициализировать счетчик циклов, вызвав:
BOOL InitalizeCriticalSectionAndSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpinCount);
Как и в InitializeCriticalSection, первый параметр этой функции — адрес структуры критической секции. Но во втором параметре, dwSpinCount, передается число циклов спин-блокировки при попытках получить доступ к ресурсу до перевода потока в состояние ожидания. Этот параметр может принимать значения от 0 до 0x00FFFFFF. Учтите, что на однопроцессорной машине значение параметра dwSpinCount игнорируется и считается равным 0. Дело в том, что применение спин-блокировки в такой системе бессмысленно: поток, владеющий ресурсом, не сможет освободить его, пока другой поток "крутится" в циклах спин-блокировки.
Вы можете изменить счетчик циклов спин-блокировки вызовом:
DWORD SetCriticalSectionSpinCount(PCRITICAL_SECTION pcs, DWORD dwSpinCount);
И в этой функции значение dwSpinCount на однопроцессорной машине игнорируется.
Вероятность того, что InitializeCriticalSection потерпит неудачу, крайне мала, но все же существует. В свое время Microsoft не учла этого при разработке функции и определила ее возвращаемое значение как VOID, т.e. она ничего не возвращает. Однако функция может потерпеть неудачу, так как выделяет блок памяти для внутрисистемной отладочной информации. Если выделить память не удается, генерируется исключение STATUS_NO_MEMORY. Вы можете перехватить его, используя структурную обработку исключений (см. ниже).
Есть и другой, более простой способ решить эту проблему — перейти на новую функцию InitializeCriticalSectionAndSpinCount. Она, тоже выделяя блок памяти для отладочной информации, возвращает FALSE, если выделить память не удается.
Используя критические секции, желательно привыкнуть делать одни вещи и избегать других. Вот несколько полезных приемов, которые пригодятся в работе с критическими секциями. (Они применимы и к синхронизации потоков с помощью объектов ядра, о ней ниже)
На каждый разделяемый ресурс используйте отдельную структуру 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 WTNAPI ThreadFunc(PVOID 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.
Иногда нужен одновременный доступ сразу к двум структурам данных. Тогда ThreadFunc следует реализовать так:
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);
LeaveCriticalSection(&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 в одинаковом порядке. Заметьте, что порядок вызовов LeaveCriticalSection несуществен, поскольку эта функция никогда не приостанавливает поток.
Крические секции в приложении должны быть заняты минимальное количество времени. Надолго занимая критическую секцию, приложение может блокировать другие потоки, что отрицательно скажется на его общей производительности.