+ Reply to Thread
Results 1 to 5 of 5

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

  1. #1
    ARCHANGEL's Avatar

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

    Вступление

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

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

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

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

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

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

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

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

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


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

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

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

    PHP Code:
    DWORD WINAPI ThreadProcLPVOID lpParameter )
    {
        ...

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

        
    // Access the shared resource.

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

        ...
    return 
    1;

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

    PHP Code:
    int main(int argcchar** argv
    {
        
    CRITICAL_SECTION CriticalSection;
        
    auto x 10;
        
    InitializeCriticalSection(&CriticalSection);
        
        
    EnterCriticalSection(&CriticalSection);
        
    EnterCriticalSection(&CriticalSection);
        
    += 55;
        
    LeaveCriticalSection(&CriticalSection);
        return 
    x;

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

    PHP Code:
    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 (или правильнее сказать структура) выглядит так:

    PHP Code:
    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 выполняется захват критической секции. Эта секция была проинициализирована и никем не захвачена пока что.

    PHP Code:
    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 в памяти, видим следующее:

    PHP Code:
    0022FE98  E0355200 FFFFFFFF 00000000 00000000  à5R.ÿÿÿÿ........  
    0022FEA8  00000000 00000000 D0FE2200 01000000  ........Ðþ"..... 
    Первый шесть двойных слов является искомой структурой. Как видим, DebugInfo уже присутствует. RecursionCount равен -1 (0xFFFFFFFF). Остальное по нулям.

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

    PHP Code:
    770E731D 8D 77 04                 lea esi,dword ptr ds:[edi+4]                           | 
    770E7320 8B C6                    mov eax,esi                                            // получаем адрес LockCount 
    PHP Code:
    770E7322 F0 0F BA 30 00           lock btr dword ptr ds:[eax],
    Вот эта конструкция крайне важна. Префикс lock делает эту операцию атомарной, т.е. непрерываемой.
    btr dword ptr ds:[eax],0 считывает младший бит из LockCount в флаг CF. А на место этого бита записывается нулевой бит. Значение CF станет равным 1, потому что LockCount был 0xFFFFFFFF, а значит все его биты были установлены в 1. Новое значение LockCount - 0xFFFFFFFE.

    PHP Code:
    770E7327 0F 83 EB E5 00 00        jae ntdll.770F5918                                     
    Сравнивает значение CF с нулём. Если там оказался 0, критическая секция уже была захвачена кем-то ранее. Сейчас же там 1, что говорит о её полной свободе. Значит мы можем спокойно её захватывать. В таком случае переход не выполняется.

    PHP Code:
    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

    PHP Code:
    770E72D6 8B 75 08                 mov esi,dword ptr ss:[ebp+8]                           |
    Получаем указатель на RTL_CRITICAL_SECTION
    PHP Code:
    770E72D9 83 46 08 FF              add dword ptr ds:[esi+8],FFFFFFFF                      
    К значению RecursionCount прибавляем -1. Помните, после захвата это значение было равным 1? После такого прибавления оно будет равно 0, что будет сигнализировать об успешном выходе из рекурсии. Какой такой рекурсии? Сейчас узнаем.

    PHP Code:
    770E72DD 75 23                    jne ntdll.770E7302                                     
    У нас в нормальной ситуации этот переход не выполнится, и мы пойдём дальше.

    PHP Code:
    770E72DF 53                       push ebx                                               |
    770E72E0 57                       push edi                                               |
    770E72E1 8D 7E 04                 lea edi,dword ptr ds:[esi+4]                           | 
    Получаем адрес поля LockCount.

    PHP Code:
    770E72E4 C7 46 0C 00 00 00 00     mov dword ptr ds:[esi+C],0                             
    Стираем Id потока, который владел критической секцией. (Обнуляем OwningThread)

    PHP Code:
    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.

    PHP Code:
    770E72F6 43                       inc ebx                                                |
    770E72F7 83 FB FF                 cmp ebx,FFFFFFFF                                       |
    770E72FA 0F 85 9F AB FE FF        jne ntdll.770D1E9F                                     
    Критическая секция может быть захвачена больше одного раза. Об этом говорит параметр LockCount.
    Собственно, он и говорит, сколько раз она была захвачена. Этот код проверяет, освободили ли мы её столько раз, сколько захватили. Если это так, то переход не выполняется, и мы выходим из функции LeaveCriticalSection.

    PHP Code:
    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 может вызываться дважды из одного и того же потока для захвата одной и той же (уже захваченной в первый раз) критической секции. Повторный захват из одного и того же потока называется рекурсивным.

    PHP Code:
    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, и переход выполнится.

    PHP Code:
    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 владельца. Так как это - один и тот же поток, то они равны. Переход не выполняется.

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

    PHP Code:
    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 для инициализации. Самыми старыми являются:

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

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

    PHP Code:
    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.

    PHP Code:
    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.

    PHP Code:
    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. Как вы помните - они нулевые, потому ни один переход не выполнится.

    PHP Code:
    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.

    PHP Code:
    778B6C85 A9 00 00 00 02           test eax,2000000                                       |
    778B6C8A 0F 85 EA AF 02 00        jne ntdll.778E1C7A                                     
    Чтобы понять смысл работы с переменной Flags, следует изучить немного теории. В документации на MSDN описывается только одно значение этой переменной - CRITICAL_SECTION_NO_DEBUG_INFO. Если зададим его, то не будет создано поле DebugInfo в структуре CRITICAL_SECTION. Но на самом деле (недокументированных) значений больше.

    PHP Code:
    #define RTL_CRITICAL_SECTION_FLAG_NO_DEBUG_INFO 0x01000000
    #define RTL_CRITICAL_SECTION_FLAG_DYNAMIC_SPIN 0x02000000
    #define RTL_CRITICAL_SECTION_FLAG_STATIC_INIT 0x04000000
    #define RTL_CRITICAL_SECTION_FLAG_RESOURCE_TYPE 0x08000000
    #define RTL_CRITICAL_SECTION_FLAG_FORCE_DEBUG_INFO 0x10000000
    #define RTL_CRITICAL_SECTION_ALL_FLAG_BITS 0xff000000 
    Так вот инструкция test eax,2000000 как раз проверяет, а не задали ли мы значение
    RTL_CRITICAL_SECTION_FLAG_DYNAMIC_SPIN ? Если это так, то SpinCount будет задан независимо от нашего желания. И будет иметь некоторое предопределённое значение:

    PHP Code:
    778E1C7A C7 47 14 D0 07 00 02     mov dword ptr ds:[edi+14],20007D0 
    Почему 536872912 - не могу сказать.
    Что интересно, RtlInitializeCriticalSectionEx может ничего и не проинициализировать. Такое может произойти, если:
    1. Используется RTL_CRITICAL_SECTION_ALL_FLAG_BITS.
    2. Используется очень большое значение SpinLock (более 0xFF000000).

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

    PHP Code:
    void WINAPI InitializeCriticalSection(
      
    _Out_ LPCRITICAL_SECTION lpCriticalSection
    ); 
    Против
    PHP Code:
    BOOL WINAPI InitializeCriticalSectionAndSpinCount(
      
    _Out_ LPCRITICAL_SECTION lpCriticalSection,
      
    _In_  DWORD              dwSpinCount
    ); 
    или

    PHP Code:
    BOOL WINAPI InitializeCriticalSectionEx(
      
    _Out_ LPCRITICAL_SECTION lpCriticalSection,
      
    _In_  DWORD              dwSpinCount,
      
    _In_  DWORD              Flags
    ); 
    PHP Code:
    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 процесса, и если это по каким-то причинам невозможно, то в зависимости от параметров запуска процесса мы можем даже увидеть отладочные сообщения об этом. Точнее это могут быть сообщения
    Трассировки
    Но к нашей тематике это не относится. Потому идём дальше.

    PHP Code:
    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>                    | 
    Здесь может производится логгирование. Это может быть полезно, если критические секции вдруг почему-то не будут работать, как надо. По дефолту оно выключено.

    PHP Code:
    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>                   | 
    Каждый процесс содержит список критических секций. Если вы инициализировали ещё одну, то она
    добавляется в список.

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

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


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

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

    PHP Code:
    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                                     
    Переход выполняется.

    PHP Code:
    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. Переход не выполняется.

    PHP Code:
    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                             
    Инициализируются некие локальные переменные.

    PHP Code:
    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 кладётся старший. Значение старшего сохраняется рядом с другими локальными переменными - стек выглядит так:

    PHP Code:
    0022FE7C  00000004  
    0022FE80  00000000  
    //SpinCount == 0
    0022FE84  00000001 
    PHP Code:
    77091D00 85 D2                    test edx,edx                                           |
    77091D02 76 B3                    jbe ntdll.77091CB7                                     
    SpinCount сравнивается с нулём, и т.к. они равны - переход выполняется.

    PHP Code:
    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 другой поток успел освободить блокировку. Но если не успел, то переход не выполнится.

    PHP Code:
    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 некоей константой. Они не равны, потому идём дальше.

    PHP Code:
    77091CD2 52                       push edx                                               |
    77091CD3 57                       push edi                                               |
    77091CD4 E8 58 00 00 00           call <ntdll.RtlpWaitOnCriticalSection>                 | 
    Ожидание освобождения критической секции происходит внутри функции RtlpWaitOnCriticalSection. Ей передаются (первым параметром) адрес критической секции, и переменная
    PHP Code:
    77091D21 C7 45 F4 04 00 00 00     mov dword ptr ss:[ebp-C],
    вот отсюда (равна 4)

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

    PHP Code:
    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 просто позволит нам перепрыгнуть две инструкции:
    PHP Code:
    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?

    PHP Code:
    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:

    This function can raise EXCEPTION_POSSIBLE_DEADLOCK if a wait operation on the critical
    section times out. The timeout interval is specified by the following registry value:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Contro l\Session Manager\CriticalSectionTimeout.
    Do not handle a possible deadlock exception; instead, debug the application.
    Если специально не создавать этот параметр, то RtlpTimoutDisable будет равен 1 (т.е. true).

    PHP Code:
    7708D32C 56                       push esi                                               |
    7708D32D E8 10 00 00 00           call <ntdll.RtlpCreateCriticalSectionSem>              | 
    Далее создаётся некий семафор. Внутри этой функции видим следующее:

    PHP Code:
    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:
    PHP Code:
    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                                                  
    Движемся дальше:

    PHP Code:
    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. Реализация пытается максимально всё проверить перед использованием объекта ядра (события). Событие будет применено только в случае крайней необходимости.

    Далее пропущены не очень интересные моменты, и вот мы, наконец, ждём события:
    PHP Code:
    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:
    PHP Code:
    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                                                  
    Ещё немного ненужного пропущено, и мы здесь:
    PHP Code:
    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. Переход выполняется, и через пару инструкций оказываемся здесь:

    PHP Code:
    77091E22 33 45 FC                 | xor eax,dword ptr ss:[ebp-4]                           | 
    В eax 0xFFFFFFFD ксорится на 3. Таким образом, eax снова равен 0xFFFFFFFE. Далее это значение атомарно записывается в LockCount, и текущий поток становится владельцем критической секции.

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

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

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

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

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

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

    - Следует ли использовать идиому RAII при работе с критическими секциями и зачем, если следует?

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

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

    В завершение

    Надеюсь, материал был вам полезен. Пишите свои вопросы и пожелания в комментариях. До новых стреч – до следующей главы.
    Last edited by ARCHANGEL; 26-08-2017 at 22:56.
    Добрым быть просто - достаточно обратить свой гнев на негодяев...

  2. 7 пользователя(ей) сказали cпасибо:
    Darwin (27-08-2017) Fa1RLiGHT (01-09-2017) JKornev (27-08-2017) Patrick (29-08-2017) gavz (27-08-2017) korsader (27-08-2017) ximera (27-08-2017)
  3. #2
    Darwin's Avatar

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

    Ждем продолжения)
    Счастлив кто отдал, а не взял. (с) Inception

  4. #3
    JKornev's Avatar

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

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

    Справедливости ради стоит отметить, что примитива синхронизации критическая секция как такового не существует, то что в Microsoft называют обьектом критической секции является мьютексом, критическая секция это скорее участок программы требующий синхронизации
    Last edited by JKornev; 27-08-2017 at 17:49.
    High tech, low life

  5. #4
    ARCHANGEL's Avatar

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

    Не совсем :), но об этом ещё будет идти речь.
    Добрым быть просто - достаточно обратить свой гнев на негодяев...

  6. #5
    JKornev's Avatar

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

    Это ещё почему? В правильной (классической) интерпритации терминов мьютекс это механизм взаимоисключающей блокировки где владельцем выступает поток(не обязательно поток ОС, мб ядром процессора и т.п.), то что Microsoft называет "критическая секция" правильно называть мьютексом. При этом критическая секция это регион программы который требует синхронизации (выполнения в одном потоке) в мультипоточной среде, поэтому не обязательно критической секцией называть синхронизированный участок кода, вполне допустимо называть крит. секцией несинхронизированный участок, который например привёл к рейс кондишену.
    Last edited by JKornev; 28-08-2017 at 11:25.
    High tech, low life

+ Reply to Thread

Tags for this Thread

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
All times are GMT. The time now is 01:21
vBulletin® Copyright ©2000 - 2018
www.reverse4you.org