Важно отметить, что критические секции – это механизм для синхронизации потоков внутри одного процесса. Для работы с критическими секциями есть ряд функций API и тип данных CRITICAL_SECTION.
Для использования критической секции нужно создать переменную данного типа, и проинициализировать ее перед использованием с помощью функции InitializeCriticalSection().
CRITICAL_SECTION g_cs;
InitializeCriticalSection(&g_cs);
Для того, чтобы войти в секцию нужно вызвать функцию EnterCriticalSection(), а после завершения работы LeaveCriticalSection().
EnterCriticalSection(&g_cs);
LeaveCriticalSection(&g_cs);
Что будет, если поток обратится к секции, в которой сейчас другой поток? Тот поток, который обратится будет блокирован пока критическая секция не будет освобождена. Саму критическую секцию можно удалить функцией DeleteCriticalSection().
DeleteCriticalSection(&g_cs);
Для того, чтобы обойти блокировку потока при обращении к занятой секции есть функция TryEnterCriticalSection(), которая позволяет проверить критическую секцию на занятость.
Пример 1
Рассмотрим использование критических секций и полезность такого решения. Для начала посмотрим, что случится, если мы попробуем обратиться к одному ресурсу двумя потоками. Создадим два потока, которые одновременно захотят “пищать” системным динамиком. Сначала нарисуем две функции, которые будут выполняться потоками, а потом и сами потоки.
DWORD WINAPI firstFunc(LPVOID lpParam)
{
Beep(100, 500);
return (0);
}
DWORD WINAPI secondFunc(LPVOID lpParam)
{
Beep(400, 500);
return (0);
}
Вызовы CreateThread можно интегрировать, например, в функцию нажатия на кнопку.
DWORD dwFirst;
HANDLE firstThread = CreateThread(NULL, 0, firstFunc, NULL, NULL, dwFirst);
DWORD dwSecond;
HANDLE secondThread = CreateThread(NULL, 0, secondFunc, NULL, NULL, dwSecond);
/* Когда описатели нам больше не требуются, удалим их */
CloseHandle(firstThread);
CloseHandle(secondThread);
Как вы думаете, что произойдёт? Правильно, ничего того, что мы ожидали. Потоки будут драться и т.к. приоритеты у них одинаковы, то “пищать” будет только один – какой, не известно, но с большей вероятностью первый вызванный. Можете сами прослушать.
Разрешить данную ситуацию можно с помощью критической секции. Обычно структуры CRITICAL_SECTION создаются как глобальные переменные, доступные всем потокам процесса. Однако вы можете сузить область видимости данной структуры, исходя из специфики задач. Главное, чтобы структура CRITICAL_SECTION была в области видимости тех потоков, которые будут обращаться к разделяемому ресурсу. Следует выполнять два условия. Во-первых, все потоки, которым может понадобиться данный ресурс, должны знать адрес структуры CRITICAL_SECTION, которая защищает этот ресурс. Во-вторых, элементы структуры CRITICAL_SECTION следует инициализировать до обращения какого-либо потока к защищённому ресурсу.
CRITICAL_SECTION csOurSection;//это объявление структуры Критическая секция
Помня про условия использования критической секции, мы её инициализируем. Эту операцию провернём в функции OnInitDialog. Не забываем, что критическую секцию следует инициализировать до какого-либо к ней обращения.
InitializeCriticalSection(&csOurSection);
Теперь перепишем наши функции, учитывая наличие критической секции.
DWORD WINAPI firstFunc(LPVOID lpParam)
{
EnterCriticalSection(&csOurSection);
Beep(100, 500);
LeaveCriticalSection(&csOurSection);
return (0);
}
DWORD WINAPI SecondFunc(LPVOID lpParam)
{
EnterCriticalSection(&csOurSection);
Beep(400, 500);
LeaveCriticalSection(&csOurSection);
return (0);
}
Вызовы создания потоков, естественно, такие же.
Когда мы понимаем, что критическая секция нам больше не нужна, мы должны её удалить. Сделать это можно, например, в функции OnClose. (Создать её можно с помощью MFC Class Wizard, по сообщению WM_CLOSE нашего диалогового окна.). Для этого надо сделать вызов:
DeleteCriticalSection(&csOurSection);
Теперь первым выполняется поток, который первым вошёл в критическую секцию. Второй ждёт, а затем и сам выполняется. Сигнализируют потоки по очереди.
Пример 2
Задача о кольцевом буфере. Потоки производители и потребители разделяют кольцевой буфер, состоящий из 100 ячеек. Производители передают сообщение потребителям, помещая его в конец очереди буфера. Потребители сообщение извлекают из начала очереди буфера. Создать многопоточное приложение с потоками писателями и читателями. Предотвратить такие ситуации как, изъятие сообщения из пустой очереди или помещение сообщения в полный буфер. При решении задачи использовать критические секции.
Пусть для определенности буфер - это целочисленный массив из 100 элементов. Задача обладает двумя проблемными участками алгоритма. Первый из них связан с операциями чтения-записи нескольких потоков в общий буфер. Второй проблемный фрагмент определяется тем, что буфер являются конечным, запись должна производиться только в те ячейки, которые являются свободными или уже прочитаны потоками-читателями (условная взаимная синхронизация).
Для защиты обоих проблемных участков воспользуемся критической секцией. Она сделает возможным запись в буфер только одного потока-писателя. И она же сделает возможным чтение из буфера только одного потока-читателя. Операция чтения должна быть защищена, потому что она является и операцией записи тоже, так как поток, прочитавший ячейку буфера, обязан ее как-то пометить. Иначе, через определенное время выполнения программы, операция записи может стать невозможной или некорректной, в силу того, что буфер конечен. Операции чтения и записи могут проходить параллельно, так как всегда происходят в разных ячейках.
За условную синхронизацию будет отвечать та же самая критическая секция. Также, должна присутствовать переменная, отображающая, сколько ячеек в буфере свободно. Ячейка свободна, когда в нее еще не осуществлялась запись или ячейка была прочитана. Вторая переменная должна показывать, сколько ячеек в буфере занято. Естественно, операция записи не может быть выполнена, пока количество занятых ячеек равно 100 (или количество свободных ячеек равно 0), и операция чтения не может быть выполнена, пока количество свободных ячеек равно 100 (или количество занятых ячеек равно 0).
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <windows.h>
const int n = 100, // блина буфера
m = 7, // количество производителей
k = 3; // количество потребителей
int buf[n], front = 0, rear = 0;//кольцевой буфер его голова и хвост
CRITICAL_SECTION ArraySection;//секция на доступ к буферу
//процесс, пополняющий буфер
DWORD WINAPI Producer(PVOID pvParam)
{
int num;
num = *((int *)pvParam);
printf("thread %d (producer): start!\n",num);
while(true)
{
EnterCriticalSection( &ArraySection );
buf[rear] = rand()%n;
printf("\nproducer %d: data = %d to %d", num, buf[rear], rear);
rear = (rear+1)%n;
LeaveCriticalSection( &ArraySection );
Sleep(1000);
}
return 0;
}
//процесс, берущий данные из буфера
DWORD WINAPI Consumer(PVOID pvParam)
{
int num=0;
int data=0;
long prev=0;
num = *((int *)pvParam);
printf("thread %d (consumer): start!\n",num);
while(true)
{
EnterCriticalSection( &ArraySection );
data = buf[front];
printf("\nconsumer %d: data = %d from %d", num, data, front);
front = (front+1)%n;
LeaveCriticalSection( &ArraySection );
Sleep(1000);
}
return 0;
}
int main(int argc, char* argv)
{
int i, x[k+m];
DWORD dwThreadId[k+m];
HANDLE hThread[k+m];
InitializeCriticalSection(&ArraySection);
for(i=0;i<k;i++)
{
x[i] = i;
hThread[i] = CreateThread(NULL,0,Producer,(PVOID)&x[i], 0, &dwThreadId[i]);
if(!hThread) printf("main process: thread %d not execute!", i);
}
for(i=k;i<k+m;i++)
{
x[i] = i;
hThread[i] = CreateThread(NULL,0,Consumer,(PVOID)&x[i], 0, &dwThreadId[i]);
if(!hThread) printf("main process: thread %d not execute!", i);
}
WaitForMultipleObjects(k+m,hThread,TRUE,INFINITE);
// закрытие критической секции
DeleteCriticalSection(&ArraySection);
return 0;
}
Варианты заданий
1. Задача о napикмахере. В тихом городке есть парикмахерская. Салон парикмахерской мал ходить там может только парикмахер и один посетитель. Парикмахер всю жизнь обслуживает посетителей. Когда в салоне никого нет, он спит в кресле. Когда посетитель приходит и видит спящего парикмахера, он будет его, садится в кресло и спит, пока парикмахер занят стрижкой. Если посетитель приходит, а парикмахер занят, то он встает в очередь и засыпает. После стрижки парикмахер сам провожает посетителя. Если есть ожидающие посетители, то парикмахер будит одного из них, и ждет пока тот сядет в кресло парикмахера и начинает стрижку. Если никого нет, он снова садится в свое кресло и засыпает до прихода посетителя. Создать многопоточное приложение, моделирующее рабочий день парикмахерской. Условную синхронизацию потоков выполнить с помощью критических секций или событий.
2. Задача о Винни-Пухе или правильные пчелы. В одном лесу живут n пчел и один медведь, которые используют один горшок меда, вместимостью Н глотков. Сначала горшок пустой. Пока горшок не наполнится, медведь спит. Как только горшок заполняется, медведь просыпается и съедает весь мед, после чего снова засыпает. Каждая пчела многократно собирает по одному глотку меда и кладет его в горшок. Пчела, которая приносит последнюю порцию меда, будит медведя. Создать многопоточное приложение, моделирующее поведение пчел и медведя. Условную синхронизацию потоков выполнить с помощью критических секций.
3. Задача о читателях и писателях. Базу данных разделяют два типа процессов - читатели и писатели. Читатели выполняют транзакции, которые просматривают записи базы данных, транзакции писателей и просматривают и изменяют записи. Предполагается, что в начале БД находится в непротиворечивом состоянии (т.е. отношения между данными имеют смысл). Каждая отдельная транзакция переводит БД из одного непротиворечивого состояния в другое. Для предотвращения взаимного влияния транзакций процесс-писатель должен иметь исключительный доступ к БД. Если к БД не обращается ни один из процессов-писателей, то выполнять транзакции могут одновременно сколько угодно читателей. Создать многопоточное приложение с потоками-писателями и потоками-читателями. Реализовать решение, используя критические секции.