R0 CREW

Multithreading - everything they want you to know. Part 1 - Critical Sections

ВступлениеДобрый день, читатели. Искренне надеюсь, что вас больше одного. Рад приветствовать вас в начале нового цикла статей, посвящённых многопоточности и примитивам синхронизации.

Возможно, вам будет интересно, что сподвигло на написание этого материала. Что же, немного предыстории. Некоторые из вас знают, что реверс-инженерией я не занимаюсь уже почти год. Я перешёл на сторону software development, и сейчас занимаюсь разработкой высокопроизводительных клиент-серверных приложений на С++.

В ходе своего карьерного роста мне пришлось столкнуться с такой темой, как многопоточное программирование. Так уж устроены современные системы, что высокой производительности можно достичь лишь с помощью параллелизма.

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

Здесь я постараюсь доступно (насколько это возможно) и последовательно изложить всё, что вам требуется знать для написания эффективных многопоточных приложений. И сегодня мы начнём разговор с такого примитива синхронизации, как критическая секция.

Зачем нужны критические секции?

Очевидно, что критическая секция – это примитив синхронизации. Про неё (критическую секцию) написано немало, и нам предстоит разобраться, что из этого всего правда, а что – нет.

Используется она для защиты участка кода от одновременного выполнения несколькими потоками. То есть, захватив критическую секцию, поток начинает выполнять участок кода, в который другой поток уже не может попасть, потому что захватить ранее захваченную критическую секцию нельзя.

Наглядно работу критической секции демонстрирует вот эта иллюстрация.

Как работать с критическими секциями.

В идеале всё сводится к нескольким простым вызовам API – проинициализировать критическую секцию, захватить, освободить и удалить. Конечно же, с некоторыми ньюансами и оговорками.
Для иллюстрации есть пример на MSDN:

Рассмотрим часть этого примера:

DWORD WINAPI ThreadProc( LPVOID lpParameter )
{
    ...

    // Request ownership of the critical section.
    EnterCriticalSection(&CriticalSection); 

    // Access the shared resource.

    // Release ownership of the critical section.
    LeaveCriticalSection(&CriticalSection);

    ...
return 1;
}

Пример хороший, простой, понятный. Вот только в общем случае делать так нельзя!
Рассмотрим такой код:

int main(int argc, char** argv) 
{
    CRITICAL_SECTION CriticalSection;
    auto x = 10;
    InitializeCriticalSection(&CriticalSection);
    
    EnterCriticalSection(&CriticalSection);
    EnterCriticalSection(&CriticalSection);
    x += 55;
    LeaveCriticalSection(&CriticalSection);
    return x;
}

Здесь переменная x защищена критической секцией. Да что там - мы ДВАЖДЫ попытались захватить секцию. И что в итоге?

00402760 | 8D 4C 24 04              | lea ecx,dword ptr ss:[esp+4]                           |
00402764 | 83 E4 F0                 | and esp,FFFFFFF0                                       |
00402767 | FF 71 FC                 | push dword ptr ds:[ecx-4]                              |
0040276A | 55                       | push ebp                                               |
0040276B | 89 E5                    | mov ebp,esp                                            |
0040276D | 56                       | push esi                                               | 
0040276E | 53                       | push ebx                                               |
0040276F | 51                       | push ecx                                               |
00402770 | 8D 5D D0                 | lea ebx,dword ptr ss:[ebp-30]                          |
00402773 | 83 EC 3C                 | sub esp,3C                                             |
00402776 | E8 C5 EF FF FF           | call testcriticalsections.401740                       |
0040277B | 89 1C 24                 | mov dword ptr ss:[esp],ebx                             |
0040277E | FF 15 3C 71 40 00        | call dword ptr ds:[<&RtlInitializeCriticalSection>]    |
00402784 | 83 EC 04                 | sub esp,4                                              |
00402787 | 8B 35 10 71 40 00        | mov esi,dword ptr ds:[<&RtlEnterCriticalSection>]      | 
0040278D | 89 1C 24                 | mov dword ptr ss:[esp],ebx                             |
00402790 | FF D6                    | call esi                                               |
00402792 | 83 EC 04                 | sub esp,4                                              |
00402795 | 89 1C 24                 | mov dword ptr ss:[esp],ebx                             |
00402798 | FF D6                    | call esi                                               |
0040279A | 83 EC 04                 | sub esp,4                                              |
0040279D | 89 1C 24                 | mov dword ptr ss:[esp],ebx                             |
004027A0 | FF 15 40 71 40 00        | call dword ptr ds:[<&RtlLeaveCriticalSection>]         |
004027A6 | 83 EC 04                 | sub esp,4                                              |
004027A9 | 8D 65 F4                 | lea esp,dword ptr ss:[ebp-C]                           |
004027AC | B8 41 00 00 00           | mov eax,41                                             | //наш х в самом конце

Между захватом и освобождением критической секции нет работы с этой переменной вообще. Критическая секция её никак не защищает.

Это иногда называют ошибками “видимости памяти”. Суть в том, что оптимизатор переставляет инструкции местами. В данном случае он не нашёл никакой связи между вызовом API и модификацией переменной. Для борьбы с такими ошибками следует использовать идиому RAII. Но об этом чуть позже.

Как работает EnterCriticalSection

Задача EnterCriticalSection - захватить критическую секцию. Перед захватом
для критической секции должна быть выделена память, плюс должна быть выполнена инициализация – InitializeCriticalSection или аналогичная. Об инициализации мы поговорим чуть позже, сейчас же вернёмся к захвату секции.

Сам объект CRITICAL_SECTION (или правильнее сказать структура) выглядит так:

typedef struct _RTL_CRITICAL_SECTION {
      PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
      LONG LockCount;
      LONG RecursionCount;
      HANDLE OwningThread;
      HANDLE LockSemaphore;
      ULONG_PTR SpinCount;
    } RTL_CRITICAL_SECTION,*PRTL_CRITICAL_SECTION;

Аналогичное определение в winnt.h.

Теперь рассмотрим случай, когда в системе Windows 7 x86 выполняется захват критической секции. Эта секция была проинициализирована и никем не захвачена пока что.

00402786 | 89 1C 24                 | mov dword ptr ss:[esp],ebx                             | //указатель на структуру передаётся через стек
00402789 | FF 15 10 71 40 00        | call dword ptr ds:[<&RtlEnterCriticalSection>]         | //EnterCriticalSection имеет forward в ntdll

Просматривая состояние структуры RTL_CRITICAL_SECTION в памяти, видим следующее:

0022FE98  E0355200 FFFFFFFF 00000000 00000000  à5R.ÿÿÿÿ........  
0022FEA8  00000000 00000000 D0FE2200 01000000  ........Ðþ".....  

Первый шесть двойных слов является искомой структурой. Как видим, DebugInfo уже присутствует. RecursionCount равен -1 (0xFFFFFFFF). Остальное по нулям.

Теперь рассмотрим самую важную часть захвата:

770E731D | 8D 77 04                 | lea esi,dword ptr ds:[edi+4]                           | 
770E7320 | 8B C6                    | mov eax,esi                                            | // получаем адрес LockCount
770E7322 | F0 0F BA 30 00           | lock btr dword ptr ds:[eax],0

Вот эта конструкция крайне важна. Префикс lock делает эту операцию атомарной, т.е. непрерываемой.
btr dword ptr ds:[eax],0 считывает младший бит из LockCount в флаг CF. А на место этого бита записывается нулевой бит. Значение CF станет равным 1, потому что LockCount был 0xFFFFFFFF, а значит все его биты были установлены в 1. Новое значение LockCount - 0xFFFFFFFE.

770E7327 | 0F 83 EB E5 00 00        | jae ntdll.770F5918                                     |

Сравнивает значение CF с нулём. Если там оказался 0, критическая секция уже была захвачена кем-то ранее. Сейчас же там 1, что говорит о её полной свободе. Значит мы можем спокойно её захватывать. В таком случае переход не выполняется.

770E732D | 64 A1 18 00 00 00        | mov eax,dword ptr fs:[18]                              |
NtGetCurrentTeb  - ищется TEB текущего потока.

770E7333 | 8B 48 24                 | mov ecx,dword ptr ds:[eax+24]                          |
Получаем Id потока из TEB.

770E7336 | 89 4F 0C                 | mov dword ptr ds:[edi+C],ecx                           |
Сохраняем Id потока в OwningThread.

770E7339 | C7 47 08 01 00 00 00     | mov dword ptr ds:[edi+8],1                             |
Ставим 1 в RecursionCount.

Как видите, захват основывается на атомарной инструкции lock btr dword ptr ds:[eax],0.

Как работает LeaveCriticalSection

770E72D6 | 8B 75 08                 | mov esi,dword ptr ss:[ebp+8]                           |
Получаем указатель на RTL_CRITICAL_SECTION.
770E72D9 | 83 46 08 FF              | add dword ptr ds:[esi+8],FFFFFFFF                      |

К значению RecursionCount прибавляем -1. Помните, после захвата это значение было равным 1? После такого прибавления оно будет равно 0, что будет сигнализировать об успешном выходе из рекурсии. Какой такой рекурсии? Сейчас узнаем.

770E72DD | 75 23                    | jne ntdll.770E7302                                     |

У нас в нормальной ситуации этот переход не выполнится, и мы пойдём дальше.

770E72DF | 53                       | push ebx                                               |
770E72E0 | 57                       | push edi                                               |
770E72E1 | 8D 7E 04                 | lea edi,dword ptr ds:[esi+4]                           |

Получаем адрес поля LockCount.

770E72E4 | C7 46 0C 00 00 00 00     | mov dword ptr ds:[esi+C],0                             |

Стираем Id потока, который владел критической секцией. (Обнуляем OwningThread)

770E72EB | BB 01 00 00 00           | mov ebx,1                                              |
770E72F0 | 8B C7                    | mov eax,edi                                            |
770E72F2 | F0 0F C1 18              | lock xadd dword ptr ds:[eax],ebx                       |

В этих трёх интруцкиях к полю LockCount атомарно добавляется 1. Помните, после захвата там было значение 0xFFFFFFFE. После прибавления единицы будет 0xFFFFFFFF. Т.е. исходное значение, как до захвата. В ebx же запишется старое значение - 0xFFFFFFFE.

770E72F6 | 43                       | inc ebx                                                |
770E72F7 | 83 FB FF                 | cmp ebx,FFFFFFFF                                       |
770E72FA | 0F 85 9F AB FE FF        | jne ntdll.770D1E9F                                     |

Критическая секция может быть захвачена больше одного раза. Об этом говорит параметр LockCount.
Собственно, он и говорит, сколько раз она была захвачена. Этот код проверяет, освободили ли мы её столько раз, сколько захватили. Если это так, то переход не выполняется, и мы выходим из функции LeaveCriticalSection.

770E7300 | 5F                       | pop edi                                                |
770E7301 | 5B                       | pop ebx                                                |
770E7302 | 33 C0                    | xor eax,eax                                            |
770E7304 | 5E                       | pop esi                                                |
770E7305 | 5D                       | pop ebp                                                |
770E7306 | C2 04 00                 | ret 4                                                  |

Таким образом, мы выяснили, что пока для нас важными является 3 поля:

LONG LockCount; // сколько раз секция была захвачена
LONG RecursionCount; // сколько раз была захвачена рекурсивно
HANDLE OwningThread; // какому потоку принадлежит секция (кто первый захватил)

Рекурсивный захват критической секции

EnterCriticalSection может вызываться дважды из одного и того же потока для захвата одной и той же (уже захваченной в первый раз) критической секции. Повторный захват из одного и того же потока называется рекурсивным.

770E7320 | 8B C6                    | mov eax,esi                                            | 
770E7322 | F0 0F BA 30 00           | lock btr dword ptr ds:[eax],0                          |
770E7327 | 0F 83 EB E5 00 00        | jae ntdll.770F5918                                     |

CF в таком случае будет равен 0, и переход выполнится.

770F5918 | 64 8B 0D 18 00 00 00     | mov ecx,dword ptr fs:[18]                              |
770F591F | 8B 57 0C                 | mov edx,dword ptr ds:[edi+C]                           |
770F5922 | 3B 51 24                 | cmp edx,dword ptr ds:[ecx+24]                          |
770F5925 | 0F 85 E1 C3 FD FF        | jne ntdll.770D1D0C                                     |

Здесь текущий Id потока сравнивается с Id владельца. Так как это - один и тот же поток, то они равны. Переход не выполняется.

770F592B | FF 47 08                 | inc dword ptr ds:[edi+8]                               |

++RecursionCount

770F592E | 5F                       | pop edi                                                |
770F592F | 33 C0                    | xor eax,eax                                            |
770F5931 | 5E                       | pop esi                                                | 
770F5932 | 8B E5                    | mov esp,ebp                                            |
770F5934 | 5D                       | pop ebp                                                |
770F5935 | C2 04 00                 | ret 4      

Инициализация критической секции.

Есть несколько API для инициализации. Самыми старыми являются:

InitializeCriticalSection
InitializeCriticalSectionAndSpinCount

Эти две функции появились в XP. Есть более новая Функция:

InitializeCriticalSectionEx

Теперь две старые функции будут работать через неё. Давайте для начала рассмотрим самый простой случай - как работает InitializeCriticalSection.

0041FC66 | C7 04 24 34 10 43 00     | mov dword ptr ss:[esp],testcriticalsections.431034     |
0041FC6D | FF 15 28 22 43 00        | call dword ptr ds:[<&RtlInitializeCriticalSection>]    |

Вызывается её форвард RtlInitializeCriticalSection. Он принимает указатель на структуру CRITICAL_SECTION.

778B9FB6 | 8B FF                    | mov edi,edi                                            |
778B9FB8 | 55                       | push ebp                                               |
778B9FB9 | 8B EC                    | mov ebp,esp                                            |
778B9FBB | 8B 45 08                 | mov eax,dword ptr ss:[ebp+8]                           | 
778B9FBE | 6A 00                    | push 0                                                 |
778B9FC0 | 6A 00                    | push 0                                                 |
778B9FC2 | 50                       | push eax                                               |
778B9FC3 | E8 61 CC FF FF           | call <ntdll.RtlInitializeCriticalSectionEx>            |
778B9FC8 | 5D                       | pop ebp                                                |
778B9FC9 | C2 04 00                 | ret 4                                                  |

RtlInitializeCriticalSectionEx принимает 3 параметра, 2 последних (SpinCount и Flags) равны нулю, потому что мы не имеем возможности их задать, вызывая InitializeCriticalSection.

778B6C29 | 8B FF                    | mov edi,edi                                            |
778B6C2B | 55                       | push ebp                                               |
778B6C2C | 8B EC                    | mov ebp,esp                                            |
778B6C2E | 8B 45 10                 | mov eax,dword ptr ss:[ebp+10]                          |
778B6C31 | 83 EC 28                 | sub esp,28                                             |
778B6C34 | A9 00 00 00 F8           | test eax,F8000000                                      |
778B6C39 | 0F 85 25 B0 02 00        | jne ntdll.778E1C64                                     |
778B6C3F | 8B 4D 0C                 | mov ecx,dword ptr ss:[ebp+C]                           |
778B6C42 | F7 C1 00 00 00 FF        | test ecx,FF000000                                      |
778B6C48 | 0F 85 21 B0 02 00        | jne ntdll.778E1C6F                                     |
778B6C4E | A9 00 00 00 04           | test eax,4000000                                       |
778B6C53 | 0F 85 BE 00 00 00        | jne ntdll.778B6D17                                     |

Так мы заходим внутрь RtlInitializeCriticalSectionEx. [ebp+10] содержит значение Flags, а [ebp+C] - SpinCount. Как вы помните - они нулевые, потому ни один переход не выполнится.

778B6C59 | 64 8B 15 18 00 00 00     | mov edx,dword ptr fs:[18]                              | 
778B6C60 | 53                       | push ebx                                               |
778B6C61 | 56                       | push esi                                               | 
778B6C62 | 33 DB                    | xor ebx,ebx                                            |
778B6C64 | 57                       | push edi                                               |
778B6C65 | 8B 7D 08                 | mov edi,dword ptr ss:[ebp+8]                           |
778B6C68 | C7 47 04 FF FF FF FF     | mov dword ptr ds:[edi+4],FFFFFFFF                      |
778B6C6F | 89 5F 08                 | mov dword ptr ds:[edi+8],ebx                           |
778B6C72 | 89 5F 0C                 | mov dword ptr ds:[edi+C],ebx                           |
778B6C75 | 89 5F 10                 | mov dword ptr ds:[edi+10],ebx                          |
778B6C78 | 8B 52 30                 | mov edx,dword ptr ds:[edx+30]                          | 
778B6C7B | 83 7A 64 01              | cmp dword ptr ds:[edx+64],1                            | 
778B6C7F | 0F 86 9A 00 00 00        | jbe ntdll.778B6D1F                                     |

Все поля структуры CRITICAL_SECTION, кроме LockCount, обнуляются. В LockCount записывается 0xFFFFFFFF. Через PEB достаётся количество процессоров в системе и сравнивается с 1. Дело в том, что спинлок имеет смысл применять только на системах, которые аппаратно поддерживают реальное параллельное выполнение. На однопроцессорных/одноядерных системах это бессмысленно. Потому совершенно неважно, какой SpinCount вы зададите на такой системе - он принудительно будет выставлен в 1.

778B6C85 | A9 00 00 00 02           | test eax,2000000                                       |
778B6C8A | 0F 85 EA AF 02 00        | jne ntdll.778E1C7A                                     |

Что интересно, RtlInitializeCriticalSectionEx может ничего и не проинициализировать. Такое может произойти, если:

  1. Используется RTL_CRITICAL_SECTION_ALL_FLAG_BITS.
  2. Используется очень большое значение SpinLock (более 0xFF000000).

Напомню, что оба эти сценария невозможны при вызове InitializeCriticalSection. При вызове этой API критическая секция будет инициализирована в любом случае. Потому у неё и соответствующая сигнатура:

void WINAPI InitializeCriticalSection(
  _Out_ LPCRITICAL_SECTION lpCriticalSection
);

Против

BOOL WINAPI InitializeCriticalSectionAndSpinCount(
  _Out_ LPCRITICAL_SECTION lpCriticalSection,
  _In_  DWORD              dwSpinCount
);

или

BOOL WINAPI InitializeCriticalSectionEx(
  _Out_ LPCRITICAL_SECTION lpCriticalSection,
  _In_  DWORD              dwSpinCount,
  _In_  DWORD              Flags
);
778B6C90 | 81 E1 FF FF FF 00        | and ecx,FFFFFF                                         |
778B6C96 | 89 4F 14                 | mov dword ptr ds:[edi+14],ecx                          |
778B6C99 | 25 00 00 00 01           | and eax,1000000                                        |
778B6C9E | 89 45 10                 | mov dword ptr ss:[ebp+10],eax                          |
778B6CA1 | 0F 85 E2 AF 02 00        | jne ntdll.778E1C89                                     |
778B6CA7 | E8 80 00 00 00           | call <ntdll.RtlpAllocateDebugInfo>                     |

Значение SpinCount уменьшается максимум до 0xFFFFFF, и если не использовался Flags RTL_CRITICAL_SECTION_FLAG_NO_DEBUG_INFO, то выделяется буфер под DebugInfo. Память выделяется в Heap процесса, и если это по каким-то причинам невозможно, то в зависимости от параметров запуска процесса мы можем даже увидеть отладочные сообщения об этом. Точнее это могут быть сообщения
Трассировки
Но к нашей тематике это не относится. Потому идём дальше.

778B6CB6 | 89 37                    | mov dword ptr ds:[edi],esi                             |
778B6CB8 | 33 C0                    | xor eax,eax                                            |
778B6CBA | 6A 01                    | push 1                                                 |
778B6CBC | 66 89 06                 | mov word ptr ds:[esi],ax                               |
778B6CBF | 89 5E 14                 | mov dword ptr ds:[esi+14],ebx                          |
778B6CC2 | 89 5E 10                 | mov dword ptr ds:[esi+10],ebx                          |
778B6CC5 | 89 7E 04                 | mov dword ptr ds:[esi+4],edi                           |
778B6CC8 | 89 5E 18                 | mov dword ptr ds:[esi+18],ebx                          |
778B6CCB | E8 8A 00 00 00           | call <ntdll.RtlLogStackBackTraceEx>                    |

Здесь может производится логгирование. Это может быть полезно, если критические секции вдруг почему-то не будут работать, как надо. По дефолту оно выключено.

778B6CD7 | 68 18 81 93 77           | push <ntdll.RtlCriticalSectionLock>                    |
778B6CDC | 66 89 46 1C              | mov word ptr ds:[esi+1C],ax                            |
778B6CE0 | E8 2B 06 FF FF           | call <ntdll.RtlEnterCriticalSection>                   |
778B6CE5 | 8B 0D 64 85 93 77        | mov ecx,dword ptr ds:[77938564]                        | 
778B6CEB | 8D 46 08                 | lea eax,dword ptr ds:[esi+8]                           |
778B6CEE | C7 00 60 85 93 77        | mov dword ptr ds:[eax],<ntdll.RtlCriticalSectionList>  |
778B6CF4 | 89 48 04                 | mov dword ptr ds:[eax+4],ecx                           | 
778B6CF7 | 89 01                    | mov dword ptr ds:[ecx],eax                             | 
778B6CF9 | 68 18 81 93 77           | push <ntdll.RtlCriticalSectionLock>                    |
778B6CFE | A3 64 85 93 77           | mov dword ptr ds:[77938564],eax                        | 
778B6D03 | E8 C8 05 FF FF           | call <ntdll.RtlLeaveCriticalSection>                   |

Каждый процесс содержит список критических секций. Если вы инициализировали ещё одну, то она
добавляется в список.

Итак, подытожим.
[I]

  1. Для начала работы с критической секцией требуется инициализация.
  2. Инициализация может быть выполнена тремя Win API функциями:
  • InitializeCriticalSection
  • InitializeCriticalSectionAndSpinCount
  • InitializeCriticalSectionEx (Windows Vista и старше)
  1. Для InitializeCriticalSectionAndSpinCount и InitializeCriticalSectionEx требуется проверять
    возвращаемое значение, т.к. эти функции могут не выполниться успешно.
  2. Основная задача инициализации - записать дефолтные значения в структуру CRITICAL_SECTION
    и добавить информацию о критической секции в RtlCriticalSectionList.
  3. InitializeCriticalSectionEx может принимать недокументированные значения параметра Flags.
  4. Возможно логировать события инициализации.
  5. До инициализации использовать её не выйдет.[/I]

Захват критической секции, ранее захваченной другим потоком.

На текущий момент у нас есть захваченная секция, Id потока владельца отличается от Id текущего потока. Мы находимся здесь:

770B5918 | 64 8B 0D 18 00 00 00     | mov ecx,dword ptr fs:[18]                              |
770B591F | 8B 57 0C                 | mov edx,dword ptr ds:[edi+C]                           |
770B5922 | 3B 51 24                 | cmp edx,dword ptr ds:[ecx+24]                          |
770B5925 | 0F 85 E1 C3 FD FF        | jne ntdll.77091D0C                                     |

Переход выполняется.

77091D0C | 8B 47 14                 | mov eax,dword ptr ds:[edi+14]                          |
77091D0F | A9 00 00 00 04           | test eax,4000000                                       |
77091D14 | 0F 85 50 D2 FD FF        | jne ntdll.7706EF6A                                     |

Значение SpinCount меньше (в нашем случае), чем 0х4000000. Переход не выполняется.

77091D1A | C7 45 FC 01 00 00 00     | mov dword ptr ss:[ebp-4],1                             |
77091D21 | C7 45 F4 04 00 00 00     | mov dword ptr ss:[ebp-C],4                             |

Инициализируются некие локальные переменные.

77091CED | 8B 47 14                 | mov eax,dword ptr ds:[edi+14]                          |
77091CF0 | 8B D0                    | mov edx,eax                                            |
77091CF2 | 81 E2 FF FF FF 00        | and edx,FFFFFF                                         |
77091CF8 | 25 00 00 00 FF           | and eax,FF000000                                       |
77091CFD | 89 45 F8                 | mov dword ptr ss:[ebp-8],eax                           |

Значение SpinCount записывается в два регистра: eax и edx. В edx остаются 3 младших байта, а в eax кладётся старший. Значение старшего сохраняется рядом с другими локальными переменными - стек выглядит так:

0022FE7C  00000004  
0022FE80  00000000  //SpinCount == 0
0022FE84  00000001  
77091D00 | 85 D2                    | test edx,edx                                           |
77091D02 | 76 B3                    | jbe ntdll.77091CB7                                     |

SpinCount сравнивается с нулём, и т.к. они равны - переход выполняется.

77091CB7 | 8B 0E                    | mov ecx,dword ptr ds:[esi]                             |
77091CB9 | F6 C1 01                 | test cl,1                                              |
77091CBC | 0F 85 A2 01 00 00        | jne ntdll.77091E64                                     |

Обратите внимание - здесь снова проверяется значение LockCount. Но теперь чтение происходит не атомарно. Эта проверка нужна на тот случай, если за время работы со SpinCount другой поток успел освободить блокировку. Но если не успел, то переход не выполнится.

77091CC2 | F7 45 F8 00 00 00 02     | test dword ptr ss:[ebp-8],2000000                      |
77091CC9 | 0F 85 FD 07 FE FF        | jne ntdll.770724CC                                     |

Снова сравнение SpinCount c некоей константой. Они не равны, потому идём дальше.

77091CD2 | 52                       | push edx                                               |
77091CD3 | 57                       | push edi                                               |
77091CD4 | E8 58 00 00 00           | call <ntdll.RtlpWaitOnCriticalSection>                 |

Ожидание освобождения критической секции происходит внутри функции RtlpWaitOnCriticalSection. Ей передаются (первым параметром) адрес критической секции, и переменная

77091D21 | C7 45 F4 04 00 00 00     | mov dword ptr ss:[ebp-C],4

вот отсюда (равна 4)

Рассмотрим весь кусок в целом:

77091CB7 | 8B 0E                    | mov ecx,dword ptr ds:[esi]                             |
77091CB9 | F6 C1 01                 | test cl,1                                              |
77091CBC | 0F 85 A2 01 00 00        | jne ntdll.77091E64                                     |
77091CC2 | F7 45 F8 00 00 00 02     | test dword ptr ss:[ebp-8],2000000                      |
77091CC9 | 0F 85 FD 07 FE FF        | jne ntdll.770724CC                                     |
77091CCF | 8B 55 F4                 | mov edx,dword ptr ss:[ebp-C]                           |
77091CD2 | 52                       | push edx                                               |
77091CD3 | 57                       | push edi                                               |
77091CD4 | E8 58 00 00 00           | call <ntdll.RtlpWaitOnCriticalSection>                 |
77091CD9 | 83 F8 01                 | cmp eax,1                                              |
77091CDC | 74 28                    | je ntdll.77091D06                                      |
77091CDE | 83 F8 02                 | cmp eax,2                                              |
77091CE1 | 75 0A                    | jne ntdll.77091CED                                     |
77091CE3 | C7 45 FC 03 00 00 00     | mov dword ptr ss:[ebp-4],3                             |
77091CEA | 89 45 F4                 | mov dword ptr ss:[ebp-C],eax                           |
77091CED | 8B 47 14                 | mov eax,dword ptr ds:[edi+14]                          |
77091CF0 | 8B D0                    | mov edx,eax                                            |
77091CF2 | 81 E2 FF FF FF 00        | and edx,FFFFFF                                         |
77091CF8 | 25 00 00 00 FF           | and eax,FF000000                                       |
77091CFD | 89 45 F8                 | mov dword ptr ss:[ebp-8],eax                           |
77091D00 | 85 D2                    | test edx,edx                                           |
77091D02 | 76 B3                    | jbe ntdll.77091CB7                                     |

Если RtlpWaitOnCriticalSection возвращает не 1 и не 2, то две локальные переменные изменяются (одна равна 3, другая возвращённому значению RtlpWaitOnCriticalSection), а дальше опять то же самое. Более того, выйти отсюда нам поможет только возврат 1, т.к. возврат 2 просто позволит нам перепрыгнуть две инструкции:

77091CE3 | C7 45 FC 03 00 00 00     | mov dword ptr ss:[ebp-4],3                             |
77091CEA | 89 45 F4                 | mov dword ptr ss:[ebp-C],eax                           |

Что же происходит внутри RtlpWaitOnCriticalSection?

77091D31 | 8B FF                    | mov edi,edi                                            |
77091D33 | 55                       | push ebp                                               |
77091D34 | 8B EC                    | mov ebp,esp                                            |
77091D36 | 83 EC 44                 | sub esp,44                                             |
77091D39 | 64 8B 0D 18 00 00 00     | mov ecx,dword ptr fs:[18]                              |
77091D40 | 53                       | push ebx                                               |
77091D41 | 56                       | push esi                                               |
77091D42 | 8B 75 08                 | mov esi,dword ptr ss:[ebp+8]                           |
77091D45 | 33 C0                    | xor eax,eax                                            |
77091D47 | 81 FE 40 83 13 77        | cmp esi,<ntdll.LdrpLoaderLock>                         |
77091D4D | 0F 94 C0                 | sete al                                                |
77091D50 | 57                       | push edi                                               |
77091D51 | C7 45 F4 00 00 00 00     | mov dword ptr ss:[ebp-C],0                             |
77091D58 | 89 4D F8                 | mov dword ptr ss:[ebp-8],ecx                           |
77091D5B | 89 45 EC                 | mov dword ptr ss:[ebp-14],eax                          |
77091D5E | 85 C0                    | test eax,eax                                           |
77091D60 | 0F 85 DC 5D FF FF        | jne ntdll.77087B42                                     |
77091D66 | 80 3D A8 88 13 77 00     | cmp byte ptr ds:[771388A8],0                           |
77091D6D | 0F 85 CB DC FF FF        | jne ntdll.7708FA3E                                     |
77091D73 | 33 C0                    | xor eax,eax                                            |
77091D75 | 38 05 68 80 13 77        | cmp byte ptr ds:[<RtlpTimoutDisable>],al               |
77091D7B | 0F 95 C0                 | setne al                                               |
77091D7E | 48                       | dec eax                                                |
77091D7F | 25 18 83 13 77           | and eax,<ntdll.RtlpTimeout>                            |
77091D84 | 89 45 F0                 | mov dword ptr ss:[ebp-10],eax                          |
77091D87 | 8B 46 10                 | mov eax,dword ptr ds:[esi+10]                          |
77091D8A | 89 45 FC                 | mov dword ptr ss:[ebp-4],eax                           |
77091D8D | 85 C0                    | test eax,eax                                           |
77091D8F | 0F 84 97 B5 FF FF        | je ntdll.7708D32C                                      |

Это достаточно большой кусок кода. Кратко прокомментирую происходящее. Вначале проверяется, не работаем ли мы со специальной критической секцией LdrpLoaderLock. Проверяется также, включена ли опция мониторинга времени захвата. Про EnterCriticalSection:

Если специально не создавать этот параметр, то RtlpTimoutDisable будет равен 1 (т.е. true).

7708D32C | 56                       | push esi                                               |
7708D32D | E8 10 00 00 00           | call <ntdll.RtlpCreateCriticalSectionSem>              |

Далее создаётся некий семафор. Внутри этой функции видим следующее:

7708D342 | 8B FF                    | mov edi,edi                                            |
7708D344 | 55                       | push ebp                                               |
7708D345 | 8B EC                    | mov ebp,esp                                            |
7708D347 | 51                       | push ecx                                               |
7708D348 | 6A 00                    | push 0                                                 |
7708D34A | 6A 01                    | push 1                                                 |
7708D34C | 6A 00                    | push 0                                                 |
7708D34E | 68 03 00 10 00           | push 100003                                            |
7708D353 | 8D 45 FC                 | lea eax,dword ptr ss:[ebp-4]                           |
7708D356 | 50                       | push eax                                               |
7708D357 | E8 14 7E 01 00           | call <ntdll.ZwCreateEvent>                             |
7708D35C | 8B 55 08                 | mov edx,dword ptr ss:[ebp+8]                           |
7708D35F | 83 C2 10                 | add edx,10                                             |
7708D362 | 85 C0                    | test eax,eax                                           |
7708D364 | 0F 8C D2 D5 04 00        | jl ntdll.770DA93C                                      |
7708D36A | 8B 4D FC                 | mov ecx,dword ptr ss:[ebp-4]                           |
7708D36D | 33 C0                    | xor eax,eax                                            |
7708D36F | F0 0F B1 0A              | lock cmpxchg dword ptr ds:[edx],ecx                    |
7708D373 | 85 C0                    | test eax,eax                                           |
7708D375 | 0F 85 B0 D5 04 00        | jne ntdll.770DA92B                                     |
7708D37B | B0 01                    | mov al,1                                               |
7708D37D | 8B E5                    | mov esp,ebp                                            |
7708D37F | 5D                       | pop ebp                                                |
7708D380 | C2 04 00                 | ret 4                                                  |

Делается вызов ZwCreateEvent, дескриптор события атомарно через lock cmpxchg записывается в поле LockSemaphore. Если ранее какое-то событие уже было создано (объект-событие), то оно будет уничтожено вызовом NtClose:

770DA92B | 8B 45 FC                 | mov eax,dword ptr ss:[ebp-4]                           |
770DA92E | 50                       | push eax                                               |
770DA92F | E8 5C A7 FC FF           | call <ntdll.NtClose>                                   |
770DA934 | B0 01                    | mov al,1                                               |
770DA936 | 8B E5                    | mov esp,ebp                                            |
770DA938 | 5D                       | pop ebp                                                |
770DA939 | C2 04 00                 | ret 4                                                  |

Движемся дальше:

77091D95 | 8B 4E 04                 | mov ecx,dword ptr ds:[esi+4]                           |
77091D98 | 8D 7E 04                 | lea edi,dword ptr ds:[esi+4]                           |
77091D9B | F6 C1 01                 | test cl,1                                              |
77091D9E | 0F 85 AA DD FF FF        | jne ntdll.7708FB4E                                     |

Снова делается попытка проверить, не освободилась ли секция - проверяется параметр LockCount. Реализация пытается максимально всё проверить перед использованием объекта ядра (события). Событие будет применено только в случае крайней необходимости.

Далее пропущены не очень интересные моменты, и вот мы, наконец, ждём события:

77091DE3 | 57                       | push edi                                               |
77091DE4 | 6A 00                    | push 0                                                 |
77091DE6 | 83 F8 FF                 | cmp eax,FFFFFFFF                                       |
77091DE9 | 0F 84 A9 FD 04 00        | je ntdll.770E1B98                                      |
77091DEF | 50                       | push eax                                               |
77091DF0 | E8 EB 47 01 00           | call <ntdll.NtWaitForSingleObject>                     |

Если нам удалось дождаться, то NtWaitForSingleObject вернёт 0, и вся функция вернёт 2:

77091DF5 | 3D 02 01 00 00           | cmp eax,102                                            |
77091DFA | 0F 84 AA FD 04 00        | je ntdll.770E1BAA                                      |
77091E00 | 85 C0                    | test eax,eax                                           |
77091E02 | 0F 8C 3E FE 04 00        | jl ntdll.770E1C46                                      |
77091E08 | 83 7D EC 00              | cmp dword ptr ss:[ebp-14],0                            |
77091E0C | 0F 85 1E 5D FF FF        | jne ntdll.77087B30                                     |
77091E12 | 5F                       | pop edi                                                |
77091E13 | 5E                       | pop esi                                                | 
77091E14 | B8 02 00 00 00           | mov eax,2                                              |
77091E19 | 5B                       | pop ebx                                                |
77091E1A | 8B E5                    | mov esp,ebp                                            |
77091E1C | 5D                       | pop ebp                                                |
77091E1D | C2 08 00                 | ret 8                                                  |

Ещё немного ненужного пропущено, и мы здесь:

77091CB7 | 8B 0E                    | mov ecx,dword ptr ds:[esi]                             |
77091CB9 | F6 C1 01                 | test cl,1                                              |
77091CBC | 0F 85 A2 01 00 00        | jne ntdll.77091E64                                     |

Опять читается LockCount, сейчас он равен 0xFFFFFFFD. Напомню, что после захвата критической секции его значение меняется с 0xFFFFFFFF на 0xFFFFFFFE, а захват “семафора” уменьшает его ещё на 1. Переход выполняется, и через пару инструкций оказываемся здесь:

77091E22 | 33 45 FC                 | xor eax,dword ptr ss:[ebp-4]                           |

В eax 0xFFFFFFFD ксорится на 3. Таким образом, eax снова равен 0xFFFFFFFE. Далее это значение атомарно записывается в LockCount, и текущий поток становится владельцем критической секции.

Популярные вопросы и ответы на них

- Считаете ли вы критическую секцию самым простым синхронизационным примитивом?
- Нет, критическая секция является симбиозом сразу трёх механизмов синхронизации: атомарных операций, спинлока и объекта-события.

- Это правда, что критическая секция работает в режиме пользователя, а к ядру не обращается. Поэтому она и быстрее тех же мьютексов?
- Нет, критическая секция устроена так, что старается делать вызовы функций ядра только в том случае, когда без них не обойтись. Но в случае захвата разными потоками одной и той же секции ZwCreateEvent всё же будет вызван.

- Как критическая секция обрабатывает параметр SpinCount (счётчик спинлока) в однопроцессороной/одноядерной системе?
- Игнорирует его. Для таких систем спинлок не имеет смысла.

- Могут ли критические секции использоваться для синхронизации разных потоков, принадлежащих разным процессам?
- Нет, не могут. Критическая секция не видна за пределами адресного пространства процесса, в котором создана.

- А как же событие, которое создаётся в критической секции? Это же объект ядра – его должно быть “видно” из другого процесса.
- Событие создаётся с нулевым указателем POBJECT_ATTRIBUTES, потому не попадает в глобальную область видимости – его видно только локально в процессе, его создавшем.
[I]

  • Следует ли использовать идиому RAII при работе с критическими секциями и зачем, если следует?[/I]
    - Следует обязательно, она позволит освободить критическую секцию в случае генерации исключения и защитит от ошибок “видимости памяти”.

- Критические секции рекурсивны?
- Да, и других (нерекурсивных) не может быть.

В завершение

Надеюсь, материал был вам полезен. Пишите свои вопросы и пожелания в комментариях. До новых стреч – до следующей главы.

Ждем продолжения)

Спасибо за детальный разбор, пишите ещё

Справедливости ради стоит отметить, что примитива синхронизации критическая секция как такового не существует, то что в Microsoft называют обьектом критической секции является мьютексом, критическая секция это скорее участок программы требующий синхронизации

Не совсем :), но об этом ещё будет идти речь.

Это ещё почему? В правильной (классической) интерпритации терминов мьютекс это механизм взаимоисключающей блокировки где владельцем выступает поток(не обязательно поток ОС, мб ядром процессора и т.п.), то что Microsoft называет “критическая секция” правильно называть мьютексом. При этом критическая секция это регион программы который требует синхронизации (выполнения в одном потоке) в мультипоточной среде, поэтому не обязательно критической секцией называть синхронизированный участок кода, вполне допустимо называть крит. секцией несинхронизированный участок, который например привёл к рейс кондишену.