Синхронизация в многопоточных приложениях MFC - Критические секции

ОГЛАВЛЕНИЕ


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

В отличие от других объектов синхронизации, критические секции работают в пользовательском режиме, за исключением случаев, когда требуется переход в режим ядра. Если поток пытается выполнить код, который может быть критической секцией, он сначала выполняет циклическую блокировку и после заданного количества времени переходит в режим ядра, чтобы ждать критическую секцию. Фактически, критическая секция состоит из счетчика цикла и семафора; счетчик цикла предназначен для ожидания в пользовательском режиме, и семафор предназначен для ожидания в режиме ядра (режим ожидания). В Win32 API есть структура CRITICAL_SECTION, которая представляет объекты критической секции. В MFC есть класс CCriticalSection. В принципе, критическая секция - это часть исходного кода, требующая интегрированного выполнения, то есть выполнение этой части кода не должно прерываться никаким другим потоком. Такие части кода могут требоваться в случаях, когда необходимо предоставить одному потоку монополию на использование общего ресурса. Простой случай – использование глобальных переменных более чем одним потоком. Например: 

int g_nVariable = 0;

UINT Thread_First(LPVOID pParam)
{
    if (g_nVariable < 100)
    {
       ...
    }
    return 0;
}

UINT Thread_Second(LPVOID pParam)
{
    g_nVariable += 50;
    ...
    return 0;
}

Этот код не является безопасным, поскольку ни один поток не имеет монопольного доступа к переменной g_nVariable. Рассмотрим следующий сценарий: предположим, что начальное значение переменной g_nVariable равняется 80, управление передано первому потоку, который видит, что значение переменной g_nVariable меньше 100, и пытается выполнить фрагмент кода при данном условии. Но в это же время процессор переключается на второй поток, который прибавляет 50 к значению переменной, так что оно становится больше 100. Впоследствии процессор переключается обратно на первый поток и продолжает выполнять блок (фрагмент кода) условия. Внутри условного блока значение g_nVariable больше 100, хотя оно должно быть меньше 100. Чтобы справиться с этой проблемой, можно использовать критическую секцию таким образом:

 CCriticalSection g_cs;
int g_nVariable = 0;

UINT Thread_First(LPVOID pParam)
{
    g_cs.Lock();
    if (g_nVariable < 100)
    {
       ...
    }
    g_cs.Unlock();
    return 0;
}

UINT Thread_Second(LPVOID pParam)
{
    g_cs.Lock();
    g_nVariable += 20;
    g_cs.Unlock();
    ...
    return 0;
}

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

Если необходимо защитить более двух разделяемых ресурсов, следует использовать отдельную критическую секцию для каждого ресурса. Не забудьте согласовать Unlock с каждым Lock. При использовании критических секций нужно быть осторожным, чтобы не создавать ситуаций взаимной блокировки для потоков, работающих совместно. Это означает, что поток может ждать, пока критическую секцию не освободит другой поток, который в свою очередь ждет освобождения критической секции, захваченной первым потоком. Обычно в такой ситуации два потока будут ждать вечно.

 

Можно внедрять критические секции в классы C++ , таким образом, делая их ориентированными на многопоточное исполнение. Это может потребоваться, когда объекты определенного класса должны использоваться более чем одним потоком одновременно. Выглядит это примерно так:

class CSomeClass
{
    CCriticalSection m_cs;
    int m_nData1;
    int m_nData2;

public:
    void SetData(int nData1, int nData2)
    {
        m_cs.Lock();
        m_nData1 = Function(nData1);
        m_nData2 = Function(nData2);
        m_cs.Unlock();
    }

    int GetResult()
    {
        m_cs.Lock();
        int nResult = Function(m_nData1, m_nData2);
        m_cs.Unlock();
        return nResult;
    }
};

Возможно, что в один и тот же момент два или более потока вызовут методы SetData и/или GetData для одного и того же объекта типа CSomeClass. Следовательно, путем заключения в оболочку (обертывания) содержимого тех методов мы предотвращаем искажение данных во время вызовов тех методов.