Критические секции в С++

Теперь я расскажу вам, что такое критическая секция, зачем она нужна и как ее использовать. Конечно же статья будет о многопоточном программировании и увы только под Windows. Я буду использовать Windows7 x64 + VS2010 SP1.Для начала напишем очень простое приложение в несколько потоков.

#include <iostream>
#include <windows.h>
#include <process.h>

void ThreadRun(void* params); //функции потока

int main(int argc, char* argv[])
{
    for (int i = 0; i &lt; 20; ++i);
    std::cout << "Start thread!rn";
    _beginthread(ThreadRun, 0, NULL); //функция запуска потока
    int a; std::cin &lt;&lt; a; //ожидание ввода, что бы не закрылось окно
    return 0;
}

void ThreadRun(void* params)
{
    std::string line;
    line.append("I am a thread! Yeah!rn");
    std::cout &lt;&lt; line.c_str();
}

И так, функция _beginthread как вы наверно уже догадались создает новый поток ее прототип:

uintptr_t _beginthread(
   void( *start_address )( void * ),
   unsigned stack_size,
   void *arglist
);

Параметр start_address задает функцию потока, параметр stack_size — размер стека ну и arglist — список аргументов передаваемых потоку. Если поток удачно запущен, он вернет его идентификатор(handle).

Значит так:

  1. Создаем функцию для потока, которая должна возвращать void и принимать в качестве аргументов void* (это для случая с _beginthread);
  2. В главном потоке(или в любом другом) с помощью _beginthread запускам экземпляры потоков;

Все ну очень просто. 

Что же такое критическая секция? Критическая секция — это объект для синхронизации данных между потоками, то есть она не позволяет выполнять некие действия одновременно. Программист сам решает что «заключить» в критическую секцию. Как с ним работать?

CRTITICAL_SECTION csFlag; //объявление критической секции
...
InitializeCriticalSection(&csFlag); //инициализациия критической секции
...
EnterCriticalSection(&csFlag); //функция входа в критическую секция
...
LeaveCriticalSection(&csFlag); //функция выхода из критической секции
...
DeleteCriticalSection(&csFlag); //Удаление(деинициализация) критической секции

В книге Джеффри Рихтера «Windows для профессионалов», автор приводит такой пример:

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

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

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

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

Давайте создадим простую программу. В ней будет массив, и доступ к нему из разных потоков.

#include <iostream>
#include <Windows.h>
#include <process.h>
#include <queue>
#include <time.h>

//Описание функции потока
void ThreadRun(void* params);
//Функция которая будет выводить массив в консоль
void ArrayShow(int arr[]);
//Функция будет заполнять массив случайными числами
void ArrayChange(int arr[]);
//Наш объект(массив), к которому должен быть синхронизирован доступ
int myArray[10];
//Критическая секция для массива
CRITICAL_SECTION csArray; 

int main(int argc, char* argv[])
{
    //Заполним массив числами
    for (int i = 0; i < 10; ++i)
        myArray[i] = i;

    //Инициализируем критическую секцию
    InitializeCriticalSection(&csArray); 

    std::cout << "Array before:rn";
    ArrayShow(myArray);
    //Запустим 5 потоков
    for (int i = 0; i < 5; ++i)
    {
        //Каждому потоку, в качестве параметра передаем его номер
        int* number = new int;
        *number = i;
        _beginthread(ThreadRun, 0, (void*) number);
    }

    //Ждем, что бы пользователь сообщил о завершении потоков
    char isFinished = 'n';
    while (isFinished != 'y')
    {
        std::cout << "MAIN_THREAD: All threads is finished? (y/n)rn";
        std::cin >> isFinished;
    }

    //Удаляем критическую секцию
    DeleteCriticalSection(&csArray);

    std::cout << "Array after:rn";
    ArrayShow(myArray);

     //Пауза
    system("PAUSE");

    return 0;
}

void ThreadRun(void* params)
{
    //Преобразуем входящие параметры в int*
    int* myNumber = (int*) params;
    std::string start;
    std::string enter;
    std::string leave;

    char buff[10];
    itoa(*myNumber, buff, 10);

    start.append("Thread["); start.append(buff); start.append("] start...rn");
    enter.append("Thread["); enter.append(buff); enter.append("] enter in critical section...rn");
    leave.append("Thread["); leave.append(buff); leave.append("] leave critical section...rn");

    std::cout << start.c_str();

    //Входим в критическую секцию
    EnterCriticalSection(&csArray);
    std::cout << enter.c_str();
    //Задержка для наглядности процесса
    Sleep(1000);
    ArrayChange(myArray);
    ArrayShow(myArray);
    //Выходим из критической секции
    LeaveCriticalSection(&csArray);
    std::cout << leave.c_str();
}

void ArrayShow(int arr[])
{
    std::string line;
    for (int i = 0; i < 10; ++i)
    {
        char buff[10];
        itoa(arr[i], buff, 10);
        line.append(buff);
        line.append("-");
    }
    line.append("rn");
    std::cout << line.c_str();
}

void ArrayChange(int arr[])
{
    srand(time(NULL));
    for (int i = 0; i < 10; ++i)
    {
        myArray[i] = rand() % 100;
    }
}

 

Результат работы программы на изображении справа.

Как видно каждый поток по очереди изменил массив чисел, при этом никаких ошибок по поводу доступа к памяти не возникло и программа отработала корректно!

Подытожим:

  1. Создаем для каждого нужного объекта свою критическую секцию;
  2. Инициализируем секции перед тем, как работать с ними;
  3. Для того, что бы начать работу с объектом вызываем функцию EnterCriticalSection и передаем ей связанную(абстрактно) с объектом критическую секцию;
  4. После окончания работы с объектом, выходим из необходимой критической секции с помощью функции LeaveCriticalSection;
  5. Когда уже точно известно, что потоки больше не будут использовать критическую секцию, можем ее удалить с помощью функции DeleteCriticalSection.
  6. ПРОФИТ!

Удачи!

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *