Страницы

Поиск по вопросам

среда, 26 февраля 2020 г.

Почему критическая секция не работает как мьютекс?

#cpp #многопоточность #winapi #mutex


У меня есть глобальная переменная global равная 100, и есть 2 потока, каждый из которых
12 раз увеличивает значение этой глобальной переменной, т.е. в итоге её значение должно
быть равным 124. Сначала я использовал для синхронизации мьютекс и всё было отлично,
т.к. потоки выполнялись поочерёдно. После я решил заменить мьютекс на критическую секцию,
и вроде бы я в итоге получаю 124, но при этом потоки не выполняются поочередно чего
я никак не мог ожидать, может быть кто-нибудь знает в чём тут дело?

Собственно код:

#include "stdafx.h" 
#include  
#include  
#include 
using namespace std;

int global = 100;
HANDLE ht1, ht2; 
CRITICAL_SECTION cs;
DWORD WINAPI ThreadProc1(LPVOID lpParameter ) { 
    int i, j;   
    for (j=1; j <= 12; j++)    {        
        EnterCriticalSection(&cs);   
        i = global;      
        i++;     
        Sleep (1);      
        global = i; 
        printf_s( "%4s %4d \n", " 1 th", i );     
        LeaveCriticalSection(&cs);   
    } 
    return 0;  
}
DWORD WINAPI ThreadProc2 (LPVOID lpParameter) { 
    int i, j; 
    for (j=1; j <= 12; j++)    {    
        EnterCriticalSection(&cs);   
        i = global;      
        i++;     
        Sleep (1);     
        global = i;
        printf_s( "%4s %4d %4d \n", " 2 th", i, j );     
        LeaveCriticalSection(&cs);    
    } 
    return 0; 
} 
int _tmain(int argc, _TCHAR* argv[]) { 
    HANDLE msh[2]; 
    InitializeCriticalSection(&cs);
    ht1 = CreateThread(NULL, 0, &ThreadProc1, NULL, 0, NULL); 
    ht2 = CreateThread(NULL, 0, &ThreadProc2, NULL, 0, NULL); 
    msh[0] = ht1; 
    msh[1] = ht2;
    Sleep(500);
    DeleteCriticalSection(&cs);
    return 0; 
} 


Такой результат я получаю с использованием мьютекса:


А вот результат с использованием критической секции:

    


Ответы

Ответ 1



А с чего им выполняться поочередно? Выделено время до переключения - и пошел-пошел-пошел, сколько успеет... Поток переключился, пошел-пошел-пошел второй... Возможное переключение при Sleep(1) не сработает - оно в критическом разделе. Так что у второго потока шанса запуститься по сути нет... Вот один поток и гонит весь цикл, а потом второй. А мьютекс у вас (код вы не привели, будем ванговать...) пропускает один поток, второй ждет. Мьютекс отпущен, второй тут же включился, первый на своей итерации влетает в ожидание. Второй отпускает - захватывает первый... Ну, и так далее. Без вашего кода трудно точно сказать. А вот если вынести Sleep за пределы раздела - типа for (j=1; j <= 12; j++) { EnterCriticalSection(&cs); i = global; i++; global = i; printf_s( "%4s %4d \n", " 1 th", i ); LeaveCriticalSection(&cs); Sleep (1); } то результат уже не такой грустный :) - 1 th 101 2 th 102 1 1 th 103 2 th 104 2 1 th 105 2 th 106 3 1 th 107 2 th 108 4 1 th 109 2 th 110 5 1 th 111 2 th 112 6 1 th 113 2 th 114 7 1 th 115 2 th 116 8 1 th 117 2 th 118 9 1 th 119 2 th 120 10 1 th 121 2 th 122 11 1 th 123 2 th 124 12 Именно потому, что Sleep обеспечивает переключение на второй поток... Я более-менее доступно пояснил? Только вот учтите, что гарантии поочередного выполнения этим переносом Sleep() не получится - есть теоретическая вероятность, что оба потока уйдут в спячку одновременно... P.S. Убрал Sleep совсем, счетчик поднял до 1000 - получил до 123 первый поток, потом до 1122 второй, потом опять первый... Т.е. переключение есть - но именно такое, как я писал - по времени.

Ответ 2



Вся суть и назначение критической секции заключается в том, что захват свободной критической секции - исключительно легкая операция, выполняемая в контексте вызывающего процесса, без системных вызовов и переключения контекста. Попытка же захвата уже занятой критической секции - "тяжелая" операция, которая помещает поток в полноценное состояние системного ожидания - "спячки" (см. однако spin count ниже). По этой причине, если поток, владеющий критической секцией, делает LeaveCriticalSection и EnterCriticalSection без какой-либо паузы между ними, у ждущего ("спящего") потока нет никаких шансов успеть вовремя проснуться и тоже сделать попытку захвата секции. Уже владеющий секцией поток будет все время единолично успешно освобождать и перезахватывать ее. Именно это вы и наблюдаете в вашем эксперименте. Чтобы уравнять шансы потоков, вам надо Либо ввести какую-то задержку между LeaveCriticalSection и EnterCriticalSection, чтобы дать ждущему потоку время проснуться до выполнения EnterCriticalSection. Либо позаботиться о том, чтобы ждущий поток не уходил в состояние системной спячки, пока первый поток "держит" секцию. Этого можно достичь путем указания spin count для критической секции. Разумеется, для такой тяжелой итерации, как у вас, понадобится неразумно большой spin count. Тем не менее, ради эксперимента, в данном примере для облегчения итерации убираем Sleep и делаем InitializeCriticalSectionAndSpinCount(&cs, 10000000); Получаем уже 1 th 101 2 th 102 1 1 th 103 2 th 104 2 2 th 105 3 2 th 106 4 1 th 107 2 th 108 5 2 th 109 6 1 th 110 2 th 111 7 2 th 112 8 1 th 113 1 th 114 2 th 115 9 1 th 116 2 th 117 10 2 th 118 11 2 th 119 12 1 th 120 1 th 121 1 th 122 1 th 123 1 th 124 А если убрать такую "тяжесть", как printf и следить за потоками более эффективными способами, то сработает и существенно меньший spin count.

Комментариев нет:

Отправить комментарий