    СИНХРОНИЗАЦИЯ потоков 

    глава 10 

    В среде, позволяющей исполнять несколько потоков одновременно, очень важно 
синхронизировать их деятельность. Для этого в операционных системах, 
базирующихся на Win32, предусмотрен целый ряд синхронизирующих объектов. В 
данной главе мы рассмотрим пять таких объектов: критические секции, 
объекты-мьютексы, семафоры, события и ожидаемые таймеры. Но существуют и 
другие, часть из них описана в прочих главах книги. 
    Здесь Вы познакомитесь с самыми разными способами применения основных 
синхронизирующих объектов. Во многих случаях они ведут себя почти одинаково, 
но различия между ними все же есть, и зачастую именно эти различия диктуют 
выбор того или иного объекта для конкретной задачи. 
    Все перечисленные объекты, за исключением критических секций, принадлежат 
ядру. Таким образом, критические секции не управляются низкоуровневыми 
компонентами операционной системы, и в работе с ними описатели не 
используются. Критическая секция — простейший синхронизирующий объект, его-то 
мы и рассмотрим в первую очередь. Но сначала обсудим общую концепцию 
синхронизации потоков. 

    Несколько слов о синхронизации потоков 

    В общем случае поток синхронизирует себя с другим так: он засыпает, и 
операционная система, невыделяя ему_процессорного времени, приостанавливает 
его выполнение. Но прежде чем заснуть, поток сообщает системе, какое особое 
событие должно произойти, чтобы его исполнение возобновилось. Как только 
указанное событие произойдет, поток вновь получит право на выделение ему 
процессорного времени, и все пойдет своим чередом. Таким образом, отныне 
выполнение потока синхронизировано с определенным событием. 
    По ходу дела я покажу, как задается такое событие и как поток, уведомив 
систему о необходимости слежения за особым событием, «отправляется спать». 

    Худшее, что можно сделать 

    Если бы синхронизирующих объектов не было, а операционная система не умела 
отслеживать особые события, потоку пришлось бы самостоятельно синхронизировать 
себя с ними, применяя метод, который я как раз и собираюсь продемонстрировать. 
Но поскольку в операционную систему встроена поддержка синхронизации объектов, 
никогда не применяйте этот метод. 
    Суть его в том, что поток синхронизирует себя с завершением какой-либо 
задачи в другом потоке, постоянно просматривая значение переменной, доступной 
обоим потокам. Чем он плох? Рассмотрим пример: 

    218 ______________________________Windows для профессионалов 


    BOOL g_fFinishedCalculation = FALSE; 
    int WINAPI WinMainC...) { 
    CreateThread(..., RecalcFunc, ...); 
    
    // ждем завершения пересчет 
    while (!g_fFinishedCalculation) 
    
    DWORD WINAPI RecalcFunc(LPVOID IpvThreadParam) { 
    // выполняем пересчет 
    g_fFinishedCalculation = TRUE; 
    return(O); 
    } 

    Как видите, первичный поток (он исполняет функцию WinMain) при 
синхронизации по такому событию, как завершение функции RecalcFunc, никогда не 
впадает в спячку. Поэтому система по-прежнему выделяет ему процессорное время 
за счет других потоков, занимающихся чем-то более полезным. 
    Другая проблема, связанная с подобным методом опроса, в том, что Булева 
переменная g_fFimshedCalculation может не получить значения TRUE — например, 
если у первичного потока более высокий приоритет, чем у потока, выполняющего 
RecalcFunc. В этом случае система никогда не предоставит процессорное время 
потоку RecalcFunc, а он никогда не выполнит оператор, присваивающий значение 
TRUE переменной g_fFinishedCalculation. 
    Отсюда следует один вывод: потоки надо синхронизировать, отправляя их в 
«спячку», но ни в коем случае не заставляя их постоянно отслеживать какое-либо событие. 
    Критические секции 
    Критическая секция (critical section) — это небольшой участок кода, 
требующий монопольного доступа к каким-то общим данным. Среди синхронизирующих 
объектов критические секции наиболее просты, но применимы для синхронизации 
потоков лишь в пределах одного процесса. Они позволяют сделать так, чтобы 
единовременно только один поток получал доступ к определенному региону. 
Рассмотрим фрагмент кода: 
    int g_nlndex = 0; 
    const int MAX_TIMES = 1000; 
    DWORD g_dwTimes[MAX_TIMES]; 
    DWORD WINAPI FirstThread(LPVOID IpvThreadParm) { 
    BOOL fDone = FALSE; 

    __________________________Глава 10. Синхронизация потоков 219 


    while (!fDone) { 
    if (g_nlndex >= MAX_TIMES) { 
    fDone = TRUE; 
    } else { 
    g_dwTimes[g_nIndex] = GetTickCountO; 
    g_nlndex++; 
    i 
    return(O); 
    } 
    DWORD WINAPI SecondThread(LPVOID IpvThreadParni) { 
    BOOL fDone = FALSE; 
    while (!fDone) { 
    if (g_nlndex >= MAX_TIMES) { 
    fDone = TRUE; 
    } else { 
    g_nlndex++; 
    g_dwTimes[g_nIndex - 1] = GetTickCountO; 
    } 
    return(O); 
    } 

    Здесь предполагается, что функции обоих потоков дают одинаковый результат, 
хоть они и закодированы с небольшими различиями. Если бы исполнялась только 
функция FirstThread, она заполнила бы массив g_dwTimes набором чисел с 
возрастающими значениями. Это верно и в отношении SecondThread — если бы она 
тоже исполнялась независимо. В идеале обе функции — даже при одновременном 
выполнении — должны бы по-прежнему заполнять массив тем же набором чисел. Но в 
нашем коде возникает проблема: массив g_dw Times не будет заполнен, как надо, 
потому что функции обоих потоков одновременно обращаются к одним и тем же 
глобальным переменным. Вот как это может произойти. 
    Допустим, мы только что начали исполнение обоих потоков в системе с одним 
процессором. Первым включился в работу второй поток, т. е. SecondThread (что 
вполне вероятно), и только она успела увеличить счетчик g_nlndexp,o 1, как 
система вытеснила ее поток и перешла к исполнению FirstThread. Та заносит в 
g_dwTimes[l] показания системного времени, и процессор вновь переключается на 
исполнение второго потока. SecondThread теперь присваивает элементу 
g_dwTimes[l - 1] новые показания системного времени. Поскольку эта операция 
выполняется позже, новые показания, естественно, выше, чем записанные в 
элемент g_dwTimes[l] функцией FirstThread. Отметьте также, что сначала 
заполняется первый элемент массива и только потом нулевой. Таким образом, 
данные в массиве оказываются ошибочными. 
    Согласен, пример довольно надуманный, но, чтобы привести реалистичный, 
нужно минимум несколько страниц кода. Важно другое: теперь Вы легко 
представите, что может произойти в действительности. Возьмем все тот же пример 
с управлением связанным списком объектов. Если доступ к связанному списку не 
синхронизирован, один поток может добавить элемент в список в тот момент, 
когда другой поток пытается найти в нем определенный элемент. Ситуация станет 
еще более угрожающей, если оба потока одновременно добавят в список новые 
элементы. Так что, используя критические секции, можно и нужно координировать 
доступ потоков к структурам данных. 

    220 ________________________________Windows для профессионалов 


    Создание 

    Для этого сначала сформируем в своем процессе структуру данных 
CRITICAL_SECTION. Она должна быть глобальной, чтобы к ней могли обращаться 
разные потоки. Обычно критические секции представляют собой набор глобальных 
переменных. Хотя структура CRITICAL_SECTION и ее элементы определены в файле 
WINNT.H', считайте ее содержимое «черным ящиком». Win32-^yHKU,HH, управляющие 
критическими секциями, сами инициализируют и модифицируют элементы данной 
структуры. Вам же этого делать не следует. 
    Добавив к предыдущему примеру критические секции, мы получим что-то вроде 
такого кода: 

    int g_nlndex = 0; 
    const int MAX_TIMES = 1000; 
    DWORD g__dwTimes[MAX_TIMES]; 
    CRITICAL_SECTION g_Critical3ection: 
    
    int WINAPI WinMain(...) { 
    HANDLE hThreads[2]; 
    
    // инициализируем критическую секцию до создания потоков, 
    // чтобы она была до того, как потоки начнут исполнение 
    InitializeCriticalSection(&g_CriticalSection); 
    hThreads[0] = CreateThread(..., FirstThread, ...); 
    nThreads[1] = CreateThread(..., SecondThread,    )• 
    // ждем, когда завершатся оба потока; 
    
    // следующую строку я поясню чуть позже 
    WaitForMultipleOb]ects(2, hThreads, TRUE, INFINITE); 
    
    // закрываем описатели потоков 
    CloseHandle(hThreads[0]); 
    CloseHandie(hThreads[1]); 
    
    // удаляем критическую секцию 
    DeleteCriticalSection(&g_CriticalSection); 
    } 
    DWORD WINAPI FirstThread(LPVOID IpvThread Farm) { BOOL fDone = FALSE; 
    while (!fDone) { 
    EnterCriticalSection(&g_CriticalSection); 
    if (g_nlndex >= MAX_TIMES) { 
    fDone = TRUE: 
    } else { 
    g_dwTimes[g_nIndex] = GetTickGount(); 
    g_nlndex++: 

    1. Сама структура CRITICAL_SECTION — под именем RTL_CRITICAL_SECTION — 
находится в файле WINBASE.H, а в файле WINNT.H определяется тип структуры RTL_CRITICAL_SECTION. 

    __________________________Глава 10. Синхронизация потоков 221 



    LeaveCriticalSection(&g_CriticalSection); 
    } 
    return(O); 
    } 
    
    DWORD WINAPI SecondThread(LPVOID IpvThreadParm) { 
    BOOL fDone = FALSE: 
    
    while (!fDone) { 
    EnterCriticalSection(&g_CriticalSection); 
    
    if (g_nlndex >= MAX_TIMES) { 
    fDone = TRUE; 
    } else { 
    g_nlndex++; 
    g_dwTlmes[g_nIndex - 1] = GetTickCount(); 
    
    } 
    LeaveCriticalSection(&g_CriticalSection); 
    }  " 
    return(O); 


    Применение 

    Прежде чем синхронизировать потоки с помощью критической секции, нужно 
инициализировать ее вызовом InitializeCriticalSection, передав адрес структуры 
CRITICAL_SECTION в параметре IpCriticalSection: 

    VOID InitializeCriticalSection(LPCRITICAL_SECTION IpCriticalSection); 

    Эта функция обязательно вызывается перед обращением к 
EnterCriticalSection. В приведенном выше коде показана критическая секция, 
инициализируемая в WinMain. В функциях обоих потоков предполагается, что 
переменная-структура g_CriticalSection будет инициализирована функцией 
InitializeCriticalSection до того, как они начнут свое исполнение. 
    Предположим, SecondThread исполняется первой. Она вызывает 
EnterCriticalSection, передавая ей адрес переменной-структуры g_CriticalSection: 

    VOID Enter-Cr-iticalSection(LPCRITICAL_SECTION IpCriticalSection): 

    Обнаружив, что для переменной g_Critica1Section она вызвана впервые, эта 
функция изменяет некоторые элементы в структуре данных и разрешает выполнение 
строки g_nlndex++. Допустим, после этой строки второй поток вытесняется 
первым, и начинается исполнение функции FirstThread. Она вызывает 
EnterCriticalSection, передавая ей адрес того же объекта, что и SecondThread. 
На этот раз EnterCriticalSection видит, что переменная-структура 
g_CriticalSection уже используется, и приостанавливает исполнение первого 
потока. Остаток процессорного времени передается другому потоку, и 
операционная система больше не выделяет квантов времени первому потоку, пока 
он спит. 
    Когда второй поток получит следующий квант времени, функция SecondThread 
исполнит оператор: 

    g_dwTimes[g_n!ndex - 1] = GetTickCount(); 

    в результате чего элементу g_dwTimes[0] присваивается текущее системное 
время. Не правда ли, события развиваются совсем иначе, чем в первый раз? Тогда 
получалось, что у элемента g_dwTimes[l] значение меньше, чем у g_dwTimes[0]. В 
этот момент, даже если система решит вытеснить второй поток, она не 
предоставит процессорного времени первому потоку, пото

    222 _______________________________Windows для профессионалов 

    му что он ждет, когда станет доступной критическая секция. Рано или поздно 
второй поток вновь получит квант времени и исполнит оператор: 

    LeaveCriticalSection(&g_CriticalSection); 

    После исполнения этой строки переменная g_CriticalSection будет указывать, 
что защита с общих структур данных снята и они доступны любому другому потоку. 
Первый поток, ожидавший этого события, вновь активизируется, вызовет 
EnterCriticalSection, и та закрепит за ним переменную g_Critica1Section. Когда 
EnterCriticalSection вернет управление, FirstThread продолжит работу. 
    Как видите, применение критических секций позволяет организовать 
поочередное обращение двух потоков к каким-то общим данным. Но в некоторых 
случаях существует вероятность одновременного обращения к общим данным и 
большего числа потоков. Тогда каждый из них должен предварительно вызвать 
EnterCriticalSection. Если один поток уже захватил критическую секцию, то 
ожидающие доступа к этой секции засыпают. Когда поток освободит критическую 
секцию, вызвав LeaveCriticalSection, система «разбудит» только одного 
ожидающего, отдав ему права на эту секцию. Другие спящие потоки останутся в 
том же состоянии. 
    Обратите внимание: допустимо — и даже полезно — когда один поток 
захватывает критическую секцию несколько раз подряд. Это может произойти, так 
как вызов EnterCriticalSection из потока, владеющего критической секцией, 
приводит к приращению счетчика ссылок (reference count). Прежде чем другой 
поток сможет захватить критическую секцию, поток, владеющий секцией в данное 
время, должен столько раз вызвать LeaveCriticalSection, сколько нужно для 
обнуления счетчика ссылок. Посмотрим, как все это работает: 

    int g_nNums[100]; 
    CRITICAL_SECTION g_CriticalSection: 
    
    DWORD WINAPI ThreadCLPVOID IpvParam) { 
    int nindex = (int) IpvParam: 
    
    EnterCriticalSection(&g_CriticalSection): 
    if (g_nNums[nIndex] < MIN_VAL) 
    IncrementNuiii(nlndex); 
    else 
    g_nNums[nIndex] = MIN_VAL: 
    LeaveCriticalSection(&g_CriticalSection); 
    return(O); 
    } 
    void IncrementNum(int nindex) { 
    EnterCnticalSection(&g_CriticalSection); 
    g_nNums[nIndex]++: 
    LeaveCriticalSection(&g_CriticalSection); 
    } 

    В этом фрагменте функция Thread занимает критическую секцию с самого 
начала своего исполнения. Поэтому она получает возможность корректно проверять 
значение элемента g_nNums [nindex], так как другой поток не сможет изменить 
его в процессе проверки. Если это значение оказывается меньше MIN_VAL, 
вызывается IncrementNum. 
    IncrementNum — функция независимая. Она работает, не зная, какая именно 
функция ее вызвала. Собираясь изменить значения элементов массива g_nNums, она запрашивает 

    _________________________Глава 10. Синхронизация потоков 223 

    разрешение на доступ к массиву, обращаясь к EnterCriticalSection. 
Поскольку IncrementNum исполняется потоком, уже занявшим критическую секцию, 
EnterCriticalSection просто увеличивает счетчик ссылок на эту секцию и дает 
потоку возможность продолжить исполнение. Если бы IncrementNum была вызвана из 
другого потока, то — обратись она к EnterCriticalSecйои^^^Ъсполнение ее потока 
приостановилось бы вплоть до вызова LeaveCriticalSection из потока, 
выполняющего функцию Thread. 
    Если в Вашей программе есть несвязанные структуры данных, переменные типа 
CRITICAL_SECTION нужно создавать для каждой структуры. В этом случае программа 
должна сначала вызвать InitTalizeCriticalSection — по разу для каждой 
переменной CRITICAL_SECTION. В дальнейшем потоки должны также вызывать 
EnterCriticalSection, передавая ей адрес переменной CRITICAL_SECTION — той, 
что относится к нужной структуре данных. Подчеркну: не создавайте критическую 
секцию, охватывающую более одной независимой структуры данных, иначе 
получится, как здесь: 

    int g_nNums[100]; 
    char g_cChars[100]; 
    CRITICAL_SECTION g_CriticalSection; 
    
    DWORD WINAPI ThreadFunc(LPVOID IpvParam) { 
    int x; 
    EnterCriticalSection(&g_CriticalSection); 
    for (x = 0; x < 100; x++) { 
    g_nNums[x] = 0: 
    g_cChars[x] = 'X'; 
    \ 
    LeaveCrlticalSection(&g_CriticalSection): 
    return(O): 

    Созданная здесь единственная критическая секция защищает оба массива — 
g_nNums и g_cChars — в период их инициализации. Но эти массивы совершенно 
различны. И при исполнении данного цикла ни один из потоков не получит доступ 
ни к одному массиву. Теперь посмотрим, что будет, если ThreadFunc реализовать так: 
    
    DWORD WINAPI ThreadFuncd-PVOID IpvParam) { 
    int x; 
    Eni-erCrlticalSRCtlon(&g_CriticalSect1o'i): 
    
    for (x = 0; x < 100; x++) 
    g_nNums[x] =0; 
    
    for (x = 0; x < 100; x++) 
    g_cChars[x] = 'X'; 
    LeaveCriticalSection(&g_Criticat Sect ion); 
    return(O); 


    224 ______________________________Windows для профессионалов 

    В этом фрагменте массивы инициализируются по отдельности, и теоретически 
после инициализации g_nNums посторонний поток, которому нужен доступ только к 
первому массиву, сможет начать исполнение — пока ThreadFunc занимается вторым 
массивом. Увы, это невозможно: обе структуры данных защищены одной критической 
секцией. Чтобы выйти из затруднения, создадим две критические секции: 

    int g_nNurns[100]: 
    char g_cChars[100]; 
    Л CRITICAL_SECTION g_CriticalSectionForNums; 
    0 CRITICAL_SECTION g_CriticalSectionForChars: 
    
    DWORD WINAPI ThreadFunc(LPVOID IpvParam) { 
    int x; 
    
    EiterCritica1Sec+ion(&r5_Criti('alSectionForNunis): 
    
    for (x = 0; x < 100; x++) 
    g_nNums[x] = 0; 
    LeaveCriticalSection(&g_CriticalSectionForNums); 
    EnterCriticalSection(&g_CriticalSectionForChars): 
    
    for (x = 0; x < 100; x++) 
    g_cChars[x] = 'X'; 
    LeaveCr11:1ca1 Section(&g_Crit!calSection For Chars); 
    return(O); 
    } 

    Теперь другой поток сможет работать с массивом g_nNums, как только 
ThreadFunc закончит его инициализацию. 
    Иногда нужен одновременный доступ сразу к двум структурам данных. Тогда 
ThreadFunc следует реализовать так: 
    
    DWORD WINAPI ThreadFuncCLPVOID IpvParam) { 
    int x; 
    EnterCriticalSection(&g_CriticalSectionForNuins); 
    EnterCriticalSection(&g_CriticalSectionForChars); 
    
    for (x = 0; x < 100; x++) 
    g_nNums[x] = 0; 
    
    for (x = 0; x < 100; x++) 
    g_cChars[x] = 'X'; 
    
    LeaveCrlticalSection(&g_CriticalSectionForChars); 
    Lea veC r it ica 1 Sect 1 on (&g_Cr it leal Sect ion For Mums); 
    return(O); 
    } 

    Предположим, доступ к обоим массивам требуется и другому потоку в данном 
процессе; при этом его функция написана так: 

    ________________________Глава 10. Синхронизация потоков 225 
    

    DWORD WINAP1 OnierFnreadPuncd-PVOlD IpvParani) { 
    int x: 
    EnterCritlcalSection(&g_CriticalSectionForClidi a); 
    EnterCriticalSection(&g_CriticalSectionForNu(iis); 
    
    for (x = 0: x < 100; x++) 
    g_nNums[x] = 0; 
    
    for (x = 0; x < 100: x++) 
    g_cChars[x] = 'X'; 
    LeaveCriticalSection(&g_CriticalSectionForNums); 
    LeaveCriticalSection(&g_CriticalSectionForChars); 
    return(O); 
    } 

    Я лишь поменял порядок вызовов EnterCriticalSection и 
beaveCriticalSection. Но из-за того, что функции ThreadFunc и OtherThreadFunc 
написаны именно так, существует вероятность взаимной блокировки (deadlock). 
Иначе говоря, поток никогда не приступит к исполнению — ведь ожидаемый им 
ресурс (критические секции в данном примере) никогда не будет доступен. 
    Допустим, ThreadFunc начинает исполнение и занимает критическую секцию 
g_CriticalSectionForNums. Получив от системы процессорное время, поток с 
функцией OtherThreadFunc захватывает критическую секцию 
g_CriticalSectionForChars. Тут-то и происходит взаимная блокировка потоков. 
Какая бы из функций — ThreadFunc или OtherThreadFunc — ни пыталась продолжить 
исполнение, она не сумеет занять другую, необходимую ей критическую секцию. 
    В предыдущем примере эту ситуацию легко исправить, написав код обеих 
функций так, чтобы они вызывали EnterCriticalSection в одинаковом порядке. 
Тогда потоки не будут блокировать друг другу доступ к нужному ресурсу. 
    А теперь рассмотрим прием, позволяющий свести к минимуму время пребывания 
программы в критической секции. Например, следующий код не дает другому потоку 
изменять значение в элементе g_nNums[3] до тех пор, пока в окно не будет 
отправлено сообщение WM_SOMEMSG: 

    int g_nNuiiis[100]; 
    CRITICAL_SECTION g_CriticalSection: 
    
    DWORD WINAP1 SomeThr-eadCLPVOID IpvParam) { 
    Ente rC r ill ca 1 Sect i о n(&g_C Fit-Leal Sect ion); 
    
    // посылаем в окно какое-нибудь сообщение 
    SendMessage(hwndSomeWnd, WM_SOMEMSG, g_nNums[3], 0); 
    LeaveCr-iticalSection(&g_CriticalSection); 
    return(O): 
    } 

    Трудно сказать, сколько времени уйдет на обработку WM_SOMEMSG оконной 
процедурой — может, несколько микросекунд, а может, и несколько лет2. В 
течение этого времени никакой другой поток не получит доступ к массиву 
g_nNums. Поэтому лучше составить код иначе: 
    2. Оконная процедура все-таки пишется поэффективнее, чем я тут 
предположил, — по крайней мере, обычно она требует не более 2—3 секунд. 

    226 ______________________________Windows для профессионалов 

    int g_nNu(ns[100]; 
    CRITICAL_SECTION g_CriticalSection; 
    
    DWORD WINAPI SomeThread(LPVOID IpvParam) { 
    int nTemp: 
    
    EnterCriticalSection(&g_CriticalSection); 
    nTemp = g_nNums[3]; 
    LeaveCriticalSection(&g_CriticalSection); 
    
    // посылаем в окно какое-нибудь сообщение 
    SendMessage(hwndSomeWnd, WM_SOMEMSG, nTemp, О): 
    return(O); 

    Этот код сохраняет значение элемента g_nNums[3] во временной переменной 
nTemp целого типа. Нетрудно догадаться, что на исполнение этой строки уходит 
всего несколько тактов процессора. Далее программа сразу вызывает 
LeaveCriticalSection — ведь защищать массив больше не нужно. Так что вторая 
версия программы намного лучше первой, поскольку другие потоки 
приостанавливаются лишь на время, необходимое для копирования значения из 
элемента массива во временную переменную. 
    По завершении программы все переменные типа CRITICAL_SECTION нужно удалить 
вызовом DeleteCriticalSection: 

    VOID OeleteCriticalSection(LPCRITICAL_SECTION IpCrlticalSection); 

    Она освобождает все ресурсы, включенные в критическую секцию. И, 
естественно, функции EnterCriticalSection и LeaveCriticalSection нельзя 
вызывать, используя удаленную переменную типа CRITICAL_SECTION, пока ее снова 
не инициализируют функцией InitializeCriticalSection. Кроме того, нельзя 
допускать удаления критической секции в тот момент, когда какой-либо поток 
вызвал для нее EnterCriticalSection. 
    В Windows NT 4 появилась новая функция, связанная с критическими секциями: 

    BOOL TryEnterCriticalSection(LPCRITICAL_SECTION IpCriticalSection): 

    Если при вызове TryEnterCriticalSection указанная критическая секция не 
занята никаким потоком, включая вызывающий, эта функция передает права на 
критическую секцию вызвавшему потоку и возвращает TRUE. Если же критическая 
секция уже занята, функция немедленно возвращает FALSE. Различие между 
EnterCriticalSection и TryEnterCriticalSection в том, что последняя функция не 
приостанавливает выполнение потока. 
    VwiNDowsy В Windows 95 TryEnterCriticalSection не реализована. Ваша 
программа не будет \95/ работать в Windows 95, если использует эту функцию. 

    Программа-пример CritSecs 

    Программа CritSecs (CRITSECS. EXE) (см. листинг на рис. 10-1) 
демонстрирует значимость критических секций в многопоточных приложениях. Файлы 
исходного кода и ресурсов этой программы находятся в каталоге CRITSECS на 
компакт-диске, прилагаемом к книге. 
    После запуска программы WinMain активизирует модальное диалоговое окно, 
служащее интерфейсом этой программы. Когда функция диалогового окна получает 
сообщение WN_INITDIALOG, функция Dlg_OnInitDialog инициализирует глобальную 
структуру CRITICAL_SECTION и все элементы управления в диалоговом окне, а 
также создает два потока: 
    Counter Thread и DisplayThread. С этого момента в процессе начинают 
исполняться три пото

    ________________________Глава 10. Синхронизация потоков 227 

    ка: первичный (обрабатывающий данные, вводимые в диалоговое окно и его 
элементы управления), CounterThread и Display Thread. 
    Где-то в начале программы CRITSECS.C появляется переменная: 
    
    // данные, подлежащие защите 
    TCHAR g_szNumber[10] = __ТЕХТ("0"); 

    Это символьный массив, инициализируемый строкой, содержащей цифру 0. Поток 
CounterThread преобразует ее в целое число, прибавляет 1 и, преобразовав в 
строку, вновь заносит в символьный массив. Поток Display Thread считывает это 
число из массива g_szNumber и добавляет его в окно списка, расположенное в 
диалоговом окне. 
    Запустив программу CritSecs, Вы увидите, что список заполняется числами, и 
через некоторое время диалоговое окно Critical Section Test Application 
становится таким: 

    III 1111111111 1111 11111111111111———^^И^^^^И 1 1 II 

    Вероятно, Вы заметили, что числа не располагаются в списке по порядку, 
потому что по умолчанию программа не синхронизирует доступ потоков к массиву 
g_szNumber. В то время как~пбток CounterThread преобразует элемент массива в 
число, увеличивает его значение и копирует обратно в массив, поток 
DisplayThread читает содержимое массива g_szNumber и копирует его в окно списка. 
    Теперь — чтобы почувствовать разницу — активизируйте флажок Synchronize 
(синхронизировать). Программа сразу же задействует переменную 
g_CriticalSection, позволяющую ограничить доступ к массиву g_szNumber. В итоге 
числа выводятся на экран строго по возрастанию: 

    228 ______________________________Windows для профессионалов 

    Я добавил и другие функциональные блоки в программу CritSecs. 
Комбинированные списки Process Priority Class (класс приоритета процесса). 
Display Thread Priority (приоритет потока вывода) и Counter Thread Priority 
(приоритет потока-счетчика) дают возможность варьировать класс приоритета всей 
программы CritSecs, равно как и относительные приоритеты двух потоков, 
исполняющих функции CounterThread и Display Thread. 
    Флажок Pause (пауза) демонстрирует, как приостанавливается и 
возобновляется выполнение потоков CounterThread и Display Thread вызовом 
функций SuspendThread и ResumeThread. 
    Флажок Show Counter Thread (показать поток-счетчик) заставляет 
CounterThread по окончании каждой итерации выводить в список следующую строку: 

    Cntr: Increment 

    При активизации этого флажка список выглядит так: 
    При этом итерация цикла в DisplayThread осуществляется быстрее, чем в 
CounterThread. Иногда DisplayThread за одну итерацию цикла в CounterThread 
успевает выполнить две итерации своего цикла. Это лишний раз доказывает, как 
важно предвидеть поведение операционной системы при распределении 
процессорного времени между потоками. Варьируя относительные приоритеты двух 
потоков, можно изменить порядок и частоту выделения им квантов времени. 
    Программа CritSecs может дать разные результаты в зависимости от 
определенных обстоятельств и характеристик Вашего компьютера: 
    • количества процессоров в системе (если Вы работаете в Windows NT); 
    • быстродействия Вашей системы; 
    • количества потоков, созданных другими, одновременно выполняемыми процессами; 
    • класса приоритета выполняемых процессов; 
    • относительного приоритета потоков, выполняемых в этих процессах. 
    Стоит упомянуть и еще о двух особенностях программы CritSecs. Во-первых, 
CounterThread, записав число обратно в массив g_szNumber, вызывает 
\Ущ32-функцию Sleep. Вот ее прототип: 

    VOID SIeep(DWORD cMilliseconds); 

    Обращаясь к функции Sleep, поток сообщает системе, что в течение 
стольких-то миллисекунд (задается параметром cMilliseconds) процессорное время 
ему не понадобится. В дан

    __________________________Глава 10. Синхронизация потоков 229 

    ном случае Counter Thread передает в параметре cMilliseconds нуль, т. е. 
говорит системе, что в следующие 0 миллисекунд процессорное время ему не 
нужно. Это может показаться странным, но один побочный эффект от такого вызова 
Sleep все же есть. В последнем случае система_отбирает у потока 
неиспользованный остаток текущего кванта времени. Я поместил вызов Sleep (0) в 
CounterThread, просто желая усилить эффект, возникающий при отсутствии 
синхронизации потоков в CritSecs. Без этого вызова CritSecs — если потоки не 
синхронизируются — по-прежнему давала бы некорректные результаты, но проблемы 
не проявлялись бы так ярко. 
    Во-вторых, хочу обратить Ваше внимание на то, как именно включается и 
выключается синхронизация потоков в этой программе. В начале циклов обеих 
функций — и CounterThread, и DisplayThread — Вы увидите строку: 

    fSyncChecked = IsDlgButtonChecked(g_hwnd, IDC_SYNCHRONIZE); 

    Эта строка позволяет выяснить и запомнить состояние флажка Synchronize в 
начале каждой итерации цикла. Далее в цикле присутствует такой код: 

    if (fSyncChecked) { 
    EnterCriticalSection(&g_CriticalSection): 
    } 
    if (fSyncChecked) { 
    LeaveCriticalSection(&g_CritlcalSection); 
    } 

    Когда я впервые писал программу CritSecs, он выглядел иначе: 

    if (Is01gButtonChecked(g_hwnd, IDC_SYNCHRONIZE)) { 
    EnterCriticalSection(&g_CritlcalSection); 
    i 
    
    if (IsDlgButtonChecked(g_hwnd, IDC_SYNCHRONIZE)) { 
    LeaveGriticalSection(&g_CriticalSection); 
    } 

    В последнем фрагменте — один маленький «жучок», из-за которого кое-какие 
потоки программы иногда зависали. В чем тут дело? В начале цикла CounterThread 
выясняла, что флажок активен, и поэтому вызывала EnterCriticalSection. Затем 
DisplayThread, видя, что флажок активен, тоже вызывала EnterCriticalSection. 
Но система — после вызова вторым потоком EnterCriticalSection — не возвращала 
ему управление, так как первый поток уже занял критическую секцию. Дальше — 
больше. Пока CounterThread считывала число, я мог сбросить флажок Synchronize. 
И тогда CounterThread, узнав об этом через IsDIgButtonChecked, не обращалась к 
LeaveCriticalSection. Таким образом, пока флажок оставался неактивен, 
CounterThread не освобождала критическую секцию, а поток, исполняющий функцию 
DisplayThread, так и ждал, когда подойдет его очередь занять критическую секцию. 
    Но, сохраняя состояние флажка в отдельной переменной и проверяя ее 
значение перед вызовом EnterCriticalSection или I^eaveCriticalSection, я 
исключил вероятность несогласованного вызова этих функций. Так что, как 
видите, при разработке многопоточного приложения нужно проявлять особую осторожность. 

    230 _______________________________Windows для профессионалов 


    CRITSECS.C 
    
    Модуль: CritSecs.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    
    ^include "..\CmnHdr.H"           /* см. приложение В */ 
    ^include <windows.h> 
    #include <windowsx.h> 
    ftinclude <tchar.h> 
    #include <stdio.h>               // для sprintf 
    #include <process,h>             // для _beginthreadex 
    ffinclude "Resource.H" 
    ////////////////////////''////////////////''/////////''////•'/'•'//Л'//',1 
,'Л'  • 
    // Глобальные переменные. 
    // g_fTerminate присваивается TRUE при закрытии диалогового окна. 
    // Это подсказывает рабочим потокам о необходимости завершения. 
    // Переменная помечена как volatile, поскольку ее значение может 
    // измениться в любой момент. 
    volatile BOOL g_fTerminate = FALSE; 
    HWND  g_hwnd; 
    HANDLE g_hThread[2]; // потоки Counter[0] и Display[1] 
    
    // данные, подлежащие защите 
    TCHAR g_szNumber[10] = __TEXT("0"); 
    
    // критическая секция для защиты данных 
    CRITICAL_SECTION g_CnticalSection: 
    illllllllllllllllllllllll/IHII/IIIIIIIIIIIHIIHIIIIIIIilllllllll/llllllllllll 
    
    / I добавляем строку в окно списка 
    void AddToListBox (LPCTSTR szBuffer) { 
    HWND hwndDataBox = GetDlgItem(g_hwnd, IDC_OATABOX): 
    int x = ListBox_AddString(hwndDataBox, szBuffer); 
    ListBox_SetCurSel(hwndDataBox, x); 
    if (ListBox_GetCount(hwndDataBox) > 100) 
    ListBox_DeleteStnng( hwndDataBox, 0); 
    i 
    /////////////////////////////////////////////////////////////////////////////// 
    
    II поток, который увеличивает счетчик в защищенных данных 
    DWORD WINAPI Counter-Thread (LPVOID IpThreadParameter) { 
    unsigned int nNumber, nDigit; 
    BOOL fSyncChecked; 
    см. след. стр. 

    Рис. 10-1. Листинг программы CritSecs 


    __________________________Глава 10. Синхронизация потоков 231 


    РИС. 10-1 (продолжение) 

    while (!g_fTerminate) { 
    
    // узнаем состояние флажка Synchronize и запомним его 
    fSyncChecked = IsDlgButtonChecked(g_hwnd, IDC_SYNCHRONIZE); 
    if (fSyncChecked) { 
    
    // если пользователь хочет синхронизировать нас, пусть синхронизирует 
    EnterCriticalSection(&g_CritlcalSection); 
    
    // преобразуем символьное значение числа в целое значение и добавим 1 
    _stscanf(g_szNuiiiber, __TEXT("%d"), &nNumber); 
    nNumber++: 
    
    // преобразуем новое число в строку 
    nDigit = 0; 
    while (nNumber != 0) { 
    // заносим разряд числа в строку 
    g_szNumber[nDigit++] = (TCHAR) (__ТЕХТ('0') + (nNumber % 10)); 
    // Вызов здесь функции Sleep сообщает системе, что неиспользованный 
    // остаток текущего кванта времени мы отдаем другому потоку. 
    // Этот вызов необходим в однопроцессорной системе для того, чтобы 
    // результаты синхронизации потоков или отсутствия таковой стали 
    // очевидны. Обычно функцию Sleep для этого, конечно, НЕ вызывают. 
    Sleep(O); 
    
    // готовимся получить следующий разряд 
    nNumber /= 10; 
    } 
    
    // все разряды преобразованы в строку; завершаем сгроку 
    g_szNuinber[nDigit] = 0. 
    
    II Символы сформированы в обратном порядке; восстанавливаем 
    // нормальный порядок. Если ANSI, вызываем _strrev, а если 
    // Unicode, то - _wcsrev. 
    _tcsrev(g_szNumber); 
    if (fSyncChecked) { 
    // Если пользователь хочет синхронизировать нас, пусть синхронизирует. 
    // В предыдущих версиях этой программы я вызывал функцию 
    // l3DlgButtonChecked вместо того, чтобы использовать переменную 
    // fSyncChecked. Это приводило к проблемам, потому что флажок 
    // Synchronize мог быть установлен или сброшен в промежутке между 
    // вызовами функций EnterCriticalSection и LeaveCriticalSection. 
    // Тем самым поток иногда освобождал ту критическую секцию, которую 
    // он никогда не занимал, а иногда занимал критическую секцию 
    // и не освобождал ее. 
    LeaveCriticalSection(&g_CriticalSection); 

    см. сле^. стр. 

    232 ________________:_____________Windows для профессионалов 


    РИС. 10-1 (продолжение) 
    
    // если полозователь хоче] чю-нибудь виде1ь на экране гих;ль 
    // каждой итерации, пусть видит 
    if (IsDlgButtonChecked(g_hwnd, IDC_SHOWCNTRTHRD)) 
    AddToListBox(__TEXT("Cntr: Increment")); 
    return(O); // сюда мы попадаем при закрытии окна 
    } 
    ; ' ' / I I ! 1 / / 11 / / / I I / I I ! 11 I I i I I I I I / I I I / I I 
/ 1 ! / ! ! I I I ! I / / ! /1 i I i ' // / / ' ,• '/ //11'/ !. ! •'.'•. 
    // поток, который добавляет текущее значение счетчика 
    // (собственно данные) в окно списка 
    DWORD WINAPI DisplayThread (LPVOIO IpThreadParameter) { 
    BOOL fSyncChecked; 
    TCHAR szBuffer[50]: 
    while (!g_rTerminate) { 
    
    // определяем: нужна ли синхронизация потоков 
    fSyncGhecked = IsDlgButtonChecked(g_hwnd, IDC_SYNCHRONIZE); 
    if (fSyncGhecked) 
    EnterCriticalSection(&g_CriticalSection); 
    
    // формируем строку с символьным представлением числа 
    _stprintf(szBuffer, __TEXT("Dspy: %s"), g_szNumber); 
    if (fSyncChecked) 
    LeaveCriticalSection(&g_CriticalSection); 
    // добавляем строку в окно списка 
    AddToListBox(szBuffer); 
    } 
    return(O): // сюда мы попадаем при закрытии окна 
    
    BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    HWND hWndCtI; 
    DWORD dwThreadID; 
    
    // связываем значок с диалоговым окном 
    chSETOLGICONS(hwnd. IDI_CRITSECS, IDI_CRITSECS); 
    
    // Запоминаем описатель диалогового окна в глобальной переменной, 
    // чтобы потоки могли легко получить к ней доступ. Это нужно 
    // сделать до создания потоков. 
    g_hwnd = hwnd: 

    см. сле1>. стр. 

    278 _______________________________Windows для профессионалов 

    // семафор служит просто счетчиком, доступным нескольким процессам; 
    
    // он НЕ используется для синхронизации потоков, а сообщает число 
    // потоков-"читателей", считывающих общие данные 
    HANDLE hSernNumReaders: 
    } SWMRG, *PSWMRG; 

    Эта структура состоит из трех описателей синхронизирующих объектов ядра. 
Первый описатель, hMutexNoWriter, используется SWMRG-функциями, чтобы 
определить, получил ли доступ к разделяемому ресурсу поток-«писатель». 
Вспомните: поток, который записывает что-то в общие данные, должен блокировать 
остальные потоки, пытающиеся обратиться к этому ресурсу. Объект-мьютекс как 
нельзя лучше подходит на эту роль, так как может принадлежать только одному 
потоку. Если он свободен, значит, поток-«писатель» сейчас не обращается к данным. 
    Второй описатель, hEventNoReaders, позволяет узнать, не обращается ли к 
данным один из потоков-«читателей». Если такие потоки не работают с общими 
данными, объект «событие со сбросом вручную» свободен. 
    Третий описатель, hSernNumReaders, сообщает о количестве 
потоков-«читателей», обращающихся к разделяемому ресурсу в данный момент. 
SWMRG-функции не используют этот семафор для синхронизации — он служит просто 
счетчиком. Объект SWMRG должен содержать такой счетчик, чтобы мы могли вести 
учет количества потоков, читающих общие данные; когда последний из них вызовет 
функцию SWMRGDoneReading, счетчик обнулится, и тогда, вызвав SetEvent, мы 
сможем освободить событие, идентифицируемое описателем hEven tNoR coders. 
    Разрабатывая структуру SWMRG, я поначалу вместо описателя семафора объявил 
в ней переменную типа LONG. Она служила мне счетчиком числа потоков, читающих 
общий ресурс. Но по мере проработки кода SWMRG я столкнулся с необходимостью 
сделать счетчик доступным другим процессам. Тогда я стал подумывать: может, 
стоит создать специальный файл и при вызове SWMRGInitialize проецировать его 
представление на адресные пространства каждого процесса? Но меня остановило 
то, что размер проецируемого в память файла минимум 1 страница — в то время 
как речь-то идет всего лишь о 4-байтовой переменной. Для системы, использующей 
страницы размером по 4096 байтов, перебор в 102 400%, а для системы со 
страницами по 8192 байта (DEC Alpha) — все 204 800%! Это слишком. 
    Пришлось поработать головой. В конце концов до меня дошло, что здесь нужен 
семафор: у него есть счетчик, он доступен разным процессам и занимает куда 
меньше памяти. Поэтому я создал объект ядра «семафор» и стал пользоваться им 
как счетчиком. Чтобы уменьшить счетчик, я вызываю WaitForSingleObject и 
передаю ей описатель семафора, а чтобы увеличить — обращаюсь к 
ReleaseSemaphore. Правда, текущее значение счетчика у семафора выяснить 
нельзя, и единственное, что можно сделать, — проверить, не равен ли он 0, 
вызвав WaitForSingleObject и передав ей нулевую величину таймаута. Если она 
вернет WAIT_TIMEOUT, счетчик семафора обнулен, а если — WAIT_OBJECT_0, счетчик 
больше 0. Это все, что можно узнать о семафоре, но ведь функциям, управляющим 
объектом SWMRG, больше ничего и не нужно. 
    В тексте файла SWMRG.С масса комментариев, объясняющих каждую функцию, но, 
пожалуй, стоит вкратце обсудить их и здесь. 
    Функция SWMRGInitialize инициализирует объект SWMRG. Она создает мьютекс, 
событие и семафор. Чтобы потоки из разных процессов могли получить доступ к 
объекту SWMRG, каждый процесс должен создать собственный экземпляр этого 
объекта. При вызове SWMRGInitialize поток должен передавать адрес и имя своего 
объекта SWMRG. Это имя функция использует при создании объектов «мьютекс», 
«событие» и «семафор». Если Вы, например, вызовете SWMRGInitialize, передав ей 
в параметре IpszName строку «Jeff», то мьютекс 

    Глава 10. Синхронизация потоков 279 

    получит имя SWMRGMutexNoWriterJeff, событие — SWMRGEventNoReadersJeff, a 
семафор — SWMRGSemNumReadersJeff. Дозапись содержимого параметра IpszNameB 
конец префикса объекта ядра осуществляется вспомогательной статической 
функцией ConstructObjName. 
    Когда поток в другом процессе вызывает SWMRGInitialize и передает 
идентичное имя в параметре IpszName, функция обнаруживает, что объекты ядра с 
такими именами уже есть, и поэтому просто возвращает описатели этих объектов. 
Так появляется возможность синхронизировать потоки в разных процессах. 
    SWMRGDelete — наипростейшая функция. Она всего лишь трижды вызывает 
CloseHandle (по разу на каждый объект ядра). 
    Когда поток-«писатель» готов модифицировать данные в разделяемом ресурсе, 
он должен сначала запросить разрешение на это, вызвав SWMRGWaitToWrite. Та 
обращается к УУш32-функции WaitForMultipleObjects, заставляя ее ждать два 
объекта: мьютекс (hMutexNoWriter) и событие (hEventNoR eaders). Если 
WaitForMultipleObjects возвращает WAIT_OBJECT_0, значит, разделяемый ресурс не 
используется другими потоками (ни «писателями», ни «читателями»). Если же она 
возвращает WAIT_TIMEOUT, поток-«писатель» узнает, что сейчас модифицировать 
общие данные небезопасно, так как к ним обращается другой поток-«писатель» или 
минимум один поток-«читатель». 
    Закончив модификацию общих данных, поток-«писатель» вызывает 
SWMRGDoneWriting. Освобождая семафор (hMutexNoWriter), она сообщает тем самым 
остальным потокам, что «писатель» больше не работает с разделяемым ресурсом. 
    Когда поток-«читатель» готов к считыванию данных из общего ресурса, он 
должен сначала запросить разрешение на это, вызвав SWMRGWaitToRead. Та 
обращается к Win32функции WaitForSingleObject, передавая ей описатель мьютекса 
(hMutexNoWriter). Так сделано из-за того, что поток-«читатель» может считывать 
данные в любой момент, если только к ним не обращается поток-«писатель». Если 
заданное время ожидания мьютекса при вызове WaitForSingleObject истекло, 
SWMRGWaitToRead возвращает WAIT_TIMEOUT. 
    Однако, если вызов WaitForSingleObject позволит благополучно занять 
объект-мьютекс, счетчик семафора (подсчитывающий число потоков-«читателей») 
следует увеличить. Это достигается вызовом Releases emaphore с передачей ей 
описателя hSemNumReaders и параметра cReleaseCount, равного 1. Функция 
возвращает предыдущее значение счетчика семафора в переменной типа DWORD, 
адрес которой передается в последнем параметре. SWMRGWaitToRead проверяет это 
значение и, если перед вызовом ReleaseSemaphore к разделяемому ресурсу не 
обращался ни один поток-«читатель», сбрасывает состояние объекта «событие» 
(hEventNoReaders), указывая, что к общим данным получил доступ минимум один 
поток-«читатель». Если же данный поток — не первый «читатель», то объект уже 
сброшен, и незачем повторно вызывать ResetEvent. 
    Перед самым возвратом управления SWMRGWaitToRead вызывает ReleaseMutex, 
передавая ей описатель hMutexNoWriter. Зачем? Дело в том, что если 
SWMRGWaitToRead благополучно дождалась этого объекта-мьютекса, то (как 
побочный эффект) он переходит в занятое состояние. А это будет означать, что к 
общим данным якобы пытается обратиться поток-«писатель». Следовательно, мы 
должны исправить последствия этого побочного эффекта, вызвав ReleaseMutex, 
чтобы не дать ложную информацию другим потокам. 
    И последняя функция, которую я хочу обсудить, — SWMRGDoneReading. Ее 
должен вызвать поток-«читатель», окончив работу с общими данными. Эта функция 
сначала обращается к WaitForMultipleObjects, передавая ей описатель 
hMutexNoWriter, так как поток, вызвавший SWMRGDoneReading, должен получить 
монопольный доступ к объектам hEventNoReaders и hSemNumReaders. Потоку нужно 
изменить состояние этих объектов, чтобы никакие другие потоки не смогли 
воспользоваться ими, пока поток, закончивший чтение, полностью не обновит их. 
Поскольку все функции (SWMRGWaitToWrite, SWMRGDoneWriting, SWMRGWaitToRead и 
SWMRGDoneReading), прежде чем продолжить выполнение, до конца ждут объект 

    _________________________Глава 10.  Синхронизация потоков 233 

    РИС. 10-1 (продолжение) 

    I/ Инициализируем критическую секцию. Это нужно сделать 
    // до того, как к ней попытается обратиться какой-нибудь поток. 
    InitializeCriticalSection(&g_CriticalSection); 
    
    // создаем поток с функцией Counter-Thread и запускаем его 
    g_hThread[0] = chBEGINTHREADEX(NULL. О, Counter-Thread, NULL, 0, &dwThreadID); 
    
    // создаем поток с функцией DisplayThread и запускаем его 
    g_hThread[1] = chBEGINTHREADEX(NULL, 0, DisplayThread, NULL, 0, &dwThreadID): 
    // заполняем комбинированный список Process Priority Class 
    // и выбираем Normal 
    hWndCtI = GetDlgItem(hwnd, IDC_PRIORITYCLASS); 
    ComboBox_AddString(hWndCtl, __TEXT("Idle")); 
    ComboBox_AddString(hWndCtl, __TEXT("Normal")): 
    ComboBox_AddString(hWndCtl, __TEXT("High")); 
    ComboBox_AddString(hWhdCtl, __TEXT("Realtime")); 
    ComboBox_SetCurSel(hWndCtl, 1); // Normal 
    // заполняем комбинированный список Display Thread Priority 
    // и выбираем Normal 
    hWndCtI = GetDlgItem(hwnd, IDC^DSPYTHRDPRIORITY): 
    ComboBox_AddString(hWndCtl, __TEXT("Idle")); 
    ComboBox_AddString(hWndCtl, __TEXT("Lowest")): 
    ComboBox_AddString(hWndCtl, __TEXT("Below normal")): 
    Combo8ox_AddString(hWndCtl, _J"EXT("Normal")); 
    ComboBox_AddString(hWndCtl. __TEXT("Above normal")): 
    ComboBox_AddString(hWndCtl, __TEXT("Highest")); 
    ComboBox_AddString(hWndCtl, __TEXT("Timecritical")); 
    ComboBox_SetCurSel(hWndCtl, 3); // Normal 
    // заполняем комбинированный список Counter Thread Priority 
    // и выбираем Normal 
    hWhdCt! = GetDlgItem(hwnd, IDC_CNTRTHRDPRIORITY): 
    ComboBox_AddSt ring(hWhdGtl, __TEXT("Idle")): 
    ComboBox_AddString(hWhdCtl, __TEXT("Lowest")); 
    ComboBox_AddString(hWndCtl, __TEXT("Below normal")); 
    ComboBox_AddStnng(hWndCtl, __TEXT( "Normal")): 
    ComboBox_AddString(hWhdCtl, __TEXT("Above normal")); 
    ComboBox_AddString(hWndCtl, __TEXT("Highest")); 
    ComboBox_AddString(hWndCtl, __TEXT("Timecritical")); 
    ComboBox_SetCurSel(hWndCtl, 3); // Normal 
    return(TRUE); 
    } 
    lilllllllllll!illlll///ll/ll/tllll//l/llillll///lll/lll//llllllllllllllllllllll 

    см. след. стр. 

    234 ____________________________Windows для профессионалов 

    РИС. 10-1 (продолжение) 

    void Dlg_OnDestroy (HWND hwnd) { 
    
    // когда диалоговое окно закрывается, сообщаем потокам, 
    // что им надо завершиться 
    c]._fTerminate = TRUE: 
    
    // возобновляем выполнение потоков, если они приостановлены 
    ResumeThread(g_hThread[0]); 
    ResumeThread(g_hThread[1]); 
    ^ 
    void Dlg_OnGommand (HWND hwnd, int id, HWND hwndCti, UINT codeNotity) { 
    HANDLE hThread; 
    DWORD dw; 
    
    switch (id) { 
    case IDCANCEL: 
    EndDiaiog(hwnd, id); 
    break; 
    case IDC_PRIORITYCLASS: 
    
    if (codeNotify != CBN_SELCHANGE) 
    break: 
    
    // пользователь меняет класс приоритета 
    switch (ComboBox_GetCurSel(hwndCtl)) { 
    case 0: 
    dw = IDLE_PRIORITY_CLASS; 
    break; 
    case 1: 
    default; 
    dw = NORMAL_PRIORITY_CLASS; 
    break; 
    case 2: 
    dw = HIGH_PRIORITY_CLASS; 
    break; 
    case 3: 
    dw = REALTIME_PRIORITY_GLASS: 
    break; 
    ) 
    SetPriorityClass(GetCurrentProcess(), dw); 
    break; 

    см. след. стр. 

    __________________________Глава 10. Синхронизация потоков 235 


    РИС. 10-1 (продолжение) 


    case IDC_DSPYTHRDPRIORITY: 
    case IDC_CNTRTHROPRIORITY: 
    
    if (codeNotify != CBN_SELCHANGE) 
    break; 
    
    switch (ComboBox_GetCurSel(hwndCtl)) { 
    case 0: 
    dw = (DWORD) THREAD_PRIORITY_IDLE; 
    break; 
    case 1: 
    dw = (DWORD) THREAD_PRIORITY_LOWEST: 
    break; 
    case 2: 
    dw = (DWORD) THREAD_PRIORITY_BELOW_NORMAL; 
    break: 
    case 3: 
    default: 
    dw = (DWORD) THREAD_PRIORITY_NORMAL: 
    break; 
    case 4: 
    dw = (DWORD) THREAD_PRIORITY_ABOVE_NORMAL; 
    break; 
    case 5: 
    dw = (DWORD) THREAD_PRIORITY_HIGHEST; 
    break; 
    case 6: 
    dw = (DWORD) THREAD_PRIORITY_TIME_CRITICAE; 
    break; 
    } 
    // пользователь меняет относительный приоритет 
    // одного из потоков 
    hThread = (id == IDC_CNTRTHRDPRIORITY) ? g_hThread[0] : g_hThread[1]; 
    SetThreadPriority(hThread. dw): 
    break; 
    case IDC_PAUSE: 
    
    // пользователь приостанавливает или 
    // возобновляет оба потока 

    см. след. стр. 

    236 ______________________________Windows для профессионалов 


    РИС. 10-1 (продолжение) 

    it (Button_GetCheck(hwnaCtl)) { 
    SuspendThread(g_hThread[0]); 
    SuspendThread(g_hThread[1]); 
    } else { 
    ResumeThread(g_hThread[0]): 
    ResumeThread(g_hThread[1]); 
    } 
    break; 
    i 
    .'7/////////////////7////////////////////////////,'///////7////.-7//,/.1,/.,,1'1',•/.1•1,1, 
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_0nlnit0ialog); 
    chHANDLE_DLGMSG(hwnd, WM_DESTROY, Dlg_0n0estroy); 
    chHANDLE„DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); 
    } 
    return(FALSE): 
    Illllllllllllllll/llll/llllllll/llllllllllllllllllllllllltlllllllltllllllltllll 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    ChWARNIFUNICODEUNDERWIN95(); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDO_CRITSECS), NULL, Dlg_Proc); 
    
    // ждем завершения обоих потоков 
    WaitForMultipleObjects(2, g_hThread, TRUE, INFINITE); 
    
    // закрываем описатели потоков 
    CloseHandle(g_hThread[0]); 
    CloseHandle(g_hThread[1]); 
    
    // потоки больше не будут использовать критическую секцию, 
    // поэтому ее можно уничтожить 
    DeleteCriticalSection(&g_CriticalSection); 
    return(O); 
    IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII конец файла ///////////////////////////////// 

    ___________________________Глава 10. Синхронизация потоков 237 


    Синхронизация потоков с объектами ядра 

    Критические секции — штука, конечно, удобная, но бывает, нужно 
синхронизировать приложения со специфическими событиями, возникающими в 
системе, или с операциями, выполняемыми в других процессах. Скажем, если Вы 
создаете дочерний процесс, может быть, придется сделать так, чтобы 
родительский процесс ожидал его завершения и только потом продолжал свою работу. 
    Для синхронизации потоков можно использовать такие объекты ядра: 
    • процессы; 
    • потоки; 
    • файлы; 
    • консольный ввод; 
    • уведомления обдз.менении файлов; 
    • объекты-мьютексы; 
    • семафоры; 
    • события (с автосбросом и сбросом вручную); 
    • ожидаемые таймеры (waitable timers) (только в Windows NT 4 и выше). 
    Каждый объект может находиться в одном из двух состояний: свободном 
(signaled) или занятом (nonsignaled). Потоки могут остановиться и ждать 
освобождения какого-либо объекта. Если, скажем, поток родительского процесса 
должен ждать завершения дочернего, его можно отправить «в спячку» вплоть до 
освобождения объекта ядра, идентифицирующего дочерний процесс. В главе 3 я уже 
говорил, что объекты «процесс» получают статус свободных в момент завершения 
соответствующих процессов. Это относится и к объектам «поток». Когда поток 
создан и начинает исполнение своего кода, сопоставленный с ним объект ядра 
«поток» получает статус занятого. При завершении потока соответствующий объект 
ядра освобождается. 
    Я предпочитаю рассматривать понятия «свободен-занят» по аналогии с 
обыкновенным флажком. Потоки спят, пока ожидаемые ими объекты заняты (флажок 
опущен). Объект освободился (флажок поднят) — спящий поток замечает это, 
просыпается и возобновляет исполнение. 
    Часть перечисленных выше объектов ядра предназначена лишь для 
синхронизации потоков, но среди них есть и такие, чье применение гораздо шире. 
Например, если потоку известен описатель объекта ядра «процесс», он может 
вызывать УУт32-функции, позволяющие изменять класс приоритета или получать код 
завершения процесса. Через этот же описатель поток может синхронизировать себя 
с завершением процесса. 
    Описатель потока служит для управления потоком и его синхронизации с 
завершением другого потока. То же относится и к описателям файлов. Используя 
такой описатель, Вы считываете файл или записываете в него какие-то данные и в 
то же время можете синхронизировать поток с окончанием какой-либо операции 
асинхронного файлового ввода/вывода. (Эта тема рассматривается в главе 15.) 
    Последний тип объектов ядра «двойного назначения» — консольный ввод. Он 
похож на объект «файл», и его тоже можно создать через функцию CreateFile. 
Приложение консольного типа использует описатель этого объекта для чтения 
данных из буфера ввода, а поток — для приостановки своей деятельности до 
появления в этом буфере данных для обработки. 
    Прояие_объекты ядра (уведомления об изменении файлов, мьютексы, семафоры, 
события и ожидаемые таймеры) служат одной__цели — синхронизации потоков. Ряд 
Win32функций, предназначенных специально для работы с этими объектами, 
позволяет создавать 

    238 _______________________________Windows для профессионалов 

    и открывать объекты, синхронизировать с ними потоки и закрывать их. 
Никакие другие операции с подобными объектами ядра не допускаются. В этой 
главе мы обсудим, как синхронизировать потоки с помощью мьютексов, семафоров, 
событий и ожидаемых таймеров; объекты «уведомление об изменении файла» будут 
рассмотрены в главе 14. 
    Потоки усыпляют_себя до освобождения какого-либо объекта ядра с помощью 
двух основных функций: 

    DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeout); 

    и 

    DWORD WaitForMultipleObJects(DWORO cObjects, LPHANDLE IpHandles, BOOL 
bWaitAll, DWORD dwTimeout); 

    WaitForSingleObject сообщает системе, что поток ожидает освобождения 
объекта ядра, на который указывает параметр hObject. Параметр dwTimeout 
подсказывает, сколько времени (в миллисекундах) поток готов ждать этого 
события. Если объект ядра не перейдет в свободное состояние в течение 
заданного времени, система вновь активизирует поток и продолжит его 
исполнение. WaitForSingleObject возвращает одно из следующих значений: 

    Возвращаемый код      Значение         Описание 

    WAIT_OBJECT_0         0х00000000        Объект перешел в свободное состояние. 
    WAIT_TIMEOUT         0х00000102        Объект не перешел в свободное 
состояние за указанный в dwTimeout период времени. 
    WAIT_ABANDONED      0х00000080        Объект-мьютекс перешел в свободное 
состояние из-за отказа от него (см. раздел «Объектымьютексы» далее в этой главе). 
    WAIT_FAILED            OxFFFFFFFF        Произошла ошибка. Чтобы узнать ее 
код, вызовите GetLastError. 

    В параметре dwTimeout функции WaitForSingleObject можно передать два 
особых значения. Нуль означает, что Вы не собираетесь чего-то ждать, а просто 
хотите выяснить: занят объект или нет. Если функция вернет WAIT_OBJECT_0, 
объект свободен, WAIT_TIMEOUT — занят. И, наконец, передача INFINITE 
(OxFFFFFFFF) заставит WaitForSingleObject ждать, пока объект освободится. В 
этом случае, если объект так и не освободится, поток останется в неактивном 
состоянии и никогда не получит процессорного времени. 
    WaitForMultipleObjects аналогична WaitForSingleObject за тем исключением, 
что позволяет ждать освобождения сразу нескольких объектов или какого-то 
одного из списка объектов. При ее вызове в параметре cObjects укажите 
количество интересующих Вас объектов (не более MAXIMUM_WAIT_OBJECTS, 
определенной как 64). Параметр IpHandles— указатель на массив описателей, 
идентифицирующих эти объекты. Объект можно указать в списке только раз, иначе 
произойдет ошибка — даже если объект идентифицируют два разных описателя. 
    Параметр bWaitAll определяет, хотите Вы ждать освобождения всех объектов 
из списка или только одного. Если он равен TRUE, WaitForMultipleObjects ждет 
одновременного освобождения всех объектов из указанного списка. А если FALSE — 
функция ждет освобождения лишь одного объекта. В последнем случае она 
сканирует массив описателей (начиная с нулевого элемента), и первый же 
освободившийся объект прерывает ожидание. Параметр dwTimeout идентичен 
одноименному параметру функции WaitForSingleObject. Если освобождаются сразу 
несколько объектов, WaitForMultipleObjects возвращает индекс первого же 
описателя, который определяет освобожденный объект. 

    _____________._____.._„______Глава 10. Синхронизация потоков 239 


    WaitForMultipleObjects возвращает одно из следующих значений: 

    Возвращаемый код      Значение       Описание 

    от WAIT_OBJECT_0 до    Начиная        При ожидании всех объектов это значение 
    (WAIT_OBJECT_0 +       с 0х00000000    указывает на то, что ожидание 
закончилось cdbjects - 1)                             успешно. При ожидании 
одного из объектов это значение представляет собой индекс того описателя в 
массиве IpHandles, который принадлежит объекту, перешедшему в свободное состояние. 
    WAIT_TIMEOUT         0х00000102      Объект или объекты не перешли в 
свободное состояние за указанный в dwTimeout период времени. 
    от WAIT_ABANDONED   Начиная        При ожидании всех объектов это значение 
    до (WAIT_ABANDONED + с 0х00000080    указывает на то, что ожидание 
закончилось успешно 
    cObjects - 1)                             и что минимум один объект был 
мьютексом, перешедшим в свободное состояние из-за отказа от него. При ожидании 
одного из объектов это значение представляет собой индекс того описателя в 
массиве IpHandles, который принадлежит объектумьютексу, перешедшему в 
свободное состояние из-за отказа от него. 
    WAIT_FAILED            OxFFFFFFFF     Произошла ошибка. Чтобы узнать ее 
код, вызовите GetLastError. 

    Функции WaitForSingleObject и WaitForMultipleObjects производят весьма 
заметные побочные эффекты на некоторые объекты ядра. Для объектов «процесс» и 
«поток» такие эффекты не проявляются, и после перехода в свободное состояние 
они в нем и остаются. Простой пример: если 10 потоков вызывают 
WaitForSingleObject и ждут- освобождения одного и того же объекта «процесс», 
то по завершении процесса этот объект получит статус свободного и все 
ожидавшие его потоки продолжат работу. То же самое относится и к объектам «поток». 
    В случае объектов-мьютексов, событий с автосбросом и ожидаемых таймеров с 
автосбросом (auto-reset waitable timers) функции WaitForSingleObject и 
WaitForMultipleObjects меняют их состояние на занятое. Как только один из 
таких объектов становится свободным и поток пробуждается, статус объекта 
меняется на занятый. Благодаря этому пробуждается только один поток из числа 
ожидающих подобный объект — остальные продолжают спать. В этих концепциях 
немудрено запутаться, но многое в них прояснится при детальном рассмотрении 
каждого из перечисленных синхронизирующих объектов. 
    Еще одно соображение по поводу WaitForMultipleObjects: при вызове ее с 
параметром bWaitAll, равным TRUE, объекты, ожидавшиеся потоком, не 
переустанавливаются в занятое состояние до тех пор, пока не станут свободны 
все объекты, указанные в списке. Иначе говоря, система периодически сканирует 
объекты из списка, и, если все они освободились, WaitForMultipleObjects 
восстанавливает занятое состояние объектов-мьютексов, событий и ожидаемых 
таймеров с автосбросом. Но заметьте: система не изменяет состояние отдельного 
объекта — описываемая операция происходит только при одновременном 
освобождении объектов, указанных в списке. (Дальнейшее рассмотрение функции 
WaitForMultipleObjects рассчитано на тех, кто хорошо понимает концепции 
синхронизации с использованием объектов ядра. Если Вы в этом деле новичок, 
советую сначала прочесть следующий раздел — об объектах-мьютексах — и только 
потом вернуться сюда.) 
    Вот пример, иллюстрирующий то, что я имел в виду. Предположим, поток 1 
ждет два объекта: мьютекс А и мьютекс В. Допустим также, что поток 2 исполняет 
код и вот-вот 

    240 _______________________________Windows для профессионалов 

    войдет в состояние ожидания мьютекса А, которого уже ждет и поток 1. Если 
теперь мьютекс А освободится, то потоку 1 — чтобы продолжить исполнение — еще 
надо дождаться освобождения мьютекса В. И тут поток 2 вызывает 
WaitForSingleObject, указав в качестве нужного объекта мьютекс А. Что 
получится? Система передаст ему «права на собственность» данным объектом. В 
результате поток 1 теперь будет ждать не только объекта В, но и объекта А, 
который должен быть освобожден потоком 2. 
    При вызове WaitForMultipleObjects такой передачи прав на отдельный объект 
не происходит. Если бы она получала права на синхронизирующие объекты по мере 
того, как они становятся доступны, могла бы возникнуть патовая ситуация. 
Допустим, потоки 1 и 2 приостановили свою деятельность вызовом 
WaitForMultipleObjects и ждут объектов-мьютексов А и В. И вот мьютекс А 
свободен (скажем, его освободил третий поток). Обнаружив это, система передает 
права на него потоку 1. Далее все тот же третий поток освобождает и мьютекс В. 
Система передает права на него потоку 2. И тут-то получается, что потоки 1 и 
2, по-прежнему неактивные, ждут, по сути, друг друга — каждому нужен объект, 
занятый другим. 
    Поняли теперь, в чем проблема? Поток 1 владеет объектом-мьютексом А, но не 
может возобновить исполнение, и поэтому мьютекс А никогда не освободится. 
Значит, поток 2 никогда не получит этот объект и тоже останется в 
«подвешенном» состоянии. Поэтому, чтобы избежать такой блокировки, 
WaitForMultipleObjects не сбрасывает состояние объектов до тех пор, пока все 
объекты (из числа указанных) не освободятся одновременно. 

    Объекты-мьютексы 

    Эти объекты весьма похожи на критические секции — за исключением того, что 
с их помощью можно синхронизировать ^доступ к данным со стороны нескольких 
процессов. Для использования объекта-мьютекса один из процессов должен сначала 
создать его вызовом CreateMutex: 

    HANDLE CreateMutex(LPSECURITY_ATTRIBUTES Ipsa, BOOL fInitialOwner, 
    LPTSTR IpszMutexName); 

    Параметр Ipsa указывает на структуру SECURITY_ATTRIBUTES. Параметр 
fInitialOwner определяет, должен ли поток, создающий мьютекс, быть 
первоначальным владельцем этого объекта. Если он равен TRUE, данный поток 
становится его владельцем, и, следовательно, объект-мьютекс оказывается в 
занятом состоянии. Любой другой поток, ожидающий данный мьютекс, будет 
приостановлен, пока поток, создавший этот объект, не освободит его. Передача 
FALSE в параметре fInitialOwner подразумевает, что объект-мьютекс не 
принадлежит ни одному из потоков и поэтому «рождается свободным». Первый же 
поток из числа ожидающих этот объект может занять его и тем самым продолжить 
свое выполнение. 
    Параметр IpszMutexName содержит либо NULL, либо адрес строки (с нулевым 
символом в конце), идентифицирующей мьютекс. Когда приложение вызывает 
CreateMutex, система создает объект ядра «мьютекс» и присваивает ему имя, на 
которое указывает параметр IpszMutexName. Это имя используется при совместном 
доступе к нему нескольких процессов (но об этом мы поговорим позже). 
CreateMutex возвращает «процессо-зависимый» описатель, определяющий созданный объект-мьютекс. 
    Одно из главных отличий объектов-мьютексов от критических секций в том, 
что первые способны синхронизировать потоки, выполняемые в разных процессах. С 
этой целью поток в каждом процессе должен располагать своим, специфичным для 
данного процесса описателем единственного объекта-мьютекса. Эти описатели 
можно получить несколькими способами. Самый распространенный: один из потоков 
каждого процесса вызывает CreateMutex, и все передают ей через параметр 
IpszMutexName идентичные строки. Первый вызов функции приводит к созданию 
объекта ядра «мьютекс», остальные вызовы просто возвращают соответствующим 
потокам описатели этого объекта, значения которых специфичны для каждого процесса. 
    Проверить, действительно ли CreateMutex создала новый объект-мьютекс, 
можно через GetLastError (но обращаться к ней нужно сразу после вызова 
CreateMutex). Если GetLastError 

    ________________________Глава 10. Синхронизация потоков 241 

    вернет ERROR_ALREADY_EXISTS, значит, новый объект-мьютекс не создан. Если 
же Вы просто хотите сделать этот объект доступным другим процессам, нет нужды 
обращаться к последней функции. 
    Еще один способ получить описатель мьютекса — вызвать OpenMutex: 

    HANDLE OpenMuTex"( DWORD fdwAccess. BOOL finherit, LPTSTR IpszName); 

    Параметр fdwAccess может быть равен либо SYNCHRONIZE, либо 
MUTEX_ALL_ACCESS. Параметр finherit определяет, унаследует ли дочерний процесс 
данный описатель данного объекта-мьютекса. А параметр IpszName — это имя 
объекта-мьютекса в виде строки с нулевым символом в конце. 
    При вызове OpenMutex система сканирует существующие объекты-мьютексы, 
проверяя, нет ли среди них объекта с именем, указанным в параметре IpszName. 
Обнаружив таковой, она создает описатель объекта, специфичный для данного 
процесса, и возвращает его вызвавшему потоку. В дальнейшем любой поток из 
данного процесса может использовать этот описатель при вызове любой функции, 
требующей такой описатель. А если объектамьютекса с указанным именем нет, 
функция возвращает NUEL. 
    Оба описанных выше способа требуют, чтобы у объекта-мьютекса было какое-то 
имя. Но существуют еще два способа, в которых имя мьютекса не нужно. Один из 
них построен на вызове DuplicateHandk, в другом используется механизм 
наследования описателей. Эти способы рассмотрены в главе 2. 

    Применение мьютексов вместо критических секций 

    Давайте перепишем пример, который иллюстрировал применение критических 
секций, используя объекты-мьютексы. Взглянув на показанный ниже код, Вы 
несомненно заметите, что он очень похож на прежний: 

    int g_nlndex = 0; 
    const int MAX_TIMES = 1000: 
    DWORD g_dwTimes[MAX_TIMES]: 
    HANDLE g_hMutex = NULL: 
    int WINAPI WinMain(... ) { 
    HANDLE hThreads[2]; 
    // создаем мьютекс до создания потоков, 4iu6oi он бшл до iuru, 
    // как потоки начнут исполнение 
    g_hMutex = CreateMutex(NULL, FALSE, NULL); 
    
    // сохраняем описатели потоков в массиве 
    hThreads[0] = CreateThread(..., FirstThread, ...): 
    hThreads[1] = CreateThread(..,, SecondThread, ...): 
    
    // ждем. когда завершатся оба потока 
    WaitForMultipleObjects(2, hThreads, TRUE, INFINITE); 
    
    // закрываем описатели потоков 
    CloseHandle(hThreads[0]); 
    CloseHandle(hThreads[1]); 
    
    // закрываем мьютекс 
    CloseHandle(g_hMutex); 
    } 

    см. след. стр. 


    242 _______________________________Windows для профессионалов 
    

    DWORD WINAPI FirstThread(LPVOID IpvThreadParm) { 
    BOOL fDone = FALSE: 
    DWORD dw; 
    while (!fDone) { 
    
    // ждем вечно, когда освободится мьютекс 
    dw = WaitForSingleOb]ect(g_hMutex, INFINITE); 
    if (dw == WAIT_OBJECT_0) { 
    
    // мьютекс перешел в занятое состояние 
    if (g_nlndex >= MAX_TIMES) { 
    fDone = TRUE; 
    } else { 
    g_dwTimes[g_nIndex] = GetTickCount(); 
    g_nlndex++; 
    } 
    
    // освобождаем мьютекс 
    ReleaseMutex(g_hMutex); 
    } else { 
    
    // произошел отказ от объекта-мьютекса 
    break: // выходим из цикла while 
    } 
    return(O); 
    
    DWORD WINAPI SecondThread(LPVOID IpvThreadParm) { 
    BOOL fDone = FALSE: 
    DWORD dw; 
    while (!fDone) { 
    
    // ждем вечно, когда освободится мьютекс 
    dw = WaitForSingleObject(g_hMutex, INFINITE); 
    if (dw == WAIT_OBJECT_0) { 
    
    // мьютекс перешел в занятое состояние 
    if (g_nlndex >= MAX_TIMES) { 
    fDone = TRUE; 
    } else { 
    g_nlndex++; 
    g_dwTimes[g_nIndex - 1] = GetTickCount(); 
    \ 
    // освобождаем мьютекс 
    ReleaseMutex(g_hMutex); 
    } else { 
    
    // произошел отказ от объекта-мьютекса 
    break; // выходим из цикла while 
    } 
    } 
    return(O); 


    ___________________________Глава 10. Синхронизация потоков 243 

    Заметьте — и это весьма важно, — что я создал объект-мьютекс до создания 
потоков. Будь то иначе, потоки могли бы вызвать WaitForSingleObject, передав 
ей вместо описателя NULL (из-за отсутствия мьютекса). Но можно написать код и 
по-другому; тогда будет вполне допустимо сначала создать потоки: 
    
    // создаем оба потока, но не разрешаем их исполнение 
    hThreads[0] = CreateTnread(..., FirstThread, NULL, CREATE_SUSPENDED, ...); 
    hThreads[1] = CreateThreadC..., SecondThread, NULL, CREATE_SUSPENDED, ...); 
    
    // создаем мьютекс 
    g_hMutex = CreateMutex(NULL, FALSE, NULL); 
    
    // разрешаем выполнение потоков 
    ResLimeThread(hThreads[0]); 
    ResumeTnread(nTnreads[1]); 

    Здесь я создал оба потока, но не разрешил их исполнение. Они не получат 
процессорного времени, пока не будут возобновлены. Далее я создал 
объект-мьютекс и сохранил его описатель в глобальной переменной g_hMutex. 
После чего — зная, что этот описатель отличен от NULL, — я возобновил 
исполнение потоков, дважды вызвав ResumeThread. Порядок действий тут очень 
важен: я сам не раз грешил тем, что ссылался на еще не созданные объекты. 
    Теперь вернемся к функции WinMain из предыдущего примера. На нем я 
показал, как первичный поток процесса ожидает завершения двух других потоков, 
— это было сделано ^"помощью WaitForMultipleObjects. При обращении к ней 
вместо первого параметра я подставил число 2, сообщив тем самым, что первичный 
поток ожидает освобождения двух объектов; параметр hThreads указывает на 
массив описателей, a TRUE означает, что первичный поток ждет освобождения 
сразу всех объектов, т. е. завершения обоих потоков. Включение идентификатора 
INFINITE говорит, что первичный поток готов ждать завершения других потоков 
хоть целую вечность. После возврата управления от WaitForMultipleObjects 
функция WinMain вызывает CloseHandle, чтобы уничтожить объект-мьютекс. 
    Функции обоих потоков были модифицированы для использования вместо 
критических секций объектов-мьютексов. Вызовы EnterCriticalSection я заменил 
на вызовы WaitForSingleObject. Последняя в принципе может возвращать одно из 
значений: WAIT_OBJECT_0, WAIT_ABANDONED или WAIT_TIMEOUT. Но WAIT_TIMEOUT Вы 
здесь никогда не получите, поскольку при вызове функции был указан 
идентификатор INFINITE. Возврат значения WAIT_OBJECT_0 означает, что 
объект-мьютекс стал свободен и поток может возобновить исполнение. Когда 
WaitForSingleObject сообщает, что объект-мьютекс перешел в свободное 
состояние, поток тут же занимает его, после чего может оперировать со 
структурой данных. Когда необходимость в доступе к этой структуре у него 
отпадает, поток вызывает ReleaseMutex: 

    BOOL ReleaseMutex(HANDLE hMutex): 

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

    244 ______________________________Windows для профессионалов 

    защищенным данным никто не обращается. А появится поток, которому нужен 
этот объект, — он тут же захватит мьютекс и заблокирует доступ к нему прочим потокам. 
    Еще раз подчеркну: используя в программе какой бы то ни было 
синхронизирующий объект, стремитесь свести к минимуму время его занятия 
каким-либо потоком. Если синхронизирующий объект ждут и другие потоки, то в 
этот период они ничего не делают. 

    Отказ от объекта-мьютекса 

    Объект-мьютекс отличается от других синхронизирующих объектов ядра тем, 
что занявшему его потоку передаются права на владение им. Прочие 
синхронизирующие объекты могут быть либо свободны, либо заняты — и все. А 
объекты-мьютексы способны еще и запоминать, какому потоку они принадлежат. 
Отказ от объекта-мьютекса происходит, когда ожидавший его поток захватывает 
этот объект (переводя в занятое состояние), а затем завершается. В таком 
случае получается, что мьютекс занят и никогда не освободится, поскольку 
никакой другой поток не сможет этого сделать вызовом ReleaseMutex. 
    Однако система не терпит подобных ситуаций и, заметив, что произошло, 
автоматически переводит мьютекс в свободное состояние. Поэтому потоки, 
ожидающие данный объект через WaitForSingleObject, получают возможность 
захватить его, а упомянутая функция возвращает WAIT_ABANDONED вместо 
WAIT_OBJECT_0. Таким образом, поток узнает, что мьютекс освобожден 
некорректно. Это обычно указывает на то, что в исходном коде программы есть 
«жучок». Выяснить, что сделал с защищенными данными завершенный поток — 
владелец объекта-мьютекса, увы, невозможно. (И не забудьте: потоки можно 
завершить принудительно вызовом ExitThread или Term in ate Thread.) 
    Поэтому я и проверял в предыдущем фрагменте кода, не было ли отказа от 
мьютекса, и, если он был, прерывал цикл while, завершая тем самым поток. 
Затем, если WinMain обнаруживала, что оба потока завершены, она уничтожала 
объект-мьютекс и завершала процесс. Я мог бы проигнорировать вероятность 
возврата функцией WaitForSingleObject идентификатора WAIT_ABANDONED, но тогда 
нельзя было бы поручиться за состояние защищенных данных. 
    И последнее. С объектом-мьютексом сопоставляется счетчик, фиксирующий, 
сколько раз данный объект передавался во владение какому-либо потоку. Поэтому, 
если поток вызовет WaitForSingleObject для уже принадлежащего ему 
объекта-мьютекса, он сразу же получит доступ к защищаемым этим объектом данным 
(так как система — по упомянутому счетчику — тут же определит, что поток уже 
владеет этим объектом-мьютексом). Кроме того, при каждом вызове 
WaitForSingleObject потоком — владельцем объекта-мьютекса этот счетчик 
увеличивается на 1. А значит, чтобы перевести мьютекс в свободное состояние, 
потоку придется соответствующее число раз вызывать ReleaseMutex. Функции 
EnterCriticalSection и LeaveCriticalSection действуют по отношению к 
критическим секциям аналогичным образом. 

    Программа-пример Mutexes 

    Программа Mutexes (MUTEXES. EXE) (см. листинг на рис. 10-2) представляет 
собой простую модификацию программы CritSecs, перестроенной на использование 
объектов-мьютексов. И даже внешне программа Mutexes ведет себя абсолютно так 
же, как и CritSecs. Но благодаря применению объектов-мьютексов вместо 
критических секций теперь мы могли бы поместить CounterThread в один процесс, 
a DisplayThread — в другой (пусть даже в примере это и не показано). Файлы 
исходного кода и ресурсов находятся в каталоге MUTEXES на компактдиске, 
прилагаемом к книге. 
прилагаемом к книге. 


    _________________________Глава 10. Синхронизация потоков 245 


    MUTEXES.C 
    
    Модуль: Mutexes.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ftinclude "..\CmnHdr,H"           /* см. приложение В */ 
    #include <windows,h> 
    #include <windowsx.h> 
    #include <tchar.h> 
    ^include <stdio.h>               // для sprintf 
    ^include <process.h>             // для _beginthreadex 
    ftinclude "Resource.H" 
    Illlllllllllllllllllllllllll/lllllllllllllllllllllll/llll/l/lllllllllllllllllll 
    I/ Глобальные переменные. 
    // g_fTerminate присваивается TRUE при закрытии диалогового окна. 
    // Это подсказывает рабочим потокам о необходимости завершения. 
    // Переменная помечена как volatile, поскольку ее значение может 
    // измениться в любой момент, 
    volatile BOOL g_fTerminate = FALSE: 
    HWND  g_hwnd; 
    HANDLE g_hThread[2]; // потоки Counter[0] и Oisplay[1] 
    
    // данные, подлежащие защите 
    TCHAR g_szNumber[10] = __TEXT("0"); 
    
    // мьютекс, используемый для защиты данных 
    HANDLE g_hMutex; 
    IIIIIIIIIIIHIIIIIIIIIIIflllllll/llllllllllllllllllllllllllllllllllllllllllllll 
    
    I / добавляем строку в окно списка 
    void AddToListBox (LPCTSTR szBuffer) < 
    HWND hwndDataBox = GetDlgItem(g_hwnd, IDC_OATABOX); 
    int x = ListBox_AddString(hwndDataBox, szBuffer); 
    ListBox_SetCurSel(hwndDataBox, x); 
    
    if (ListBox_GetCount(hwndDataBox) > 100) 
    ListBox_DeleteString(hwndDataBox, 0); 
    lllll/illllililll/lll//tll/llllll/llll/llll/ll/llllllllllllll/llllllllllllll/ll 

    см. след. стр. 

    Рис. 10-2. Листинг программы Mutexes 

    246 _______________________________Windows для профессионалов 

    РИС. 10-2 (продолжение) 
    
    II поток, который увеличивает счетчик в защищенных данных 
    DWORD WINAPI Counter-Thread (LPVOID IpThreadParameter) { 
    unsigned int nNumber, nDlgit; 
    BOOL fSyncChecked; 
    while (! g_rTerminate) { 
    
    // узнаем состояние флажка Synchronize и запоминаем его 
    fSyncChecked = IsDlgButtonChecked(g_hwnd, IDC_SYNCHRONIZE); 
    if (fSyncChecked) { 
    
    // если пользователь хочет синхронизировать нас, пусть синхронизирует 
    WaitFor-SingleObject(&g_hMutex, INFINITE): 
    
    // преобразуем символьное значение числа в целое значение и добавим 1 
    _stscanf(g_szNumber, __TEXT("%d"), &nNumber); 
    nNumber++; 
    
    // преобразуем новое число в строку 
    nDigit = 0; 
    while (nNumber != 0) { 
    // заносим разряд числа в строку 
    g_szNumber[nDigit++] = (TCHAR) (__ТЕХТ('0') + (nNumber % 10)): 
    // Вызов здесь функции Sleep сообщает системе, что неиспользованный 
    // остаток текущего кванта времени мы отдаем другому потоку. 
    // Этот вызов необходим в однопроцессорной системе для того, чтобы 
    // результаты синхронизации потоков или отсутствия таковой стали 
    // очевидны. Обычно функцию Sleep для этого, конечно, НЕ вызывают. 
    Sleep(O); 
    
    // готовимся получить следующий разряд 
    nNumber /= 10; 
    } 
    
    // все разряды преобразованы в строку; завершаем строку 
    g_szNumber[nDigit] = 0; 
    
    // Символы сформированы в обратном порядке; восстанавливаем 
    // нормальный порядок. Если ANSI, вызываем _strrev, а если 
    // Unicode, то - _wcsrev. 
    _tcsrev(g_szNumber); 
    if (fSyncChecked) { 
    // если пользователь хочет синхронизировать нас, пусть синхронизирует 
    ReleaseMutex(g_hMutex): 
    \ 
    см. след. стр. 


    __________________________Глава 10. Синхронизация потоков 247 


    РИС. 10-2 (продолжение) 
    
    // если пользователь хочет что-нибудь видеть на экране после 
    // каждой итерации, пусть видит 
    if (IsDlgButtonChecked(g_hwnd. IDC_SHOWCNTRTHRD)) 
    AddToListBox(__TEXT("Cnt г: Increment")); 
    return(O); // сюда мы попадаем при закрытии окна 
    } 
    '' •'••' ••'! /.' !/: i' i!,! / /!! 111111111111! I! 11111111111111! :!    
!ii •'.'     ':' 
    I / поток, который добавляет текущее значение счетчика 
    // (собственно данные) в окно списка 
    DWORD WINAPI DisplayThread (LPVOID IpThr-eadParameter) { 
    BOOL fSyncChecked; 
    TCHAR szBuffer[50J: 
    while (!g_fTerminate) { 
    
    // определяем: нужна ли синхронизация потоков 
    fSyncChecked = IsDlgButtonChecked(g_hwnd, IDC_SYNCHRONIZE); 
    if (fSyncChecked) 
    WaitForSingleObJect(&g_hMutex, INFINITE); 
    
    // формируем строку с символьным представлением числа 
    _stprintf(szBuffer, __TEXT("Dspy: %s"). g_szNumber); 
    if (fSyncChecked) 
    ReleaseMutex(&g_hMutex): 
    
    // добавляем строку в окно списка 
    AddToListBox(szBuffer); 
    } 
    return(O); // сюда мы попадаем при закрытии окна 
    } 
    
    BOOL Dlg_OnlnitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    HWND hWndGtl: 
    DWORD dwThreadID; 
    
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_MUTEXES, IDI_MUTEXES); 
    
    // Запоминаем описатель диалогового окна в глобальной переменной, 
    // чтобы потоки могли легко получить к ней доступ. Это нужно 
    // сделать до создания потоков. 
    g_hwnd = hwnd; 

    см. след. стр. 

    248 ______________________________Windows для профессионалов 


    РИС. 10-2 (продолжение) 

    I I Инициализируем обьект-мьютекс, Это нужно сделать 
    // до того, как к ней попытается обратиться какой-нибудь поток. 
    // Здесь необходима проверка на ошибки. 
    q_hMutex = CreateMutex(NULL, FALSE, NULL); 
    
    // создаем поток с функцией CounterThread и запускаем его 
    g_hThread[0] = chBEGINTHREADEX(NULL, О, 
    CounterThread, NULL, 0, &dwThreadID); 
    
    // создаем поток с функцией OisplayThread и запускаем его 
    g_hThread[1] = chBEGINTHREADEX(NULL, О, 
    DisplayThread, NULL, 0, &dwThreadID); 
    // заполняем комбинированный список Process Priority Class 
    // и выбираем Normal 
    hWndCt! = GetDlgItem(hwnd, IDC_PRIORITYCLASS); 
    ComboBox_AddString(hWndCtl, __TEXT("Idle")); 
    ComboBox_AddString(hWndCtl, __TEXT("Normal")): 
    ComboBox_AddString(hWndCtI, __TEXT("High")); 
    ComboBox_AddString(hWndCtl, __TEXT("Realtime")); 
    ComboBox_SetCurSel(hWndCtl, 1); // Normal 
    // заполняем комбинированный список Display Thread Priority 
    // и выбираем Normal 
    hWndCtI = GetDlgItem(hwnd, IDC_DSPYTHRDPRIORITY); 
    ComboBox_AddString(hWndCtl, __TEXT("Idle")); 
    ComboBox_AddStnng(hWndCtl, __TEXT(" Lowest")); 
    ComboBox_AddString(hWndCtl, „TEXT( "Below normal")); 
    ComboBox_AddString(hWndCtl, _TEXT("Normal")); 
    ComboBox_AddString(hWndCtl, __TEXT("Above normal")); 
    ComboBox_AddString(hWndCtl, __TEXT("Highest")); 
    ComboBox_AddString(hWndCtl, __TEXT("Timecritical")); 
    ComboBox_SetCurSel(hWndCtl, 3); // Normal 
    // заполняем комбинированный список Counter Thread Priority 
    // и выбираем Normal 
    hWndCtI = GetDlgItem(hwnd, IDC_CNTRTHRDPRIORITY); 
    ComboBox_AddString(hWndGtl, __TEXT("Idle")): 
    ComboBox_AddString(hWndCtl. __TEXT("Lowest")); 
    ComboBox_AddString(hWndCtl, __TEXT("Below normal")); 
    ComboBox_AddStnng(hWndCtl, __TEXT( "Normal")); 
    ComboBox_AddString(hWndCtl, __TEXT("Above normal")); 
    ComboBox_AddSt ring(hWndCtI, __TEXT("Highest")); 
    ComboBox_AddString(hWndCtl, __TEXT("Timecritical")); 
    ComboBox_SetCurSel(hWndCtl, 3); // Normal 
    return(TRUE); 
    } 
    llllllllllllllll//lll/llll/lllllll/IIIIHIIIIIIIIIIIIfll/lll^l/,i! /: 

    см. след. стр. 

    __________________________Глава 10. Синхронизация потоков 249 


    РИС. 10-2 (продолжение) 

    void Dlg_OnDestroy (HWND hwnd) { 
    
    // когда диалоговое окно закрывается, сообщаем потокам, что им 
    // надо завершиться 
    g_fTerminate = TRUE; 
    
    /У возобновляем выполнение потоков, если они приостановлены 
    ResumeThread(g_hThread[0]): 
    ResumeThread(g_hThread[1]); 
    i 
    
    /////////////////////////////////////////////////////////////////////////////// 
    void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    HANDLE hThread; 
    DWORD dw; 
    
    switch (id) { 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break; 
    case IDC_PRIORITYCLASS: 
    
    if (codeNotify != CBN_SELCHANGE) 
    break: 
    
    // пользователь меняет класс приоритета 
    switch (ComboBox_GetCurSel(hwndCtl)) { 
    case 0: 
    dw = IDLE_PRIORITY_CLASS; 
    break; 
    case 1: 
    default: 
    dw = NORMAL_PRIORITY_CLASS; 
    break; 
    case 2: 
    dw = HIGH_PRIORITY_CLASS: 
    break; 
    case 3: 
    dw = REALTIME_PRIORITY_CLASS; 
    break; 
    } 
    SetPriorityClass(GetCurrentProcess(), dw); 
    break; 
    case IDC_DSPYTHRDPRIORITY; 
    case IDC_CNTRTHRDPRIORITY: 

    см. след. стр. 


    250 _______________________________Windows для профессионалов 


    РИС. 10-2 (продолжение) 
    
    if (codeNotify != CBN_SELCHANGE) 
    break; 
    
    switch (ComboBox_GetCurSel(hwndCtl)) { 
    case 0: 
    dw = (DWORD) THREAD_PRIORITY_1DLE; 
    break; 
    case 1: 
    dw = (DWORD) THREAD_PRIORITY_LOWEST; 
    break; 
    case 2: 
    dw = (DWORD) THREAD_PRIORITY_BELOW_NORMAL; 
    break; 
    case 3: 
    default: 
    dw = (DWORD) THREAD_PRIORITY_NORMAL; 
    break; 
    case 4: 
    dw = (DWORD) THREAD_PRIORITY_ABOVE_NORMAL; 
    break; 
    dw = (DWORD) THREAD_PRIORITY__HIGHEST: 
    break; 
    case 6: 
    dw = (DWORD) THREAD_PRIORITY_TIME_CRITICAL; 
    break; 
    //' пользователь меняет относительный приоритет 
    // одного из потоков 
    hThread = (id == IDC_CNTRTHRDPRIORITY) ? g_hThread[0] : g_hThread[1]; 
    SetThreadPriority(hThread, dw); 
    break; 
    case IDC_PAUSE: 
    
    // пользователь приостанавливает или 
    // возобновляет оба потока 

    см. след. стр. 


    __________________________Глава 10. Синхронизация потоков 251 


    РИС. 10-2 (продолжение) 

    it (Bui:ton_GeLCneck(nwridCLl)} { 
    SuspendThread(g_hThread[0]); 
    SusреndThread(g_hThread[1]); 
    } else { 
    ResumeThread(g_hThread[0]); 
    ResumeThread(g_hThread[1]); 
    } 
    break; 
    BOOL CALLBACK 01g_Proc (HWND hwnd. UINT uMsg, WPARAM wPararn, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd. WM_INITOIALOG, Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd. WM_DESTROY, Dlg_OnDestroy); 
    chHANOLE_DLGMSG(hwnd. WM_COMMAND, Dlg_OnComniand); 
    > 
    return(FALSE); 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    chWARNIFUNICODEUNDERWIN95(); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_MUTEXES), NULL, Dlg_Proc); 
    
    // ждем завершения обоих потоков 
    WaitForMultipleOb]ects(2, g_hThread, TRUE, INFINITE); 
    
    // закрываем описатели потоков 
    CloseHandle(g_hThread[0]); 
    CloseHandle(g_hThread[1]); 
    
    // потоки больше не будут использовать мьютекс, 
    // поэтому его можно уничтожить 
    CloseHandle(g_hMutex); 
    return(O); 
    [ 
    IIIIIIIIIIIIIIIIIHIiHIIIIIIIIII/ конец файла 11111111111111111 I! 11111111 111111 


    252 ______________________________Windows для профессионалов 


    Семафоры 

    Объекты ядра «семафор» используются для учета ресурсов. Они позволяют 
потоку запрашивать число доступных ресурсов. Если один или более ресурсов 
свободны, счетчик доступных ресурсов после запроса уменьшается на 1. Семафоры 
осуществляют такую операцию на атомном уровне. Иными словами, когда Вы 
запрашиваете у семафора ресурс, операционная система сама проверяет, свободен 
ли данный ресурс, и уменьшает счетчик доступных ресурсов, исключая 
вмешательство другого потока. Только после уменьшения счетчика доступных 
ресурсов система разрешает другому потоку запрашивать какой-либо ресурс. 
    Допустим, у компьютера 3 последовательных порта, их могут использовать не 
более трех потоков одновременно, и каждый порт можно закрепить за одним 
потоком. Эта ситуация — отличная возможность применить семафор. Чтобы 
отслеживать занятость последовательных портов, Вы создаете семафор со 
счетчиком, равным 3 (ведь последовательных портов у Вас именно 3). При этом 
нужно учесть, что семафор считается свободным, если его счетчик больше 0, и 
занятым, если счетчик равен 0 (меньше 0 он быть не может). При каждом вызове 
из потока WaitForSingkObject с передачей ей описателя семафора система 
проверяет: больше ли 0 счетчик ресурсов у данного семафора? Если да, уменьшает 
счетчик на 1 и «будит» поток. Если при вызове WaitForSingleObject счетчик 
семафора равен 0, система оставляет поток неактивным до того, как другой поток 
освободит семафор (т. е. увеличит его счетчик ресурсов). 
    Поскольку на счетчик ресурсов семафора могут влиять несколько потоков, 
семафоры (в отличие от критических секций или мьютексов) не передаются во 
владение какому-либо потоку. И значит, один поток может ждать объект «семафор» 
(уменьшив его счетчик ресурсов), а другой — освободить семафор (тем самым 
увеличив его счетчик). 
    Семафор создается вызовом CreateSemaphore: 

    HANDLE CreateSemaphore(LPSECURITY_ATTRIBUTE Ipsa, 
    LONG cSemInitial, LONG cSemMax, LPTSTR IpszSemName); 

    Эта функция создает семафор, максимальное значение счетчика которого может 
достигать cSemMax. В нашем примере в этот параметр следовало бы поместить 
число 3 (три последовательных порта). Параметр cSemInitial позволяет задать 
начальное состояние счетчика. При запуске системы все последовательные порты 
будут свободны, значит, и сюда надо занести число 3. Но если при инициализации 
операционной системы Вы хотите указать, что все они заняты, введите в этот 
параметр 0. 
    Последний параметр функции, IpszSemName, присваивает семафору имя в 
виде_строки. В дальнейшем это имя позволит получить описатель семафора из 
других процессов с помощью CreateSemaphore или OpenSemaphore: 

    HANDLE OpenSemaphore(DWORD fdwAccess, BOOL finherit, LPTSTR IpszName): 

    По семантике эта функция идентична уже рассмотренной OpenMutex. 
    Чтобы перевести семафор в свободное состояние (увеличить его счетчик 
ресурсов), вызовите ReleaseSemaphore: 

    BOOL ReleaseSemaphore(HANDLE hSemaphore, LONG cRelease, LPLONG IplPrevious); 

    Она похожа на ReleaseMutex, но имеет ряд отличий. Во-первых, любой поток 
может вызвать ее когда угодно, поскольку объекты «семафор» не принадлежат лишь 
какому-то одному потоку. Во-вторых, с ее помощью счетчик ресурсов можно 
увеличивать более чем на 1 единовременно. Параметр cRelease как раз и 
определяет, какими порциями должен освобождаться семафор. Например, у нас есть 
программа, копирующая данные из одного последовательного порта в другой. Она 
должна дважды запрашивать ресурсы у семафора, соответ

    __________________________Глава 10. Синхронизация потоков 253 

    ственно дважды вызывая WaitForSingleObject. Но освободить оба ресурса 
программа может через один вызов ReleaseSemaphore. Вот как это делается: 
    
    // получаем два последовательных порта 
    WaitForSingleObject(g_hSemSerialPort, INFINITE): 
    WaitForSingleObject(g_hSemSerialPort, INFINITE): 
    // чего-то делаем с ними 
    
    // освобождаем последовательные порты, чтобы и другие 
    // приложения могли ими попользоваться 
    ReIeaseSemaphore(g_hSemSerialPort, 2, NULL): 

    Было бы неплохо, если бы мы могли вместо двухкратного вызова 
WaitForSingleObject один раз обратиться к WaitForMultipleObjects. Но последняя 
не разрешает в одном вызове дважды использовать одинаковый описатель. 
Утешьтесь тем, что хоть счетчик семафора можно сразу увеличить до нужного значения. 
    Последний параметр функции ReleaseSemaphore— IplPrevious— указатель на 
переменную типа LONG, в которой возвращается значение счетчика ресурсов, 
предшествующее тому, что получилось после его увеличения на cRelease. Если Вас 
не интересует это значение, передайте в параметре IplPrevious значение NULL. 
    Было бы удобнее работать с \Уш32-функцией, способной определять у семафора 
состояние счетчика, не меняя его значения. Поначалу я думал, что вызовом 
ReleaseSemaphore с передачей ей во втором параметре нуля можно узнать истинное 
значение счетчика в переменной типа LONG, на которую указывает параметр 
IplPrevious. Но не вышло; функция занесла туда 0. Я передал во втором 
параметре заведомо большее число, и — тот же результат. Тогда мне стало ясно: 
получить значение_саетчика, не изме1шв_его,невозможно. 

    Программа-пример «Супермаркет» 

    Программа SprMrkt (SPRMRKT.EXE) (см. листинг на рис. 10-4) демонстрирует 
управление моделью супермаркета с помощью объектов-мьютексов и семафоров. 
Файлы исходного кода и ресурсов находятся в каталоге SPRMRKT на компакт-диске, 
прилагаемом к книге. После запуска программы на экране появится диалоговое окно: 

    254 _______________________________Windows для профессионалов 

    Это диалоговое окно позволяет установить начальные параметры, необходимые 
при моделировании супермаркета. Закончив настройку, щелкните кнопку Open For 
Business (открыть супермаркет) — тем самым Вы создадите поток, моделирующий 
супермаркет, и начнете его исполнение. Функция, идентифицирующая этот поток, 
называется ThreadSuperMarket. 
    Данный поток отвечает за следующие операции: 
    1. Открытие супермаркета. 
    2. Создание потоков, представляющих отдельных покупателей. 
    3. Закрытие центрального входа по окончании работы супермаркета (после 
этого в него не могут входить новые покупатели). 
    4. Ожидание оставшихся в супермаркете покупателей, которые заканчивают 
расплачиваться и постепенно покидают магазин. 
    5. Уведомление GUI-потока (или первичного потока) об окончании 
моделирования — чтобы он вновь активизировал элементы управления в диалоговом 
окне на случай изменения параметров и нового запуска модели. 
    .Время от времени поток-супермаркет создает новый поток-покупатель, для 
чего вызывает функцию _ begin threadex-.                             "'" 

    hThread = (HANDLE) chBEGINTHREADEX { 
    NULL,                  // атрибуты защиты 
    О,                     // стек 
    ThreadShopper,         // функция потока 
    (LPVOID) ++nShopperNum,// номер покупателя как IpvParam 
    0.                     // флаги 
    &i1wThrpadTcl) •           // идентификатоп потока 
    CloseHandle(hThread); 

    Вызов CloseHandle сообщает системе, что поток-супермаркет не ссылается 
напрямую на поток-покупатель. После создания им потока-покупателя тот 
приступает к исполнению своего кода. Когда покупатель, расплатившись за 
покупки, выходит из супермаркета, соответствующий ему поток завершается, а 
сопоставленный с ним объект ядра разрушается. 
    Уверен, теперь Вы уже догадываетесь, что могло бы произойти, забудь я 
вызвать CloseHandle. Правильно: утечка ресурсов. Ведь акт создания потока дает 
новый объект «поток» с начальным значением счетчика числа его пользователей, 
равным 1. Далее, поскольку макрос chBEGINTHREADEX3 возвращает описатель 
объекта «поток», счетчик этого объекта увеличивается до 2. Когда покупатель 
уходит из супермаркета (т. е. его поток завершается), счетчик числа 
пользователей этого объекта «поток» уменьшается до 1. Если бы не было 
обращения к CloseHandle, счетчик никогда бы не обнулился, и система не закрыла 
бы объект «поток» вплоть до завершения всего процесса. А так как 
потоки-покупатели создаются достаточно часто да еще и пользователь может 
запускать модель по несколько раз, не выходя из программы, то количество 
неосвобожденных объектов «поток» могло бы достичь критических пределов. Таким 
образом, вызов CloseHandle действительно необходим. 
    Поток-супермаркет создает потоки-покупатели через случайные промежутки 
времени. Максимальный интервал указывается в диалоговом окне на полосе 
прокрутки Shopper Create Delay (задержка перед созданием нового «покупателя»). 
    Располагая потоком, моделирующим супермаркет, и группой отдельных потоков, 
моделирующих покупателей, мы создаем впечатление, что и супермаркет, и каждый 
из покупателей — все действуют сами по себе. 
    3. chBEGINTHREADEX— макрос, просто вызывающий _beginthreadex из 
стандартной библиотеки С. Но предварительно он осуществляет приведение типов, 
чтобы угодить компилятору. Этот макрос определен в заголовочном файле 
CmnHdr.h, который содержится на компакт-диске, прилагаемом к книге. Подробнее 
см. приложение А. 

    ___________________________Глава 10. Синхронизация потоков 255 

    «Покупатели» выполняют следующие операции: 
    1. Ждут входа в магазин. 
    2. Делают случайное количество покупок. 
    3. Подходят к стойкам закусочной и заказывают что-нибудь перекусить. 
    4. Становятся в очередь к кассе. 
    5. Проводят у кассы случайный период времени. 
    6. Отходят от кассы. 
    7. Уходят из супермаркета. 
    После ухода покупателя из супермаркета его поток завершается. 
    В процессе работы модели окно списка Shopper Events (события, происходящие 
с покупателями) информирует Вас о том, что делается в супермаркете. Изучая эту 
информацию, Вы сможете выделить в магазине «узкие места» и наметить пути их 
ликвидации (изменив соответствующие параметры перед следующим прогоном 
модели). В итоге менеджер супермаркета, взяв полученные данные, сможет 
определить оптимальное число открытых касс и подобрать нужное количество 
обслуживающего персонала за стойками закусочной. На рис. 10-3 показаны 
результаты прогона модели с параметрами, приведенными на предыдущей иллюстрации. 

    ---> Opening the supermarket to shoppers. 
    0001: Waiting to get in store (11), 
    0002: 'WaTting to get in store (16). 
    0001: In supermarket, shopping for 46. if 
    0002: In supermarket, shopping for 38. 
    0003: Waiting to get in store (17). 
    0003: In supermarket, shopping for 65. 
    0002: Not going to to the deli counter. 
    0001: Waiting for service at deli counter (17). -• 
    0002: Waiting for an empty checkout counter. 
    0001: Being served at deli (49). 
    0002: Checking out (30). 
    0003: Waiting for service at deli counter (0). 
    0003: Tyred of waiting at deli. 
    0004; Waiting to get in store (7). 
    0002: Leaving checkout counter. 
    0001: Waiting for an empty checkout counter. 
    0005: Waiting to get in store (3). 
    0003: Waiting for an empty checkout counter. 
    0004: In supermarket, shopping for 9, 
    0006: Waiting to get in store (8). 
    0002: Left the supermarket. 
    0001: cHecking out (0). 
    0005: In supermarket, shopping for 0. 
    0003: Checking out (22). 
    0006: In supermarket, shopping for 42. 
    0004: Not going to the deli counter. 
    0001: Leaving checkout counter. 
    0005: Not going to the deli counter. 

    см. след. стр. 

    Рис. 10-3. Результаты моделирования с использованием параметров, заданных 
в диалоговом окне на предыдущей иллюстрации 

    256 _______________________________Windows для профессионалов 

    РИС. 10-3 (продолжение) 

    0003: Leaving checkout counter. 
    0004: Waiting for an empty checkout counter. 
    0001: Left the supermarket. 
    0005: Waiting for an empty checkout counter. 
    0004: Checking out (36). 
    0003: Left the supermarket. 
    0006; Waiting for service at deli counter (13). 
    0007: Waiting to get in store (7). 
    0005: Checking out (3). 
    0006; Being served at deli (42). 
    0004: Leaving checkout counter. 
    0007: In supermarket, shopping for 41. 
    0008: Waiting to get in store (16). 
    0005: Leaving checkout counter. 
    0006: Waiting for an empty checkout counter, 
    0005: Left the supermarket, 
    0004: Left the supermarket. 
    0008: In supermarket, shopping for 24. 
    0007: Not going to the deli counter. 
    0006: Checking out (43). 
    0007: Waiting for an empty checkout counter. 
    0008: Not going to the deli counter. 
    0007: Checking out (8). 
    0009: Waiting to get in store (10). 
    0008: Waiting for an empty checkout counter. 
    0008: Checking out (27). 
    0006: Leaving checkout counter. 
    0009; In supermarket, shopping for 18. 
    0007: Leaving checkout counter. 
    0006: Left the supermarket, 
    0007: Left the supermarket. 
    0009: Not going to the deli counter. 
    0008: Leaving checkout counter. 
    0008: Left the supermarket. 
    0009: Waiting for an empty checkout counter. 
    0009: Checking out (46). 
    0010: Waiting to get in store (16). 
    0010: In supermarket, shopping for 79. 
    0009; Leaving checkout counter. 
    0009: Left the supermarket. 
    0011: Waiting to get in store (12), 
    ООП: In supermarket, shopping for 31. 
    0010: Waiting for service at deli counter (5), 
    ООП: Not going to the deli counter. 
    0010; Being served at deli (1). 
    ООП: Waiting for an empty checkout counter. 
    0010: Waiting for an empty checkout counter. 
    0010: Checking out (22). 
    ООП: Checking out (5). 

    см. след. стр. 

    _________________________Глава 10. Синхронизация потоков 257 


    РИС. 10-3 (продолжение) 

    0012: Waiting to get in store (1). 
    0011: Leaving checkout counter 
    0010: Leaving checkout counter. 
    0012; In supermarket, shopping for 0. 
    ООП: Left the supermarket. 
    0010: Left the supermarket. 
    0012: Not going to the deli counter. 
    0013: Waiting to get in store (5). 
    0012: Waiting for an empty checkout counter. 
    0012: Checking out (35), 
    0013: In supermarket, shopping for 55. 
    0014: Waiting to get in store (14), 
    0014: In supermarket, shopping for 38. 
    0012: Leaving checkout counter. 
    0013: Waiting for service at deli counter (14). 
    0013: Being served at deli (32). 
    0012: Left the supermarket. 
    0014: Waiting for service at dell counter (18). 
    0013: Waiting for an empty checkout counter. 
    0014: Tired of waiting at deli. 
    0014: Waiting for an empty checkout counter. 
    0015: Waiting to get in store (2). 
    0013: Checking out (35). 
    0014: Checking out (23). 
    0015; In supermarket, shopping for 58. 
    0013: Leaving checkout counter. 
    0013: Left the supermarket. 
    0014: Leaving checkout counter. 
    0015: Not going to the deli counter, 
    0014: Left the supermarket. 
    0015; Waiting for an empty checkout counter, 
    0016: Waiting to get in store (7). 
    0015: Checking out (9). 
    0016; In supermarket, shopping for 18. 
    0015: Leaving checkout counter. 
    0015: Left the supermarket. 
    0016; Waiting for service at deli counter (16). 
    0016: Being served at deli (36). 
    0017: Waiting to get in store (15). 
    0016; Waiting for an empty checkout counter. 
    0017: In supermarket, shopping for 27. 
    0016: Checking out (10), 
    0017: Not going to the deli counter. 
    0016: Leaving checkout counter. 
    0017: Waiting for an empty checkout counter. 
    0017: Checking out (29). 
    0016: Left the supermarket. 
    0017: Leaving checkout counter. 
    0017: Left the supermarket. 

    см. след. стр. 

    258 _______________________________Windows для профессионалов 


    РИС. 10-3 (продолжение) 

    0018; Waiting to get in store (13). 
    0018: In supermarket, shopping for 75. 
    0019: Waiting to get in store (2). 
    0019; In supermarket, shopping for 11. 
    0019: Not going to the deli counter. 
    0018: Not going to the deli counter. 
    0020: Waiting to get in store (8). 
    0019: Waiting for an empty checkout counter. 
    0018: Waiting for an empty checkout counter. 
    0019: Checking out (4). 
    0020: In supermarket, shopping for 54. 
    0021: Waiting to get in store (3). 
    0018: Checking out (52). 
    . 0019: Leaving checkout counter, 
    0021: In supermarket, shopping for 65, 
    0019: Left the supermarket. 
    0020: Not going to the deli counter. 
    0020: Waiting for an empty checkout counter. 
    0018; Leaving checkout counter. 
    0021: Waiting for service at deli counter (3). 
    : 0020: Checking out (49). 
    0018: Left the supermarket. 
    0021: Being served at deli (35). 
    0022: Waiting to get in store (3). 
    0020: Leaving checkout counter, 
    0020: Left the supermarket, 
    0021: Waiting for an empty checkout counter. 
    0022: In supermarket, shopping for 58. 
    0023: Waiting to get in store (5). 
    0021: Checking out (34). 
    0023: In supermarket, shopping for 54, 
    0022: Not going to the deli counter. 
    0024: Waiting to get in store (9). 
    0021: Leaving checkout counter. 
    0023: Waiting for service at deli counter (7). 
    0022; Waiting for an empty checkout counter. 
    0024: In supermarket, shopping for 66. 
    0021: Left the supermarket. 
    0023: Being served at deli (2). 
    0022: Checking out (31), 
    0023: Waiting for an empty checkout counter. 
    0022: Leaving checkout counter. 
    0024: Not going to the deli counter. 
    0023: Checking out (56). 
    0025: Waiting to get in store (2). 
    0022: Left the supermarket. 
    0024: Waiting for an empty checkout counter. 
    0025: In supermarket, shopping for 73. 
    0024: Checking out (32), 

    см. след. стр. 

    ___________________________Глава 10. Синхронизация потоков 259 


    РИС. 10-3 (продолжение) 

    0023: Leaving checkout counter. 
    0026: Waiting to get in store (9). 
    0023: Left the supermarket. 
    0025; Waiting for service at deli counter (16). 
    0024: Leaving checkout counter. 
    0026: In supermarket, shopping for 21. 
    0027: Waiting to get in store (9). 
    0025: Being served at deii (68). 
    0024: Lett the supermarket. 
    0027: In supermarket, shopping for 45. 
    0028: Waiting to get in store (14). 
    0026: Waiting for service at deli counter (15). 
    0025: Waiting for an empty checkout counter. 
    0028: In supermarket, shopping for 67. 
    0026: Being served at deii (27). 
    0029: Waiting to get in store (17). 
    0027: Waiting for service at deli counter (19). 
    0025: Checking out (34). 
    0026: Waiting for an empty checkout counter. 
    0029: In supermarket, shopping for 4. 
    0028: Not going to the deli counter. 
    ---> Waiting for shoppers to check out so store can close. 
    0027: Being served at deli (13). 
    0025: Leaving checkout counter, 
    0026; Checking out (50), 
    ---> 0 shoppers NOT in store. 
    0028: Waiting for an empty checkout counter. 
    0029; Not going to the deli counter. 
    0027: Waiting for an empty checkout counter, 
    ---> 1 shoppers NOT in store. 
    0025: Left the supermarket. 
    0028: Checking out (39). 
    0027: Checking out (11), 
    0029: Waiting for an empty checkout counter. 
    0026: Leaving checkout counter. 
    ---> 2 shoppers NOT in store. 
    0027: Leaving checkout counter. 
    ---> 3 shoppers NOT in store. 
    0029: Checking out (50). 
    0026: Left the supermarket. 
    0028: Leaving checkout counter. 
    0027: Left the supermarket. 
    ---> 4 shoppers NOT in store, 
    0028: Left the supermarket. 
    0029: Leaving checkout counter. 
    ---> 5 shoppers NOT in store. 
    0029: Left the supermarket. 
    ---> 6 shoppers NOT in store. 
    -"> 7 shoppers NOT in store, 


    см. след. стр. 

    260 _______________________________Windows для профессионалов 

    PUC. 10-3 (продолжение) 

    ---> 8 shoppers NOT in store. 
    ---> 9 shoppers NOT in store. 
    ---> 10 shoppers NOT in store. 
    ---> 11 shoppers NOT in store. 
    ---> 12 shoppers NOT in store. 
    ---> 13 shoppers NOT in store, 
    ---> 14 shoppers NOT in store. 
    ---> 15 shoppers NOT in store. 
    ---> 16 shoppers NOT in store. 
    ---> 17 shoppers NOT in store, 
    ---> 18 shoppers NOT in store. 
    ---> 19 shoppers NOT in store. 
    ---> 20 shoppers NOT in store. 
    ---> 21 shoppers NOT in store. 
    ---> 22 shoppers NOT in store. 
    ---> 23 shoppers NOT in store. 
    ---> 24 shoppers NOT in store. 
    ---> 25 shoppers NOT in store, 
    ---> 26 shoppers NOT in store. 
    ---> 27 shoppers NOT in store. 
    ---> 28 shoppers NOT in store. 
    ---> 29 shoppers NOT in store. 
    ---> Store closed—end of simulation. 

    Теперь представим, что супермаркет открылся и в него вошло несколько 
покупателей. При этом, как Вы понимаете, может происходить столько всего, что 
нужно как-то синхронизировать выполняемые потоки. В этом примере используется 
несколько видов синхронизации. 
    Начиная исполнение, поток-супермаркет тут же создает объект «семафор», 
идентифицируемый глобальной переменной g_hSemEntrance: 

    g_hSemEntrance = CreateSemaphore( 
    NULL,               // атрибуты защиты 
    О,                  // начальное состояние счетчика блокировки входа 
    g_nMaxOccupancy,    // максимальное количество покупателей, которое 
    // может находиться в магазине 
    NULL):              // семафор без имени 

    Этот объект предназначен для отслеживания максимального числа покупателей, 
которых может одновременно обслужить магазин. Оно указывается в диалоговом 
окне на полосе прокрутки Maximum Occupancy. Какое-то — очень короткое — время 
после начала работы входные двери супермаркета остаются закрытыми, не пуская 
покупателей в магазин. В данном случае этот период принимается равным 0 
(счетчик блокировки входа обнулен). Когда магазин готов принять покупателей, 
его поток вызывает: 

    ReleaseSemaphore(g_hSemEntrance, g_nMaxOccupancy, NULL): 

    А когда создается поток-покупатель, то первое, что он делает, — вызывает 
функцию WaitForSingleObject 

    dwResult = WaitForSingleObject(g_hSemEntrance, nDuration); 

    Если в магазине уже находится g_nMaxOccupancy покупателей, данный 
поток-покупатель приостанавливается. А если число покупателей еще не достигло 
максимума (g_nMaxOccupancy), 

    _________________________Глава 10. Синхронизация потоков 261 

    WaitForSingleObject сразу возвращает управление, разрешая новому 
покупателю войти в магазин. Счетчик семафора в этом случае также уменьшается, 
отмечая, что одним ресурсом стало меньше. 
    Вы заметили, что в последнем фрагменте кода я поставил параметр 
nDuratiori? Он позволяет задать максимальное время ожидания входа в магазин, 
по истечении которого покупатель уходит домой. Значение этого параметра можно 
указать в диалоговом окне на полосе прокрутки Wait To Get In Market (ожидание 
на входе в магазин). Если покупателю надоест ждать очереди на входе, 
WaitForSingleObject вернет значение WAIT_TIMEOUT. Поток-покупатель помещает 
уведомление об этом событии в окно списка Shopper Events и завершается. 
    Войдя в магазин, посетитель что-то покупает. Максимальную 
продолжительность этого процесса можно установить на полосе прокрутки Time To 
Shop (время, отведенное на покупки). Этот процесс реализуется в 
потоке-покупателе самым элементарным образом: он просто вызывает Sleep и 
передает ей время, отведенное на покупки. 
    Выбрав товары по душе (возврат из функции Sleep), клиент направляется к 
стойке закусочной и берет что-нибудь перекусить (не знаю, как он, а я бы от 
ростбифа не отказался). Но, как мне подсказал один знакомый, многие, заходя в 
супермаркет, даже близко не подходят к закусочным. Поэтому в программе каждый 
клиент может подойти к стойке с вероятностью «один к трем» (мне больше достанется). 
    Если покупателю повезет и он все же подойдет к стойке, ему придется 
подождать. В данном варианте модели стойка обслуживается одним официантом. 
Поэтому синхронизация потоков-покупателей с потоком-официантом осуществляется 
с помощью объекта-мьютекса. В каждый момент его может занять лишь какой-то 
один поток-покупатель. Если клиент подходит к стойке в то время, как официанта 
ожидает другой клиент, только что прибывший становится в очередь и ждет, когда 
первый закончит свои дела за стойкой, т. е. освободит объект-мьютекс. После 
этого мьютекс переходит в распоряжение второго клиента, его поток «садится за 
стойку», вызывает функцию Sleep и засыпает на случайный период времени, 
максимальное значение которого определяется на полосе прокрутки Time At Deli 
Counter (время, проводимое за стойкой закусочной). 
    Существует также вероятность, что посетитель засидится за стойкой, и тогда 
ждущий своей очереди может потерять терпение и уйти. Максимальное время 
ожидания в этом случае регулируется на полосе прокрутки Wait For Deli Counter 
(время ожидания у стойки). 
    В этой части модели две проблемы. Во-первых, стойку обслуживает всего один 
официант. Здесь надо бы добавить параметр, который позволит управлять 
количеством официантов. Тогда удалось бы обслуживать сразу несколько 
посетителей. Если Вы решите поступить именно так, Вам будет достаточно 
заменить объект-мьютекс, контролирующий доступ к стойке, на семафор, 
максимальное значение счетчика у которого было бы адекватно количеству 
обслуживающего персонала. Я предпочел этого не делать, так как хотел еще раз 
проиллюстрировать работу с мьютексами. 
    Вторая проблема: покупатели вовсе не обязательно обслуживаются в том 
порядке, в каком они появились у стойки. Иначе говоря, если покупатель 1 уже 
обслуживается официантом, а затем прибывает покупатель 2 и спустя несколько 
мгновений — покупатель 3, то, когда первый покинет закусочную, потоки 2 и 3 
по-прежнему ждут объект-мьютекс. Система не дает никакой гарантии, что 
предоставит мьютекс сначала покупателю 2 только из-за того, что он первый в 
очереди. Поэтому, если Вы хотите синхронизировать потоки с учетом сказанного, 
Вам придется самостоятельно разработать логику этого процесса. Ни операционная 
система, ни Win32 API не предусматривают средств для автоматического 
соблюдения очередности. 
    Следующий шаг посетителя — что бы он ни делал у стойки — ожидание в 
очереди к кассе. Число касс в супермаркете определяется на полосе прокрутки 
Checkout Counters (число касс). 

    262 ____________________________Windows для профессионалов 

    Тут появляется еще одно место, где модель несколько расходится с жизнью. В 
скольких супермаркетах я ни был, везде покупатели стараются выбрать такую 
очередь к кассе, в которой народа поменьше и которая движется побыстрее. 
Последнее зависит от количества предметов на тележках у посетителей. И, кроме 
того, всегда стараешься встать к кассирше поприветливее. 
    В модели супермаркета степень детализации, конечно, ниже. Здесь ожидание у 
кассы больше похоже на то, как школьник тянет руку, надеясь, что учитель 
спросит именно его. Когда учитель задает вопрос, все школьники, которым 
кажется, что они знают ответ, поднимают руки. Но учитель выбирает лишь одного 
— совершенно случайным образом. В модель супермаркета заложено фиксированное 
количество касс. Они защищены семафором, созданным потоком, имитирующим супермаркет: 

    g_hSeniCheckout = CreateSemaphore( 
    NULL,                  // атрибуты защиты 
    g_nCheckoutRegisters,  // все кассы свободны 
    g_nCheckoutRegisters,  // количество касс в магазине 
    NULL);                 // семафор без имени 

    Когда покупатель готов рассчитаться за покупки, его поток ждет этот 
семафор. Если касса свободна, покупатель немедленно расплачивается, а счетчик 
семафора уменьшается на 1. Если все кассы заняты, покупателю приходится 
подождать. Я построил модель так, что покупатель не взбесится от слишком 
долгого ожидания, не бросит покупки и не уйдет из супермаркета. Как только 
поток-покупатель впадает в состояние ожидания, он ждет уже до победного конца: 

    WaitForSingleObject(g_hSemCheckout, INFINITE); 

    Когда покупатель оказывается у кассы, какое-то время уходит на считывание 
кодовых меток с товаров. Этот период ожидания тоже имитируется функцией Sleep, 
которой передается случайное число, а его максимальное значение задается 
полосой прокрутки Time At Checkout (время расчета у кассы). 
    Расплатившись, клиент отходит от кассы, а его поток освобождает семафор g_hSemCheckout 

    ReieaseSemaphore(g_hSemCheckout, 1, NULL); 

    Это позволяет другому покупателю, ждущему своей очереди, подойти к кассе. 
Расплатившийся покупатель должен покинуть супермаркет, освободив и семафор g_hSemEntrance: 

    ReleaseSemaphore(g_hSemEntrance, 1, NULL); 

    Данная строка сообщает семафору, контролирующему доступ покупателей в 
супермаркет, что один посетитель ушел и поэтому можно впустить следующего. 
Ушедший из магазина покупатель свою миссию выполнил — его поток завершается. 
    Ну что ж, думаю, мы вдоволь наговорились о потоках-покупателях — вернемся 
к потоку-супермаркету. Как я уже упоминал, поток-супермаркет отвечает за 
случайное создание потоков-покупателей. Но супермаркет работает не 
круглосуточно, часы его работы регулируются полосой прокрутки Time Open (время работы). 
    Обнаружив, что заданные часы работы истекли, поток-супермаркет прекращает 
создавать новых покупателей. Однако супермаркет не закрывается, пока его 
персонал не обслужит всех оставшихся в нем клиентов. Поэтому в потоке 
супермаркета исполняется цикл: 

    for (nMaxOccupancy = 0; 
    nMaxOccupancy < g_nMaxOccupancy; nMaxOccupancy++) { 
    WaitForSingleObject(g_hSemEntrance, INFINITE): 
    } 

    Здесь просто-напросто периодически вызывается WaitForSingleObject до тех 
пор, пока поток-супермаркет не захватит входной семафор ^nMaxOccupancy раз; а 
это может случить

    ___________________________Глава 10. Синхронизация потоков 263 

    ся лишь после того, как последний покупатель покинет магазин. Тут тоже 
может возникнуть проблема, если поток-покупатель будет создан перед самым 
закрытием супермаркета. В этом случае семафор будут ждать и поток-супермаркет, 
и поток-покупатель. Система, как Вы помните, не гарантирует, какой именно 
поток займет освободившийся семафор. Таким образом, есть вероятность, что 
последний покупатель войдет даже после закрытия магазина. 
    Нельзя исключить и того, что поток-супермаркет захватит семафор, так 
сказать, окончательно и бесповоротно. В этом случае любые созданные, но не 
успевшие «войти» потокипокупатели без толку прождут своей очереди и, устав от 
ожидания, завершатся. Для подготовки более реалистичной модели нужно 
разобраться и с этими шероховатостями. 
    Конечно, гораздо удобнее было бы всего раз вызвать из потока-супермаркета 
функцию WaitForMultipleObjects, а. не обращаться периодически к 
WaitForSingleObject. Но это невозможно по двум причинам._Во-первых, описатель 
единственного объекта нельзя передать в WaitForMultipleObjects более одного 
раза. Во-вторых, эта функция позволяет ждать не более MAXIMUM_WAIT_OBJECTS (т. 
е. 64) объектов. Для нас этого маловато, и в данной модели 
WaitForMultipleObjects бесполезна. 
    Захватив входной семафор g_nMaxOccupancy раз, поток-супермаркет делает ряд 
вызовов, чтобы система уничтожила все синхронизирующие объекты: 

    CloseHandle(g_hSerriCheckout): 
    CloseHandle(g_hMtxOeliCntr); 
    CloseHandle(g_hSemEntrance); 
    SPRMRKT.C 
    Модуль: SprMrkt.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    Sinclude "..\CmnHdr.H"           /* см. приложение В */ 
    ftinclude <windows,h> 
    ftinclude <windowsx.h> 
    #include <tchar.h> 
    ffinclude <stdio,h> 
    #include <stdlib.h>              // для генерации случайных чисел 
    #lnclude <string.h> 
    #include <stdarg.h> 
    ftinclude <process.h>             // для _beginthreadex 
    #include "Resource.H" 
    //' это необходимо для коррекции "жучка" в windowyx.n 
    #undef FORWARD_WM_HSCROLL 
    ffdefine FORWARD_WM_HSCROLL(hwnd, hwndGtl, code, pos, fn) \ 
    (void)(fn)((hwnd), WM_HSCROLL, \ 
    MAKEWPARAM((UINT)(code),(UINT)(pos)), \ 
    (LPARAM)(UINT)(hwndCtl)) 
    /// •ii/llllllll/lll/llllllliti //a. .'/у/у,1'//,',-1/ •1 УУ - yy/y •    • 
У ' i'1 
    см. след. стр. 
    Рис. 10-4. Листинг программы SprMrkt 

    264 ______________________________Windows для профессионалов 
    РИС. 10-4 (продолжение) 
    II упреждающие ссылки на функции потоков, моделирующих супермаркет 
    // и покупателей 
    DWORD WINAPI ThreadSuperMarket (LPVOID IpvParam); 
    DWORD WINAPI ThreadShopper (LPVOID IpvParam); 
    /' ' I ! ! 1 / / ! / 1 ' I I !' I I ! ! I I I I I / ! / I I ' / I I I I ! 
I I ! I / I ! / / I I / ' ! I ' ' / •' / ! •' / ':    ,  ! •' ,;'1, .' •'    1 
,','•'•   •                             ':' 
    // глобальные переменные 
    
    HWND g_hwndLB = NULL;  // окно списка для отображения событий, 
    // происходящих с покупателями 
    
    // параметры модели, устанавливаемые пользователем 
    int g_nMaxOccupancy: 
    
    int g_nTimeOpen;                                                           
                  ' 
    int g_nCheckoutCounters;                                                   
                 :: 
    int g_nMaxDelayBetweenShopperCreation: 
    int g_nMaxWaitToGetInMarket; 
    int g_nMaxTime8hopping; 
    int g_nMaxWaitForDeliCntr; 
    int g_nMaxTimeSpentAtDeli; 
    int g_nMaxTimeAtCheckout: 
    
    // синхронизирующие объекты, коюрые контролируют процесс моделирования 
    HANDLE g_hSemEntrance; 
    HANDLE g_hMtxDeliCntr; 
    HANDLE g_hSernCheckout; 
    // эта функция (с переменным числом аргументов) формирует строку. 
    // используя переданный формат, и добавляет ее в окно списка, на которое 
    // указывает глобальная переменная g_hwndLB 
    void AddStr (LPCTSTR szFnit, .,.) { 
    TCHAR szBuf[150]; 
    int nindex: 
    va_list va_params; 
    
    // делаем так, чтобы va_params указывала на первый аргумент после szFmt 
    va_start(va_params, szFmt); 
    // формируем отображаемую строку 
    _vstprintf(szBuf, szFmt, va_params);                                       
              .1 
    do {                                                                       
                        'I 
    // добавляем строку в конец списка 
    nindex = ListBox_AddString(g_hwndLB, szBuf); 
    // если окно списка заполнено, удаляем из него первый элемент 
    if (nindex == LB_ERR) 
    ListBox_DeleteString(g_hwndLB, 0); 
    } while (nindex ==.LB.ER.R); 


    __________________________Глава 10.  Синхронизация потоков 265 


    РИС. 10-4 (продолжение) 
    
    / I выделяем подсветкой только что добавленный элемент 
    ListBox_SetCurSel(g_hwndLB, nindex): 
    // сообщаем, что прекратили ссылаться на переменные аргументы 
    va_end(va_params); 
    ^ 
    /1111/1///1/1///1/1//11//1111/1//111////11/1/1//11/11/1111111111/ИИ1111111/11 
    / / эта функция возвращает случайное число в диапазоне от 0 до nMaxValue 
    // включительно 
    int Random (int nMaxValue) { 
    return(rand() / (RAND_MAX / (nMaxValue + 1))); 
    } 
    
    l//lll/ll///ll//ill///llll//l/l/ll//llllllllll/l/lll//ll//ll////l/llllll/l/llll 
    BOOL Dlg_0nlnit0ialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    HWND hwndSB; 
    
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IOI_SPRMRKT, IDI_SPRMRKT); 
    
    // сохраняем описатель окна списка в глобальной переменной, чтобы 
    // функция AddStr могла получить к нему доступ 
    g_hwndLB = Get01gltem(hwnd, IDC_SHOPPEREVENTS); 
    // задаем диапазон для полос прокрутки и значения параметров 
    // моделирования по умолчанию 
    hwndSB = Get01gltem(hwnd, IDC_MAXOCCUPANCY); 
    ScrollBar_SetRange(hwndSB, 0, 500, TRUE); 
    
    // устанавливаем начальную позицию ползунка на полосе прокрутки 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 30, SendMessage); 
    hwndSB = Get01gltem(hwnd, IDC_TIMEOPEN): 
    ScrollBar_SetRange(hwndSB, 0, 5000, TRUE); 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 5000, SendMessage); 
    hwndSB = GetDlgItem(hwnd, IDC_NUMCOUNTERS): 
    ScrollBar_SetRange(hwndSB, 0, 30, TRUE); 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 5, SendMessage); 
    hwndSB = GetDlgItem(hwnd, IDC_SHOPPERCREATIONDELAY); 
    ScrollBar_SetRange(hwndSB, 0, 1000, TRUE); 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 300, SendMessage); 
    hwndSB = GetDlgItem(hwnd, IDC_DELAYTOGETIN); 
    ScrollBar_SetRange(hwndSB, 0, 100, TRUE); 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 20, SendMessage); 


    266 _______________________________Windows для профессионалов 


    РИС. 10-4 (продолжение) 

    hwndSB = GetDlgItem(hwnd, IDC_TIMETOSHOP); 
    ScrollBar_SetRange(hwndSB, 0, 100, TRUE): 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 80, SendMessage); 
    hwndSB = GetDlgItem(nwnd, IDC_WAITDELICNTR); 
    ScrollBar_SetRange(hwndSB, 0, 100, TRUE); 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 20, SendMessage): 
    hwndSB = GetDlgItern(hwnd, IOC_TIMEATDELICNTR): 
    ScrollBar_SetRange(hwndSB, 0, 100, TRUE); 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 70, SendMessage); 
    hwndSB = GetDlgItem(hwnd, IDC_TIMEATCHECKOUT); 
    ScrollBar_SetRange(hwndSB, 0, 100, TRUE): 
    FORWARD_WM_HSCROLL(hwnd, hwndSB, SB_THUMBTRACK, 60, SendMessage); 
    return(TRUE); 
    } 
    !!!ll/llll|llll/•!/ii!ii!///!l/l/il/li/l/ill|||||/|||||lll/ll!lll^'l|l||lil|//! 
    void Dlg_OnHScroll(HWND hwnd. HWND hwndCtI, UINT code, int pos) { 
    TCHAR szBuf[10]: 
    int nPosCrnt, nPosMin, nPosMax; 
    
    // получаем текущую позицию ползунка и допустимый диапазон полосы 
    // прокрутки, с которой сейчас работает пользователь 
    nPosCrnt = ScrollBar_GetPos(hwndCtl); 
    ScrollBar_GetRange(hwndGtl, SnPosMin, &nPosMax); 
    switch (code) { 
    case SB_LINELEFT: 
    nPosCrnt--; 
    break; 
    case SB_LINERIGHT: 
    nPosCrnt++; 
    break; 
    case SB_PAGELEFT: 
    nPosCrnt -= (nPosMax - nPosMin + 1) / 10; 
    break; 
    case SB_PAGERIGHT: 
    nPosCrnt += (nPosMax - nPosMin + 1) / 10; 
    break; 
    case SB_THUMBTRACK: 
    nPosCrnt = pos: 
    break; 

    __________________________Глава 10.  Синхронизация потоков 267 


    РИС. 10-4 (продолжение) 

    case SB_LEFT: 
    nPosCrnt = nPosMin; 
    break; 
    case SB_RIGHT: 
    nPosCrnt = nPosMax; 
    break; 
    
    // проверяем, укладывается ли новая позиция ползунка 
    // в допустимый диапазон 
    if (nPosCrnt < nPosMin) 
    nPosCrnt = nPosMin; 
    
    if (nPosCrnt > nPosMax) 
    nPosCrnt = nPosMax; 
    
    // устанавливаем ползунок в новую позицию 
    ScrollBar_SetPos(hwndCtl, nPosCrnt, TRUE); 
    
    // изменяем значение, показываемое в текстовом окне (метке), 
    // в соответствии с новой позицией ползунка на полосе прокрутки 
    _stprintf(szBuT, __TEXT("%d"), nPosCrnt); 
    SetWindowText(GetPrevSibling(hwndCtl), szBuf); 
    i 
    г 
    /////////////////////////////////////////////////////////////////////////////// 
    void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    DWORD dwThreadId; 
    HANDLE hThread; 
    switch (id) { 
    case IDOK: 
    
    // записываем новые значения параметров в глобальные переменные, 
    // чтобы использовать их при моделировании 
    g_nMaxOccupancy = ScrollBar_GetPos( 
    GetDlgItem(hwnd, IDC_MAXOCCUPANCY)); 
    
    g_nTimeOpen = ScrollBar_GetPos( 
    GetDlgItem(hwnd, IDC_TIMEOPEN)); 
    
    g_nCheckoutCounters = ScrollBar_GetPos( 
    GetDlgItem(hwnd, IDC_NUMCOUNTERS)); 
    
    g_nMaxDelayBetweenShopperCreation = ScrollBar_GetPos( 
    GetDl9ltem(hwnd, IDC_SHOPPERCREATIONDELAY)); 
    g_nMaxWaitToGet!nMarket = ScrollBar_GetPos( 
    GetDlgItem(hwnd, IDC_DELAYTOGETIN)): 


    268 ______________________________Windows для профессионалов 


    РИС. 10-4 (продолжение) 
    
    g_nMaxTlmeShopping = ScrollBar_GetPos( 
    Get01gltem(hwnd, IDC_TIMETOSHOP)); 
    
    9_nMaxWaitForDeliCntr = ScrollBar_GetPos( 
    GetDlgItem(hwnd, IDC_WAITOELICNTR)); 
    
    
    g_nMaxTimeSpentAtDeli = ScrollBar_GetPos( 
    Get01glte[fl(hwnd, IDC_TIMEATDELICNTR)): 
    
    
    g_nMaxTimeAtCheckout = ScrollBar_GetPos( 
    Get01gltem(hwnd, IDC_TIMEATCHECKOUT)): 
    
    
    // очищаем окно списка 
    ListBox_ResetContent(GetDlgItem(hwnd, IDC_SHOPPEREVENTS)); 
    
    
    // отключаем кнопку Open For Business, пока идет 
    // процесс моделирования 
    EnableWindow(hwndCtl, FALSE); 
    
    
    if (NULL == GetFocusO) { 
    
    
    SetFocus(GetDlgItem(hwnd, IDC_MAXOCCUPANGY)); 
    
    } 
    
    
    // перегруженность системы вызовет искажение результатов: 
    
    
    // поэтому, чтобы свести этот эффект к минимуму, повышаем 
    // класс приоритета данного процесса 
    SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS); 
    
    
    // создаем поток-супермаркет 
    hThread = chBEGINTHREADEX( 
    
    
    NULL,               // атрибуты защиты 
    
    
    О,                  // стек 
    
    
    ThreadSuperMarket, // функция потока 
    
    
    (LPVOID) hwnd,      // параметр функции потока 
    
    
    О,                  // флаги 
    
    
    SdwThreadId);       // идентификатор потока 
    
    
    // поскольку мы не собираемся управлять из этой функции 
    // объектом "поток", можно закрыть его описатель 
    CloseHandle(hThread); 
    
    
    break: 
    
    
    case IDCANCEL: 
    
    
    EndDialog(hwnd, id): 
    
    
    break; 
    
    
    } 
    
    
    /////////////////////////////////////////////////////////////////////////////// 
    
    
    см. след. cinr 

    ___________________________Глава 10. Синхронизация потоков 269 


    РИС. 10-4 (продолжение) 

    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog): 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); 
    chHANDLE_DLGMSG(hwnd. WM_HSCROLL, Dlg_OnHScroll); 
    case WM_USER: 
    
    // это сообщение посылается функцией SuperMarketThread, чтобы 
    // уведомить нас об окончании процесса моделирования 
    
    // восстанавливаем обычный класс приоритета процесса моделирования 
    SetPnorityClass(GetCurrentProcess(), NORMAL_PRIORITY_CLASS); 
    
    // включаем кнопку Open For Business, чтобы пользователь мог 
    // запустить модель с новыми параметрами 
    EnableWindow(GetDlgItem(hwnd, IDOK), TRUE); 
    break; 
    returntFALSE); 
    } 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) < 
    chWARNIFUNICODEUNDERWIN95(); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_SPRMRKT), NULL, Dlg_Proc); 
    return(O); 
    
    DWORD WINAPI ThreadSuperMarket (LPVOID IpvParam) { 
    DWORD dwCloseTime; 
    HANDLE hThread; 
    DWORD dwThreadId; 
    int nShopperNum = 0, nMaxOccupancy; 
    g_hSemEntranee = Create8emaphore( 
    NULL,                  // атрибуты защиты 
    О,                     // начальное состояние счетчика блокировки входа 
    g_nMaxOccupancy,       // максимальное число покупателей, которое может 
    
    // находиться в магазине 
    NULL):                 // семафор без имени 
    g_hMtxDeUCntr = CreateMutex( 
    NULL,                  // атрибуты защиты 

    см. след. стр. 


    270 _______________________________Windows для профессионалов 


    РИС. 10-4 (продолжение) 
    
    FALSE,                 // изначально у стойки закусочной никого нет 
    NULL);                 // мьютекс без имени 
    
    Q_hSemCheckout = CreateSe(naphore( 
    
    NULL.                  // атрибуты защиты 
    g_nCheckoutCounfers,   // все кассы свободны 
    g_nCheckoutCounters,   // число касс в магазине 
    NULL):                 // семафор без имени 
    
    // открываем супермаркет для покупателей 
    AddStr(__TEXT("---> Opening the supermarket to shoppers.")): 
    ReleaseSemaphore(g_hSemEntrance, g_nMaxOccupancy, NULL); 
    // получаем время, в которое надо прекратить создание 
    // потоков-покупателей 
    dwCloseTime = GetTickCount() + g_nTimeOpen: 
    
    // продолжаем цикл, пока магазин не закроется 
    while (GetTickCount() < dwCloseTime) { 
    
    // создаем поток-покупатель 
    hThread = chBEGINTHREADEX( 
    NULL,                  // атрибуты защиты 
    О,                      // стек 
    ThreadShopper,         // функция потока 
    (LPVOID) ++nShopperNum,// номер покупателя как IpvParain 
    О,                      // флаги 
    &dwThreadId);          // идентификатор потока 
    
    
    // поскольку мы не собираемся управлять из этой функции 
    // объектом "поток", можно закрыть его описатель 
    CloseHandle(hThread): 
    
    
    // ждем, пока в супермаркет не войдет другой покупатель 
    Sleep(Random(g_nMaxDelayBetweenShopperCreation)); 
    
    
    // супермаркет пора закрывать; ждем ухода оставшихся покупателей 
    AddStr(__TEXT("---> Waiting for shoppers to check out ") 
    __TEXT("so store can close.")); 
    
    
    nMaxOccupancy = 1; 
    
    
    for (; nMaxOccupancy <= g_nMaxOccupancy; nMaxOccupancy++) { 
    WaitForSingleObject(g_hSemEntranee, INFINITE); 
    
    
    AddStr(__TEXT("---> %d shoppers NOT in store."). nMaxOccupancy); 
    
    
    } 
    
    см. след. стр. 

    __________________________Глава 10. Синхронизация потоков 271 

    РИС. 10-4 (продолжение) 
    
    AudSLi („ТЕХТС1---/ Store closed—end от Simula Lion' )); 
    
    
    // все ушли - моделирование закончено 
    CloseHandle(g_hSemCheckout); 
    
    
    CloseHandle(g_hMtxDeliCntr): 
    
    
    CloseHandle(g_hSemEntranee); 
    
    
    // Уведомляем GUI-поток, что моделирование завершено. Описатель 
    // диалогового окна, принадлежащего GUI-потоку, передан через 
    // параметр IpvParam в этот поток еще при его создании. 
    SendMessage((HWND) IpvParam, WM_USER, 0, 0); 
    
    
    return(O); 
    
    
    DWORD WINAPI ThreadShopper (LPVOID IpvParam) { 
    int nShopperNum = (int) IpvParam; 
    
    
    DWORD dwResult: 
    
    
    int nDuration; 
    
    
    // формируем зародыш случайных чисел для данного l'lOIu^a; иначе асе 
    // потоки-покупатели будут использовать один и тот же зародыш 
    // и генерировать одинаковую последовательность случайных чисел 
    srand(GetTickCount() * nShopperNum); 
    
    
    // ждем, когда покупатель войдет в супермаркет 
    nDuration = Random(g_nMaxWaitToGetInMarket); 
    
    
    AddStr(__TEXT("%041u: Waiting to get in store (%lu),"), nShopperNum, nDuration); 
    
    
    dwResult = WaitForSingleObject(g_hSemEntrance, nDuration); 
    
    
    if (dwResult == WAIT_TIMEOUT) { 
    
    
    // покупателю надоело ждать и он ушел 
    
    AddStr(_J~EXT("%041u: Tired of waiting; went home."), 
    
    
    nShopperNum); 
    
    
    return(O); 
    
    
    // покупатель вошел в супермаркет; пора заняться покупками 
    nDuration = Random(g_nMaxTimeShopping); 
    
    
    AddStr(__TEXT("%041u: In supermarket, shopping for %lu."), nShopperNum, nDuration); 
    
    
    Sleep(nDuration); 
    
    
    см. след. стр. 


    272 _______________________________Windows для профессионалов 


    РИС. 10-4 (продолжение) 
    
    II Покупки закончены. Один к трем, что он зайдет в закусочную 
    if (Random(2) == 0) { 
    
    
    // все-таки зашел 
    nDuration = Random(g_nMaxWaitForDeliCntr); 
    
    
    AddStr( 
    
    
    __TEXT("%041u: Waiting for service at ") 
    
    
    __TEXT("Deli Counter (%lu)."), 
    
    
    nShopperNum, nDuration); 
    
    
    dwResult = WaitForSingleObject(g_hMtxDeliCntr. nDuration); 
    
    
    if (dwResult == 0) { 
    // сделал заказ 
    nDuration = Random(g_nMaxTimeSpentAtDeli); 
    
    
    AddStr(__TEXT("%041u: Being served at Dell (%lu)."), nShopperNum, nDuration); 
    
    
    Sleep(nDuration); 
    
    
    // покинул стойку 
    ReleaseMutex(g_hMtxDeliCntr): 
    
    
    } else { 
    
    
    // устал ждать, продолжил покупки 
    
    
    Add8tr(__TEXT("%041u: Tired of waiting at Deli."), nShopperNum); 
    
    
    i 
    
    
    } else { 
    
    AddStr(__TEXT("%041u: Not going to the Deli counter."), nShopperNum); 
    
    
    // встал в очередь к кассе 
    AddStr(__TEXT("%041u: Waiting for an empty checkout counter."), nShopperNum); 
    
    
    WaitForSingleObject(g_hSemCheckout, INFINITE); 
    
    
    // расплачивается 
    nDuration = Random(g_nMaxTimeAtCheckout); 
    
    
    AddStr(__TEXT("%041u: Checking out (%lu)."), nShopperNum, nDuration); 
    
    
    Sleep(nDuration); 
    
    
    // отходит от кассы 
    AddStr(__TEXT("%041u: Leaving checkout counter."), nShopperNum); 
    
    
    ReleaseSemaphore(g_hSemCheckout, 1, NULL); 
    
    
    // уходит из магазина 
    AddStr(__TEXT("%041u: Left the supermarket."), nShopperNum); 
    
    
    ReleaseSemaphore(g_hSemEntrance, 1, NULL); 
    
    
    // Все. Конец потоку-покупателю. 
    
    
    return(O); 
    
    
    \ 
    
    IIIIIIIIIIIIIIII/HIIIIIIIIIHIIII конец файла I /1111 /1111 Ц 11111 /1 H 


    __________________________Глава 10. Синхронизация потоков 273 


    События 

    События — самая примитивная разновидность синхронизирующих объектов, резко 
отличающаяся от семафоров и мьютексов. Последние обычно применяются для 
контроля за доступом к данным, а события просто уведомляют об окончании 
какой-либо операции. Объекты «событие» бывают двух типов: со сбросом вручную 
(manual-reset events) и с автосбросом (auto-reset events). Первые позволяют 
уведомлять об окончании операции сразу несколько потоков, вторые — только один поток. 
    События обычно используют в том случае, когда какой-то поток выполняет 
инициализацию, а затем сигнализирует другому потоку, что тот может продолжить 
работу. Инициализирующий поток переводит объект «событие» в занятое состояние 
и приступает к своим операциям. По окончании инициализации поток сбрасывает 
событие в свободное состояние. В то же время рабочий поток приостанавливает 
свое исполнение и ждет перехода события в свободное состояние. Как только 
инициализирующий поток освободит событие, рабочий поток «проснется» и 
продолжит работу. 
    Например, в процессе исполняются два потока. Первый считывает данные из 
файла в буфер и оповещает второй, что можно заняться обработкой данных. 
Закончив обработку, второй поток сигнализирует первому, чтобы тот загрузил в 
буфер новую порцию данных из файла и т. д. 
    Начнем с создания событий. Семантика функций, оперирующих с событиями, 
идентична семантике соответствующих функций, предназначенных для 
объектов-мьютексов и семафоров. Событие создается функцией CreateEvent 

    HANDLE CreateEvent(LPSECURITY_ATTRIBUTES Ipsa, BOOL fManualReset, 
    BOOL fInitialState, LPTSTR IpszEventNaine); 

    Параметр fManualReset (Булева переменная) сообщает системе, какое событие 
Вы хотите создать: со сбросом вручную (TRUE) или с автосбросом (FALSE). 
Параметр fInitialState определяет начальное состояние события: свободное 
(TRUE) или занятое (FALSE). После того как система создает объект «событие», 
CreateEvent возвращает описатель события, специфичный для конкретного 
процесса. Потоки из других процессов могут получить доступ к этому объекту: 1) 
вызовом CreateEvent с тем же параметром IpszEventName; 2) наследованием 
описателя; 3) применением DuplicateHandle; и 4) вызовом OpenEvent с передачей 
в параметре IpszEventName имени, совпадающего с тем, что было указано в 
аналогичном параметре функции CreateEvent. Вот что представляет собой 
OpenEvent HANDLE OpenEvent(DWORD fdwAccess, BOOL finherit, LPTSTR IpszName); 
    Ну а закрытие событий осуществляется весьма популярной функцией CloseHandle. 

    События со сбросом вручную 

    События этого типа автоматически не переустанавливаются в занятое 
состояние функциями WaitForSingleQbject и WaitForMultipleObjects. В случае 
объектов-мьютексов, когда поток вызывает одну из этих функций, та ждет его 
освобождения, а затем автоматически переводит мьютекс в занятое состояние. Это 
очень важно, так как гарантирует, что только один поток, ожидающий мьютекс, 
получит его в свое распоряжение и сможет продолжить выполнение своего кода. 
Если бы сами потоки отвечали за возврат мьютекса в занятое состояние, то 
объект могли бы захватить два и более потоков, прежде чем один из них успел бы 
сбросить его состояние. 
    В отношении событий со сбросом вручную дело обстоит совершенно иначе. У 
Вас может быть несколько потоков, ждущих возникновения одного события. Когда 
оно наконец происходит, каждый из ожидавших потоков получает возможность 
выполнить свои операции. Чтобы лучше разобраться в этом механизме, вернемся к 
примеру с чтением и обработкой файловых данных. Допустим, один поток отвечает 
за считывание данных из файла в буфер. После того как данные считаны, Вы 
запускаете девять других потоков. Каждый из них обрабатывает данные по-своему. 
Предположим, в файле находится документ, созданный в каком

    274 ______________________________Windows для профессионалов 

    нибудь текстовом процессоре. Тогда пусть первый поток подсчитывает 
символы, второй — слова, третий — страницы, четвертый, скажем, проверяет 
орфографию, пятый печатает документ и т. п. Отметьте — и это крайне важно, — 
что у всех потоков есть общая черта: ни один из них ничего не записывает в 
файл. Данные для них — ресурс, открытый только для чтения. 
    Ясно, что при возникновении события все ожидающие его потоки должны 
возобновить свое выполнение. Вот Вам и один из случаев применения событий со 
сбросом вручную. Когда объект «событие со сбросом вручную» переходит в 
свободное состояние, система разрешает исполнение всех ожидавших его потоков. 
Поток переводит такой объект «событие» в свободное состояние вызовом SetEvent: 

    BOOL SetEvent(HANDLE hEvent); 

    Эта функция принимает описатель объекта «событие» и меняет состояние 
объекта на свободное. Если операция прошла успешно, функция возвращает TRUE. 
После перевода объекта в свободное состояние он пребывает в нем, пока 
какой-нибудь поток явным образом (т. е. «вручную») не сбросит событие, вызвав: 

    BOOL ResetEvent(HANOLE hEvent); 

    Эта функция принимает описатель объекта «событие» и переводит объект в 
занятое состояние. Если операция прошла успешно, функция возвращает TRUE. (Я 
же говорил, что события — самые примитивные из синхронизирующих объектов.) 
    Что касается последнего примера, то поток, считывающий данные из файла в 
общий буфер памяти, должен вызывать ResetEvent как раз перед этой операцией, а 
закончив загрузку данных, — обратиться к SetEvent. 
    Да, и вот еще что. Как поток, считывающий данные из файла, узнает, что 
пора считывать следующий блок? Нам известно, что это должно произойти по 
окончании работы прочих потоков с текущей порцией данных. Значит, и эти потоки 
должны как-то сигнализировать об окончании своих операций. Лучше всего создать 
в каждом из них собственный объект «событие», а описатели этих объектов 
поместить в массив; тогда поток, отвечающий за чтение данных, сможет 
воспользоваться функцией WaitForMultipleObjects и сообщить ей, что он 
собирается ждать освобождения всех девяти объектов. 
    Поскольку в программистской практике нередко после вызова SetEvent объект 
«событие» освобождается и тут же следует вызов ResetEvent, в Win32 
предусмотрена дополнительная функция, способная самостоятельно выполнять все 
три операции: 

    BOOL PulseEvent(HANDLE hEvent); 

    После возврата управления функцией PulseEvent событие остается в занятом 
состоянии. Если функция выполнена успешно, возвращается TRUE. 

    Программа-пример «корзина с шарами» 

    Во многих приложениях возникает одна и та же проблема синхронизации, о 
которой часто говорят как о классическом сценарии «группа читателей/группа 
писателей». В чем ее суть? Представьте: произвольное число потоков пытается 
получить доступ к некоему глобальному ресурсу. Каким-то потокам («писателям») 
нужно модифицировать данные, а каким-то потокам («читателям») — лишь прочесть 
эти данные. Синхронизация такого процесса необходима хотя бы потому, что Вы 
должны придерживаться следующих правил: 
    1. Когда один поток что-то пишет в область общих данных, другие этого 
делать не могут. 
    2. Когда один поток что-то пишет в область общих данных, другие не могут 
ничего считывать оттуда. 
    3. Когда один поток считывает что-то из области общих данных, другие не 
могут туда ничего записывать. 

    __________________________Глава 10. Синхронизация потоков 275 

    4. Когда один поток считывает что-то из области общих данных, другие тоже 
могут это делать. 
    Посмотрим на проблему в контексте базы данных. Допустим, с ней работают 5 
конечных пользователей: двое вводят в нее записи, трое — считывают. 
    В этом сценарии правило 1 необходимо потому, что мы, конечно же, не можем 
позволить одновременно обновлять запись, скажем, с номером 3457. Если двое 
пользователей попытаются модифицировать одну и ту же запись, то не исключено, 
что это произойдет одновременно. И о последствиях тогда лучше не думать. 
    Правило 2 запрещает доступ к записи, обновляемой в данный момент другим 
пользователем. Иначе пользователь считывал бы запись, например, под номером 
2543, когда другой пользователь изменял ее содержимое. Что они увидели бы на 
мониторах своих компьютеров, предсказать не берусь. Правило 3 служит тем же 
целям, что и правило 2. И действительно, какая разница, кто первый получит 
доступ к данным: тот, кто записывает, или тот, кто считывает, — все равно 
одновременно этого делать нельзя. 
    И, наконец, последнее правило. В самом деле, раз никто не модифицирует 
записи в базе данных, все пользователи могут свободно читать любые записи. 

    0'кэй, суть проблемы Вы ухватили. А теперь вопрос: как ее решить? 

    Программа Bucket (BUCKET.EXE) (см. листинг на рис. 10-5) демонстрирует 
способ решения проблемы с синхронизацией доступа к общим данным. В ней 
создается 5 потоков, обращающихся к небольшой базе данных. Для синхронизации 
этих потоков в программе используются три типа синхронизирующих объектов ядра: 
события со сбросом вручную, семафоры и мьютексы. Хотя программа имеет дело 
всего с 5 потоками (два обновляют базу данных, три — считывают записи), 
представленные в ней приемы и методы легко распространить и на более сложные 
случаи. Файлы исходного кода и ресурсов этой программы находятся в каталоге 
BUCKET на компакт-диске, прилагаемом к книге. 
    После запуска программы Bucket на экране появляется диалоговое окно: 
    База данных, которой управляет программа, — это корзина, вмещающая не 
более 100 шаров. Изначально корзина пуста. Раздел Bucket Writers в верхней 
части диалогового окна представляет 2 потока, способных добавлять в корзину 
или удалять из нее цветные шары; кроме того, они могут изменять «цветовой 
состав» находящихся в корзине шаров. Справа от номера потока указывается время 
задержки (в секундах). Для первого потока оно равно 1 — значит, этот поток 
каждую секунду пытается получить доступ к корзине и добавить в нее еще один 
шар. Время задержки можно изменять на расположенной рядом полосе прокрутки 
(диапазон регулировки 0-60 секунд). Поток 2 делает то же самое, но у него 
время задержки изначально поставлено на 3 секунды. 

    276 _______________________________Windows для профессионалов 

    В нижней части диалогового окна представлены 3 потока-«читателя». Они 
работают аналогично потокам-«писателям». Через каждые п секунд (от 0 до 60) 
они стирают содержимое окон списков, пересчитывают шары разных цветов и 
обновляют показываемую информацию. 
    Важно помнить, что здесь все потоки синхронизированы. Если 
поток-«писатель» получает разрешение на добавление, удаление или изменение 
цвета шара в корзине, никакой другой поток-«писатель» или «читатель» к корзине 
не допускается. С другой стороны, если поток-«читатель» получает доступ к 
корзине и занимается подсчетом шаров, то и остальные «читатели» могут делать 
то же самое, но потоки-«писатели» к корзине подпускать нельзя. 
    Ну а теперь, когда мы познакомились с принципом работы программы, я хочу — 
прежде чем переключиться на ее исходный текст (см. рис. 10-5) — поговорить еще 
на одну тему, изложенную в следующем разделе. 

    /анимАниЕ\ 
    На протяжении всей главы я пытался исподволь показать Вам, как 
трудно находить и исправлять ошибки, допущенные при синхронизации 
потоков. Как выяснилось, и я не застрахован от этого — несколько 
читателей любезно обратили мое внимание на ошибки в программе Bucket, 
приведенной в первом издании книги. Я исправил их (по крайней мере мне так 
кажется) и вообще переработал программный код. Если Вы использовали программу 
Bucket из первого издания, Вам будет небезынтересно изучить ее новую версию и 
проверить, не надо ли внести улучшения и в свой код. 


    Составной синхронизирующий объект SWMRG 

    Начав исправлять программу Bucket для второго издания, я понял, что 
синхронизация потоков реализована в ней слишком сложно и запутанно — все это 
надо сделать как-то иначе. Поскольку сценарий «один писатель/несколько 
читателей» часто встречается при синхронизации потоков, я пришел к выводу, что 
для решения этой проблемы лучше создать некий универсальный объект, который 
можно использовать и повторно. Так что, если и перед Вами стоит похожая 
задача, смело заимствуйте мой код и включайте в свои программы — с изменениями 
или без, дело Ваше. Плоды своих трудов я назвал SWMRG (я произношу его как 
swimerge); это аббревиатура от single writer/multiple reader guard. 
    Объект SWMRG — разработанная мной структура данных. Его описание и 
работающие с ним функции содержатся в файлах SWMRG.Н и SWMRG.С (в каталоге 
BUCKET на компакт-диске, прилагаемом к книге). Итак, объект SWMRG — это 
составной синхронизирующий объект, созданный специально для решения 
классической проблемы синхронизации «один писатель/несколько читателей». Я 
называю его составным, так как в нем 3 объекта ядра: мьютекс, семафор и 
событие со сбросом вручную. Как он использует их, я поясню позже. А сейчас 
поговорим, как с ним работает программа. 
    Начнем с того, что этот объект применяется точно так же, как и объект 
CRITICAL_SECTION. Во-первых, Вы создаете в приложении экземпляр объекта SWMRG. 
Как и в случае объектов CRITICAL_SECTION, он тоже обычно создается как 
глобальная переменная (чтобы любой поток в процессе мог получить к нему доступ): 

    Sinclude "SWMRG.h" 
    SWMRG g_SWMRG:   // глобальный объект SWMRG 

    Объект SWMRG следует рассматривать как «черный ящик»; программе нет нужды 
обращаться к его элементам данных — с ними работают только специально 
предназначенные для этого функции (файл SWMRG.С). Как видите, и тут полная 
аналогия с объектами CRITICAL_SECTION.                                         
     —-—-
    Прежде чем какой-нибудь поток сможет синхронизировать себя с объектом 
SWMRG, последний надо инициализировать вызовом SWMRGInitialize (обычно из WinMain): 

    BOOL SWMRGInitialize(PSWMRG pSWMRG, LPCTSTR IpszName); 


    ___________________________Глава 10. Синхронизация потоков 277 

    Первый параметр — адрес объекта SWMRG, а второй позволяет присвоить 
объекту имя. Хотя SWMRGInitialize аналогична функции 
InitializeCriticalSection, между ними все-таки есть одно существенное 
различие: объект SWMRG построен так, что к нему могут обращаться потоки, 
выполняемые в разных процессах. Но для этого в параметре IpszName Вы должны 
передать строку с именем объекта (прием, аналогичный тому, что используется 
при создании мьютекса). Если Вы не собираетесь «делить» объект SWMRG с другими 
процессами, в параметре IpszName передайте NULL. К использованию объекта SWMRG 
сразу в нескольких процессах я вернусь позже. 
    Если процесс заканчивается и Вы уверены, что больше никакой поток (ни 
«писатель», ни «читатель») не попытается обратиться к SWMRG, объект нужно 
разрушить вызовом SWMRGDelete: 

    void SWMRGDelete(PSWMRG pSWMRG); 

    В промежутке между вызовами функций SWMRGInitialize и SWMRGDelete любой 
поток может использовать этот объект для синхронизации. Интерфейс очень прост. 
Желая получить доступ к. общему ресурсу, поток-«писатель» вызывает функцию, 
аналогичную EnterCriticalSection: 

    DWORD SWMRGWaitToWr-ite( PSWMRG pSWMRG, DWORD dwTimeout); 

    Поток, стремящийся записать какие-то данные, передает в первом параметре 
адрес объекта SWMRG, а во втором — величину таймаута. И тут проявляется еще 
одно отличие объекта SWMRG от критических секций: у Вас есть возможность 
задать период ожидания. Функция SWMRGWaitToWrite возвращает либо 
WAIT_OBJECT_0, либо WAIT.TIMEOUT. 
    Если поток-«писатель» получил право монопольного доступа к общим данным 
(функция вернула WAIT_OBJECT_0), то по окончании работы с этим ресурсом он 
должен вызвать SWMRGDoneWriting. Это позволит другому потоку («писателю» или 
«читателю») обратиться к разделяемому ресурсу: 

    void SWMRGDoneWriting(PSWMRG pSWMRG); 

    Эта функция похожа на LeaveCriticalSection. 
    До сих пор мы рассматривали объект SWMRG применительно к 
потоку-«писателю». Теперь обсудим, как этим объектом пользуется 
поток-«читатель». Когда потоку нужно прочесть какие-то данные в общем ресурсе, 
он вызывает SWMRGWaitToRead: 

    DWORD SWMRGWaitToRead(PSWMRG pSWMRG, DWORD dwTimeout); 

    Она, как и SWMRGWaitToWrite, принимает те же параметры и возвращает те же 
значения. Разница лишь в том, что они делают, — но об этом позже. 
    Закончив чтение общих данных, поток должен обратиться к SWMRGDoneReading, 
чтобы и поток-«писатель» мог получить доступ к разделяемому ресурсу: 

    void SWMRGDoneReading(PSWMRG pSWMRG); 

    Разумеется, поток-«писатель» получит доступ, только если этот ресурс не 
ожидают другие потоки-«читатели». 
    А теперь пришло время рассказать, как в действительности реализован объект 
SWMRG. Начнем с рассмотрения элементов данных структуры SWMRG: 

    typedef struct SingleWriterMultiReaderGuard { 
    
    // мьютекс охраняет доступ к другим объектам, управляемым этой 
    // структурой, а также уведомляет программу, записывают ли что-то 
    // потоки-"писатели" в общие данные 
    HANDLE hMutexNoWriter; 
    
    // событие со сбросом вручную свободно, если потоки-"читатели" 
    // не обращаются к общим данным 
    HANDLE hEventNoReaders; 

    см. слеп. стр 


    280 ______________________________Windows для профессионалов 


    hMutexNo Writer, то поток-«читатель», вызвавший SWMRGDoneReading, может 
блокировать остальные потоки, пока не обновит объекты «событие» и «семафор». 
    Я уже говорил, что SWMRGDoneReading вызывает WaitForMultipkObjects вместо 
WaitForSingleObject. Еще один описатель, который она передает 
WaitForMultipleObjects, — hSemNumReaders. Но вспомните: успешное завершение 
ожидания семафора означает, что система автоматически уменьшает на 1 его 
счетчик. И теперь функция SWMRGDoneReading должна проверить, не является ли 
данный поток последним «читателем». 
    Правда, определить последнее оказалось не так просто, как я полагал 
сначала. Это было бы несложно, будь в Win32 функция, которая возвращает 
текущее значение счетчика семафора, но ее нет. Оставалось одно — вызвать 
ReleaseSemaphore, а затем — WaitForSingkObject с передачей описателя 
hSemNumReaders, чтобы вновь уменьшить счетчик семафора на 1. Если 
ReleaseSemaphore приводит к обнулению переменных INumReaders, значит, данный 
поток последний, и я перевожу событие hEventNoReaders в свободное состояние 
вызовом SetEvent. 
    И последнее. Перед самым возвратом управления SWMRGDoneReading должна 
освободить мьютекс (hMutexNoWriter) вызовом ReleaseMutex. Как и в случае 
SWMRGWaitToRead, это нужно для того, чтобы мог «проснуться» какой-нибудь 
другой поток, ожидающий доступа к общим данным. 

    Исходный текст программы Bucket 

    После запуска программа Bucket первым делом создает синхронизирующий 
объект SWMRG. Поскольку данная программа единственная, которой нужен доступ к 
объектам «мьютекс», «событие» и «семафор», в функцию SWMRGInitialize через 
параметр IpszName передается NULL. Вслед за инициализацией объекта SWMRG 
программа вызывает DialogBox для вывода на экран своего диалогового окна. 
    Открытие окна приводит к тому, что система посылает сообщение 
WM_INITDIALOG. В процессе работы функции Dlg_OnImtDialog инициализируются 
полосы прокрутки и создаются 2 потока-«писателя» и 3 потока-«читателя». 
Описатели этих потоков запоминаются в глобальном массиве, так как они нужны 
для корректного завершения процесса. Все потоки работают одинаково: каждый, 
выполнив ряд инициализирующих операций, входит в цикл while, заканчиваемый по 
достижении глобальной переменной g_lTerminate значения, равного 1. (В момент 
запуска программы этой переменной присваивается 0; подробнее о ней я расскажу 
чуть позже.) 
    В цикле while каждый поток сначала вызывает Wii'132-функцию Sleep, 
передавая ей значение, установленное на полосе прокрутки, связанной с данным потоком: 

    Sleep(1000 * GetDlgItemInt(g_hwndDlg, nNumID, NULL, FALSE)); 

    Вызов Sleep имитирует деятельность потока («писателя» или «читателя»), 
которая не требует обращения к данным из разделяемого ресурса. Пробуждаясь, 
поток-«писатель» вызывает SWMRGWaitToWrite, а поток-«читатель» — 
SWMRGWaitToRead. После возврата из SWMRGWaitToWrite поток-«писатель» знает, 
что никакой другой поток не имеет доступа к данным. Далее он обращается к 
функции Bucket_AlterContents и изменяет содержимое корзины. 
    После возврата из функции SWMRGWaitToRead поток-«читатель» знает, что у 
потока«писателя» нет доступа к корзине, но прочие потоки-«читатели» могут 
оперировать с данными. Поэтому он вызывает функцию Bucket_DumpToLB, которая 
проверяет (но не изменяет) содержимое корзины и выводит результаты в 
соответствующий список диалогового окна. 
    Закончив манипуляции с данными, поток-«писатель» или поток-«читатель» 
вызывает функцию SWMRGDoneWriting или SWMRGDoneReading (в зависимости от типа 
потока), и 

    _________________________Глава 10. Синхронизация потоков 281 

    тем самым дает возможность другому потоку получить доступ к корзине — 
начинается новая итерация цикла while. 
    Как же поток узнает, когда ему надо завершиться? Когда пользователь 
закрывает диалоговое окно, первичный поток процесса, управляющий 
пользовательским интерфейсом, возвращается из вызова DiahgBox в функцию 
WinMain программы и исполняет строку кода: 

    InterlockedIncrernent((PLONG) &g_lTerminate); 

    Это увеличивает значение глобальной переменной g_lTerminate с 0 до 1. В 
конце концов все потоки заметят, что значение этой переменной изменилось, и 
для них это будет означать одно: пора завершаться. Функция 
Interlockedlncrement на атомном уровне изменяет значение переменной типа LONG, 
адрес которой передается функции при вызове. Это значит, что любой поток, 
попытавшийся выяснить значение переменной g_lTerminate, немедленно 
приостанавливается, пока Interlockedlncrement не закончит обновления этой 
переменной. (Подробнее о функции Interlockedlncrement см. в последнем разделе главы.) 
    Поскольку значение переменной g_lTerminate было увеличено, потоки 
(«писатели» и «читатели») прекращают исполнение. Но это может произойти с 
задержкой, если они еще заняты какой-то работой. Чтобы корректно завершить 
процесс, первичный поток дожидается завершения всех потоков. Поэтому, вызвав 
WaitForMultipleObjects, он передает ей описатели пяти потоков. (Не забудьте: 
эти описатели были сохранены в глобальном массиве при вызове функции 
Dlg_OnInitDialog.) Когда все потоки завершатся, WaitForMultipleObjects вернет 
управление, и первичный поток 5 раз вызовет CloseHandle, поочередно передавая 
ей описатели завершенных потоков. 
    И, наконец, первичный поток обратится к функции SWMRGDelete, чтобы 
уменьшить счетчики числа пользователей объектов hMutexNoWriter, 
hEventNoReaders и hSemNumReaders и таким образом высвободить эти объекты. 
    Тестируя завершение потоков и процесса Bucket, я воспользовался утилитой 
PERFMON.EXE, поставляемой в комплекте с Windows NT, — чтобы убедиться в 
корректной "рабБте'своей программы. Запустив BUCKET.EXE, я запустил и 
PERFMON.EXE, а затем выбрал из меню Edit (правка) команду Add To Chart 
(вывести на диаграмму); в итоге я увидел такое диалоговое окно: 

    ____________________________________________1 

    В этом диалоговом окне в поле Object (объект) я выбрал Process (процесс), 
в поле Instance (экземпляр) — Bucket, а в поле Counter (счетчик) — Thread 
Count (количество потоков). Потом щелкнул кнопку Add (добавить). Таким 
образом, я настроил программу PerfMon на отслеживание количества потоков, 
исполняемых в процессе Bucket. А вот диаграмма, которую я получил в результате: 

    282 _______________________________Windows для профессионалов 

    По диаграмме можно заметить, что в приложении Bucket почти одновременно 
образуются 6 потоков. Спустя какое-то время я закрыл диалоговое окно в своей 
программе. Первичный поток немедленно увеличил переменную g_lTerminate на 1; 
на это тут же среагировали 3 потока и завершились. После непродолжительной 
задержки и четвертый поток закончил свой цикл while, проверил переменную 
g_lTerminate и завершился. Несколько мгновений, и то же произошло с пятым 
потоком. Обнаружив факт завершения 5 потоков, пробудился первичный поток и 
тоже завершился. Вот почему на диаграмме виден такой резкий перепад от двух 
потоков до нуля. 
    В общем, PERFMON.EXE — невероятно полезная утилита, которую многие 
программисты игнорируют, — а зря! 

    BUCKET.C 
    Модуль; Bucket.С Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    #*************+*************************+*************************************/ 
    #include "..\CmnHdr.H"           /* см. приложение В */ 
    Sinclude <windows.h> 
    Itinclude <windowsx.h> 
    #include <tchar.h> 
    ftinclude <stdio.h>               // для sprintf 
    ffinclude <stdlib.h>              // для генерации случайных чисел 
    ffinclude <process.h>             // для _beginthreadex 
    Sinclude "Resource.H" 
    #include "SWMRG.H" 
    
    // объект для синхронизации потоков "один писатель/несколько читателей" 
    SWMRG g_SWMRG; 

    см. след. стр. 

    Рис. 10-5. Листинг программы Bucket 


    _______________________Глава 10. Синхронизация потоков 283 


    РИС. Ю-5 (продолжение) 

    I/ массив описателей потоков, необходимый для корректного завершения процесса 
    HANDLE g_hThreads[5]; 
    
    // флаг, указывающий потокам на необходимость завершения 
    // (volatile, т.к. флаг изменяется асинхронно) 
    long volatile g_lTerminate = 0; 
    
    // оконный описатель диалогового окна 
    HWND g_hwndDlg = NULL; 
    
    lllllllllllllllllllllll/lllllllllllllli/ll/'llil'ln/llli/iiil/liiii In // 
IIIIIIIIIIIIIIIII данные и процедуры для управления корзиной ////////////////// 
    ///////////////////////////////////////////////////////////////////////////// 
    
    // список допустимых цветов шаров (перечислимого типа) 
    typedef enum { 
    BC_FIRSTBALLCLR, 
    // BC_NULL равнозначен пустому месту в корзине 
    BCJIULL = BC_FIRSTBALLCLR, 
    BC_BLACK, 
    BC_RED, 
    BC_GREEN, 
    BC_BLUE, 
    BCJriHITE, 
    BC_YELLOW, 
    BC_ORANGE, 
    BC_CYAN, 
    BC_GRAY, 
    
    BC_LASTBALLCLR = BC_GRAY 
    } BALLCOLOR: 
    
    // список допустимых цветов шаров (строкового типа) 
    const TCHAR *szBallColors[] = { 
    NULL, 
    __TEXT("Black"), 
    __TEXT("Red"), 
    __TEXT("Green"), 
    __TEXT("Blue"), 
    __TEXT("White"), 
    __TEXT("Yellow"), 
    __TEXT("Orange"), 
    __TEXT("Cyan"), 
    __TEXT("Gray") 
    }; 
    
    
    // максимальное количество шаров в корзине 
    Sdefine MAX_BALLS    100 
    
    // Изначально корзина пуста. Она тоже volatile, т.к. ее содержимое 
    // изменяется асинхронно. 


    284 ______________________________Windows для профессионалов 


    РИС. 10-5 (продолжение) 

    BALLCOLOR volatile g_Bucket[MAX_BALLS] = { BC_NULL }; 
    
    /////////////////////////////////////////////////////////////////////////////// 
    void BLicket_AlterContents (void) { 
    
    // добавляет/"вынимает" разноцветные шары из корзины (случайным образом) 
    g_Bucket[rand() % MAX_BALLS] = (BALLCOLOR) (rand() % 10); 
    
    /////////////////////////////////////////////////////////////////////////////// 
    vuia Bucket_DumpToLB (HWND hwridLB) { 
    int nBallNurri; 
    int nBaUColor[BC_LASTBALLCLR - BC_FIRSTBALLCLR + 1] = { 0 }: 
    BALLCOLOR BallColor; 
    TCHAR szBuf[50]; 
    
    // вычисляем, сколько шаров каждого цвета в корзине 
    for (nBallNum = 0; nBallNum < MAX_BALLS; nBallNu(ii++) { 
    // получаем цвет шара под номером nBallNum 
    BallGolor = g_Bucket[nBallNum]: 
    
    // увеличиваем счетчик шаров этого цвета 
    пВа11Со1ог[Ва11Со1ог]++; 
    } 
    
    // очищаем окно списка 
    ListBox_ResetContent(hwndLB); 
    
    // формируем его содержимое 
    BallColor = BC_FIRSTBALLCLR; 
    for (; BallColor <= BC_LASTBALLCLR; Ва11Со1ог++) { 
    if (szBallColors[BallColor] != NULL) { 
    
    _stpnntf(szBuf, __TEXT("%s: %*s%2d"), szBallColors[BallColor], 
    7 - lstrlen(szBallColors[BallColor]), __TEXT(" "), 
    nBallColor[BallColor]); 
    } else { 
    _stpnntf(szBuf, __TEXT( "Total: %2d"). 
    MAX_BALLS - nBaUColor[BallGolor]); 
    i 
    г 
    ListBox_AddString(hwndLB, szBuf): 
    i / 
    1111111111111111111111111111111111111111111111111111111111111111111111111111111 

    ________________________Глава 10. Синхронизация потоков 285 


    РИС. 10-5 (продолжение) 
    
    DWORD WINAPI Writer (LPVOID IpvParam) { 
    int nWriterNum = (int) IpvParam, nNumID; 
    
    switch (nWriterNum) { 
    case 1: 
    nNumID = IDC_WRITE1NUM; 
    break; 
    case 2: 
    nNumID = IDC_WRITE2NUM; 
    break; 
    default: 
    
    nNumID = 0; // мы не должны попадать сюда 
    break; 
    
    // цикл продолжается до тех пор, пока не завершится процесс 
    while (!g_lTerminate) { 
    
    // отправляемся спать на заданное пользователем время 
    Sleep(1000 * GetDlgItemInt(g_hwndDlg, nNumID, NULL, FALSE)); 
    
    // ждем, когда запись будет безопасной (не будет ни "писателей", 
    // ни "читателей") 
    SWMRGWaitToWrite(&g_SWMRQ. INFINITE); 
    
    // пишем в общие данные (в корзину) 
    Bucket_AlterContents(); 
    
    // сообщаем остальным потокам, что мы закончили 
    SWMRGDoneWriting(&g_SWMRG); 
    1 ( 
    return(O); 
    i i 
    iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii/iii/iiiiiiiiiiiiiiiiiiiiiiiiii 
    DWORD WINAPI Reader (LPVOID IpvParam) { 
    int nReaderNum = (int) IpvParam. nNumID ^ 0; 
    HWND hwndLB = NULL: 
    
    // получаем оконный описатель окна списка, принадлежащего данному потоку, 
    // и идентификатор статического элемента управления (метки), в котором 
    // показывается номер "читателя" 
    switch (nReaderNum) { 
    case 1: 
    nNumID = IDC_READ1NUM; 
    hwndLB = GetDlgItem(g_hwndDIg, IDC_READ1LIST); 
    break; 

    286 _______________________________Windows для профессионалов 


    РИС. 10-5 (продолжение) 

    case 2: 
    nNumID = IDC_READ2NUM; 
    hwndLB = GetDlgItem(g_hwnd01g, IDC_READ2LIST); 
    break; 
    case 3: 
    nNumID = IDC_READ3NUM; 
    hwndLB = GetDlgItem(g_hwndDlg, IDC_READ3LIST); 
    break: 
    default: 
    
    nNumID = 0; // мы не должны попадать сюда 
    hwndLB = NULL; 
    break; 
    
    // цикл продолжается до тех пор, пока не завершится процесс 
    while (! g_lTernnnate) { 
    
    // отправляемся спать на заданное пользователем время 
    Sleep(1000 * GetDlgItemInt(g_hwndDlg, nNumID. NULL, FALSE)); 
    
    
    // ждем, когда чтение будет безопасным (не будет "писателей") 
    SWMRGWaitToRead(&g_SWMRG, INFINITE): 
    
    
    // считываем из общих данных (из корзины) 
    Bucket_DumpToLB(hwndLB); 
    
    
    // сообщаем остальным потокам, что мы закончили 
    
    
    SWMRGDoneReading(&g_SWMRG); 
    
    
    } 
    return(O); 
    
    
    BOOL Dlg_OnInitDialog (MHO hwnd, HWND hwndFocus, LPARAM IParam) { 
    
    
    DWORD dwTrireadID: 
    
    
    int x; 
    
    
    // связываем значок с диалоговым окном 
    ch8ETDLGICONS(hwnd, IDI_BUCKET, IDI_BUCKET); 
    
    
    // Сохраняем описатель диалогового окна в глобальной переменной, чтобы 
    // потоки могли легко получить к нему доступ. Это нужно сделать до 
    // создания потоков. 
    g_hwndDlg = hwnd; 
    
    см. след. стр. 

    __________________________Глава 10. Синхронизация потоков 287 


    РИС. 10-5 (продолжение) 

    II инициализируем значения, отражаемые полосами прокрутки 
    // для потоков-писателей" 
    ScrollBar_SetRange(GetDlgItern(hwnd, IDC_WRITE1SCRL), 0, 60. FALSE); 
    ScrollBar_SetPos(GetDlgItem(hwnd, IDC„WRITE1SCRL), 1, TRUE); 
    SetDlgItemInt(hwnd, IOC_WRITE1NUM, 1, FALSE); 
    ScrollBar_SetRange(GetDlgItem(hwnd, IDCJOTTE2SCRL), 0, 60, FALSE); 
    ScrollBar_SetPos(GetDlgItem(hwnd, IDCJi)RITE2SCRL), 3, TRUE); 
    SetDlgItemInt(hwnd, IDC_WRITE2NUM, 3, FALSE); 
    // инициализируем значения, отражаемые полосами прокрутки 
    // для потоков-"читателей" 
    ScrollBar_SetRange(GetDlgItem(hwnd, IDC_READ1SCRL), 0, 60, FALSE); 
    ScrollBar_SetPos(GetDlgItem(hwnd, IDC_READ1SCRL), 2, TRUE); 
    SetDlgItemInt(hwnd, IDC_REA01NUM, 2. FALSE); 
    ScrollBar_SetRange(GetDlgItem(hwnd, IDC_READ2SCRL), 0, 60, FALSE): 
    ScrollBar_SetPos(GetDlgItem(hwnd, IDC_READ2SCRL), 4, TRUE); 
    SetDlgItemInt(hwnd, IDC_READ2NUM, 4, FALSE); 
    ScrollBar_SetRange(GetDlgItem(hwnd, IDC_READ3SCRL), 0, 60, FALSE); 
    Scr-ollBar_SetPos(GetDlgItem(hwnd, IDC_READ3SCRL), 7, TRUE); 
    SetDlgItemInt(hwnd, IDC_READ3NUM, 7, FALSE); 
    // Создаем 2 потока-"писателя" и 3 потока-"читателя". 
    // "Примечание: эти потоки НУЖНО создать ПОСЛЕ создания •  t . 
    // всех синхронизирующих объектов. 
    for (х = 0; х <= 1: х++) { 
    
    g_hThreads[x] = chBEGINTHREADEX(NULL, 0, Writer, 
    (LPVOID) (х + 1), 0, &dwThreadID); 
    } 
    tor (x = 2; x <= 4; x++) { 
    g_hThreads[x] = chBEGINTHREADEX(NULL, 0, Reader, 
    (LPVOID) (x - 1), 0, &dwThreadID); 
    \ 
    i 
    return(TRUE); 
    } 
    ///'//////////////////////////////////////////////////V///'///'///11///-1/,,'/1,1"/,,1.1/ 
    void Dlg_OnHScroll (HWND hwnd, HWND hwndCtI, UINT code, int pos) { 
    int posCrnt, posMin, posMax; 
    posCrnt = ScrollBar_GetPos(hwndCtl); 
    ScrollBar_GetRange(hwndCtl, &posMin, &posMax); 

    см. след. стр. 


    288 _______________________________Windows для профессионалов 


    РИС. 10-5 (продолжение) 

    switch (code) { 
    case SB_LINELEFT: 
    posCrnt--; 
    break; 
    case SB_LINERIGHT: 
    posCrnt++; 
    break: 
    case SB_PAGELEFT: 
    posCrnt -= 10; 
    break; 
    case SB_PAGERIGHT: 
    posCrnt += 10; 
    break: 
    case SB_THUMBTRACK; 
    posCrnt = pos; 
    break: 
    case SB_LEFT: 
    posCrnt = 0; 
    break; 
    case SB_RIGHT: 
    posCrnt = posMax; 
    break; 
    
    if (posCrnt < 0) 
    posCrnt = 0; 
    
    
    if (posGrnt > posMax) 
    posCrnt = posMax; 
    
    ScrollBar_SetPos(hwndGtl, posCrnt, TRUE); 
    SetDlgItemInt(hwnd, GetDlgCtrlID(hwndCtl) - 1, posCrnt, FALSE); 
    } 
    
    /////////////////////////////////////////////////////////////////////////////// 
    void Dlg_OnVScroll (HWND hwnd, HWND hwndCtI, UINT code, int pos) { 
    int posGrnt, posMin, posMax; 
    posCrnt = ScrollBar_GetPos(hwndCtl); 
    ScrollBar_GetRange(hwndCtl. &posMln, &posMax); 


    _________________________Глава 10. Синхронизация потоков 289 


    РИС. 10-5 (продолжение) 
    
    switch (code) < 
    case SB_LINEUP: 
    posCrnt--; 
    break; 
    case SB_LINEDOWN: 
    posCrnt++; 
    break; 
    case SB_PAGEUP: 
    posCrnt -= 10; 
    break; 
    
    case SB_PAGEDOWN 
    posCrnt += 10: 
    break; 
    case SB_THUMBTRACK: 
    posCrnt = pos; 
    break; 
    case SB_TOP: 
    posCrnt = 0; 
    break; 
    case SB_BOTTOM: 
    posCrnt = posMax; 
    break; 
    
    if (posCrnt < 0) 
    posCrnt = 0; 
    
    if (posCrnt > posMax) 
    posCrnt = posMax; 
    ScrollBar_SetPos(hwndGtl, posCrnt, TRUE); 
    SetDlgItemInt(hwnd, GetDlgCtrlID(hwndCtl) - 1, posCrnt, FALSE); 
    
    void Dlg_OnGoirimand (HWND hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    switch (id) { 
    case IDCANCEL: 
    EndDialog(hwnd, id); 
    break; 
    } 


    290 ______________________________Windows для профессионалов 


    РИС. 10-5 (продолжение) 
    
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG, Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand); 
    chHANDLE_DLQMSG(hwnd, WM_HSCROLL, Dlg_OnHScroll); 
    chHANDLE_DLGMSG(hwnd, WM_VSCROLL, Dlg_OnVScroll): 
    return(FALSE); 
    i 
    !///i/////li////////////l///l//lli/i/l//!i/i:/i!Hi/:i!i//i!ii/, ,',. i,, 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hins'tPrev, LPTSTR pszCmdLine, int nCnidShow) { 
    int x: 
    chWARNIFUNICODEUNDERWIN95(); 
    
    // Инициализируем объект SWMRG. Это надо сделать до того, 
    // как им попытается воспользоваться любой из потоков. 
    SWMRGInitialize(&g_SWMRG, NULL); 
    
    // отображаем пользовательский интерфейс программы 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_BUCKET), NULL, Dlg_Proc): 
    // Когда пользователь прекращает процесс, проводим очистку: 
    
    // 1. Информируем потоки, что процесс завершается. 
    Inter locked Increment((PLONG) &g_lTerminate); 
    
    // 2, Ждем завершения всех потоков. На это может уйти какое-то время, 
    // т.к. часть потоков, возможно, спит и поэтому еще не проверила 
    // значение переменной g_lTerminate. 
    WaitForMultipleObjects(chDIMOF(g_hThreads), g_hThreads, TRUE, INFINITE): 
    
    // 3. Закрываем свои описатели потоков. 
    for (x = 0; х < chDIMOF(g_hThreads); x++) 
    CloseHandle(g_hThreads[x]); 
    
    // 4. Удаляем синхррнизирующий объект SWMRG, Это нужно сделать, 
    // когда будет точно известно, что уже ни один поток не попытается 
    // им воспользоваться. 
    SWMRGDelete(&g_SWMRG); 
    return(O); 
    } 
    ////////////////////////////////// конец файла ///////////////////////////////// 

    см. след. i'fnp. 

    ________________________Глава 10. Синхронизация потоков 291 


    РИС, 10-5 (продолжение) 

    SWMRG.C 
    Модуль: SWMRG.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    fflnclude "..\CmnHdr.H"           /* см. приложение В */ 
    ftinclude <windows.h> 
    #include <string.h> 
    Sinclude <tchar.h> 
    ttinclude "SWMRG.H"               // заголовочный файл 
    lllllllllllllll/ll//^!/i!/!l/!^!/^/lllllllllllllllll/l^^!^!i':!!:•:!•:l::!:'!!' 
    static LPCTSTR ConstructObjName ( 
    LPCTSTR IpszPrefix, LPCTSTR IpszSuffix, 
    LPTSTR IpszFullName, size_t cbFullName, PBOOL fOk) { 
    *f0k = TRUE; // допустим, что все благополучно 
    
    if (IpszSuffix == NULL) 
    return(NULL); 
    if ((_tcslen(lpszPrefix) + _tcslen(lpszSuffix)) >= cbFullName) { 
    // если строки переполнят буфер, укажем на ошибку 
    *f0k = FALSE; 
    return(NULL); 
    } 
    _tcscpy(lpszFullNarne, IpszPrefix); 
    _tcscat(IpszFullName, IpszSuffix); 
    return(IpszFullName); 
    } 
    /•/,1'/,'/// /l/lllllllll/ill /:!! /Ilii i/ i/!i/i /in'i/i ! /i/ 'lil/i^i 
    
    BOOL SWMRGInitiallze(PSWMRG pSWMRG, LPCTSTR IpszName) { 
    TCHAR szFullObjName[100]; 
    LPCTSTR IpszObjName: 
    BOOL fOk; 
    
    5|,     // инициализируем все элементы как NULL, чтобы иметь возможность 
    // аккуратно проверить, не произошла ли ошибка 
    pSWMRG->hMutexNoWrlter = NULL; 
    pSWMRG->hEventNoReaders = NULL; 
    pSWMRG->hSernNumReaders = NULL: 
    // Объект-мьютекс охраняет доступ к другим объектам, управляемым 
    // этой структурой данных, а также указывает, не записывает ли что-то 
    
    см. слеЗ. стр. 


    292 _______________________________. Windows для профессионалов 


    РИС. 10-5 (продолжение) 
    
    II поток-"писатель". Изначально мьютекс не принадлежит ни одному потоку. 
    IpszObjName = ConstructObjName(__TEXT("SWMRGMutexNoWriter"), IpszName, 
    szFullObjName, chDIMOF(szFullObjName), &f0k); 
    if (fOk) 
    pSWMRG->hMutexNoWriter = CreateMutex(NULL, FALSE, IpszObjName); 
    // Создаем объект "событие со сбросом вручную"; он свободен, когда 
    // с данными не оперируют потоки-"читатели". Изначально потоки-"читатели" 
    // к данным не обращаются. 
    IpszObjName = ConstructObjName(__TEXT("SWMRGEventNoReaders"), IpszName, 
    szFullObjName, chDIMOF(szFullObjName), &f0k); 
    if (fOk) 
    pSWMRG->hEventNoReaders = CreateEvent(NULL, TRUE, TRUE, IpszObjName); 
    // Инициализируем переменную, которая сообщает количество потоков. 
    // читающих данные. Изначально потоки-"читатели" к данным не обращаются. 
    IpszObjName = ConstructObjName(__TEXT("SWMRGSemNumReaders"), IpszName, 
    szFullObjName. chDIMOF(szFullObjName), &f0k); 
    if (fOk) 
    pSWMRG->hSemNumReaders = CreateSemaphore(NULL, 0, Ox7FFFFFFF, IpszObjName); 
    if ((NULL == pSWMRG->hMutexNoWriter) :; (NULL == pSWMRG->hEventNoReaders) ;: 
    (NULL == pSWMRG->hSemNumReaders)) < 
    // если синхронизирующий объект создать не удалось, 
    // разрушаем все созданные объекты и сообщаем об ошибке 
    SWMRGDelete(pSWMRG); 
    fOk = FALSE; 
    } else { 
    fOk = TRUE; 
    } 
    // если все благополучно, возвращаем TRUE; в ином случае - FALSE 
    return(fOk); 
    г 
    /Illlllllllllll/l/l/llllllllllllll/lllll/llllllll/llllllllllflllllllllll/llllll 
    void SWMRGDelete(PSWMRG pSWMRG) { 
    // разрушаем все успешно созданные синхронизирующие объекты 
    if (NULL != p8WMRG->hMutexNoWriter) 
    CloseHandle(pSWMRG->hMutexNoWriter); 
    
    
    if (NULL != pSWMRG->hEventNoReaders) 
    CloseHandle(pSWMRG->hEventNoReaders); 
    
    
    if (NULL != pSWMRG->hSemNumReaders) 
    CloseHandle(pSWMRG->hSemNumReaders); 
    
    
    } 
    
    //IIIIIIIIIIIIIIIIIH/  /11/1111///1/11///111/1/1 //111/1///111/I IIIIIIIIH ;//; 


    _________________________Глава 10.  Синхронизация потоков 293 


    РИС. 10-5 (продолжение) 
    
    DWORD SWMRGWaitToWrite(PSWMRG pSWMRG, DWORD dwTimeout) { 
    DWORD dw; 
    
    HANDLE aHandles[2]; 
    // Запись допустима при выполнении следующих условий: 
    // 1. Мьютекс доступен и в общие данные не пишет ни один 
    //    поток-"писатель". 
    // 2. Данные никто не читает. 
    aHandles[0] = pSWMRQ->hMutexNoWriter; 
    aHandles[1] = pSWMRG->hEventNoReaders; 
    dw = WaitForMultipleObjects(2, aHandles, TRUE, dwTimeout); 
    if (dw != WAITJ'IMEOUT) { 
    
    // Этот поток может записывать в разделяемый ресурс. 
    // Поскольку идет запись, мьютекс нельзя освобождать. 
    // Тем самым мы останавливаем остальные потоки. 
    return(dw); 
    } 
    Ill/lill/lll/lllli/llll/llllllli/lll/lllllllllllllllllllll/llllllllllllllllllll 
    void SWMRGDoneWriting(PSWMRG pSWMRG) { 
    
    // Предположительно поток-"писатель", вызывающий эту функцию, успешно 
    // вызвал SWMRGWaitToWrite. А это значит, что ждать синхронизирующего 
    // объекта не надо, т.к. данный поток уже владеет объектом-мьютексом. 
    // Разрешаем остальным потокам использовать объект SWMRG. 
    ReleaseMutex(pSWMRG->hMutexNoWriter); 
    } 
    ll/lllllllllllll/lllllllllllil/llll/lllll//l//!/i//i///lil/liil/ll!lll!!il/!ll' 
    
    DWORD SWMRGWaitToRead(PSWMRG pSWMRG, DWORD dwTimeout) ( 
    DWORD dw: 
    LONG IPreviousCount; 
    // можно читать, если мьютекс доступен и потоки 
    // ничего не записывают в разделяемый ресурс 
    dw = WaitForSingleObject(pSWMRG->hMutexNoWriter, dwTimeout); 
    if (dw != WAIT_TIMEOUT) { 
    
    // Этот поток может считывать данные из разделяемого ресурса. 
    // Увеличиваем счетчик числа потоков-"читателей". 
    Release8emaphore(pSWMRG->hSemNumReaders, 1, &lPreviousCount); 
    if (IPreviousCount == 0) { 
    // если это первый поток-"читатель", соответственно 
    // настраиваем объект-событие 
    ResetEvent(pSWMRG->hEventNoReaders); 
    ) 


    294 _____________________________ Windows для профессионалов 


    РИС. 10-5 (продолжение) 
    
    II разрешаем другим потокам использовать 
    // синхронизирующий объект SWMRG 
    ReleaseMutex(pSWMRG->hMutexNoWriter); 
    } 
    return(dw); 
    
    void SWMRGDoneReading (PSWMRG pSWMRG) { 
    HANDLE aHandles[2]; 
    LONG INumReaders; 
    
    // можно прекратить чтение, если мьютекс доступен, но при этом 
    // мы должны уменьшить счетчик числа потоков-"читателей" на 1 
    aHandles[0] = p8WMRG->hMutexNoWriter; 
    
    
    aHandles[1] = pSWMRG->hSemNumReaders; 
    
    
    WaitForMultipleObjects(2, aHandles. TRUE, INFINITE): 
    
    
    // получаем оставшееся число "читателей", освобождая семафор 
    // и восстанавливая его счетчик немедленным вызовом 
    // WaitForSingleObject 
    ReleaseSemaphore(pSWMRG->hSemNumReaders, 1, &lNumReaders); 
    
    
    WaitForSingleObject(pSWMRG->hSemNumReaders, INFINITE); 
    
    
    if (INumReaders == 0) { 
    
    
    // если это последний поток-"читатель", соответственно 
    // настраиваем объект-событие 
    
    
    SetEvent(pSWMRG->hEventNoReaders); 
    
    
    i 
    { 
    
    
    II разрешаем другим потокам использовать 
    // синхронизирующий объект SWMRG 
    ReleaseMutex(pSWMRG->hMutexNoWriter); 
    
    
    } 
    
    
//////////////////////конец файла///////////////
    
    
    SWMRG.Н 
    
    
    /,,,,.*,**,*»*,*,*.. .^,,^.,^„,^.^^„.„...„„*,„*.*„,^,.^^^ 
    
    Модуль: SWMRG.Н  • 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    
    
    // синхронизирующий объект SWMRG (single-writer/multiple-reader guard) 
    typedef struct SingleWriterMultiReaderGuard { 
    
    
    См. след. стр. 

    __________________________Глава 10.  Синхронизация потоков 295 


    РИС. 10-5 (продолжение) 
    
    II мьютекс охраняет доступ к другим объектам, управляемым 
    // этой структурой данных, а также сообщает программе: 
    
    
    // обращаются ли к общим данным потоки-"писатели" 
    HANDLE hMutexNoWriter; 
    
    
    // событие со сбросом вручную свободно, если к общим данным 
    // не обращаются потоки-"читатели" 
    HANDLE hEventNoReaders; 
    
    
    // семафор служит простым счетчиком, доступным разным 
    // процессам; он НЕ используется для синхронизации потоков, 
    // а сообщает число потоков, считывающих общие данные 
    HANDLE hSemNumReaders; 
    
    
    } SWMRG, *PSWMRG; 
    
    
    11 llllllll/11111111/l, l iiiHii:/Hii:/:/il, //7/'////////7/,7/7//л/.,1, /,...,,„,, 
    
    
    // Инициализация структуры SWMRG (должна проводиться до того, как 
    // структурой попытается воспользоваться какой-либо из потоков). 
    // Структуру создайте в программе, а ее адрес передайте в первом 
    // параметре, Второй параметр (IpszName) определяет имя объекта. 
    // Передайте в нем NULL, если Вам не нужно совместно использовать 
    // данный объект. 
    BOOL  SWMRGInitialize (PSWMRG pSWMRG, LPCTSTR IpszName); 
    
    
    // Удаление системных ресурсов, связанных со структурой SWMRG. 
    // Структуру можно удалить, только если ее не ожидает какой-либо 
    // из потоков вызывающего процесса, 
    void  SWMRGDelete (PSWMRG pSWMRG); 
    
    
    // поток-"писатель" вызывает эту функцию, чтобы узнать, в какой 
    
    // момент можно записать что-то в общие данные 
    
    
    DWORD SWMRGWaitToWrite (PSWMRG pSWMRG, DWORD dwTimeout); 
    
    
    // поток-"писатель" вызывает эту функцию, чтобы уведомить другие 
    
    
    // потоки о том, что ему больше не надо записывать что-то в общие данные 
    
    void  SWMRGDoneWnting (PSWMRG pSWMRG); 
    
    
    // поток-"читатель" вызывает эту функцию, чтобы узнать, в какой 
    
    
    // момент можно обратиться к общим данным 
    
    
    DWORD SWMRGWaitToRead  (PSWMRG pSWMRG, DWORD dwTimeout); 
    
    
    // поток-"читатель" вызывает эту функцию, чтобы уведомить другие 
    
    
    // потоки о том, что ему больше не надо считывать что-то из общих данных 
    
    
    void  SWMRGDoneReading (PSWMRG pSWMRG); 
    
/////////////////////////конец файла 1111111111! 111111111111 H 11111111 

    296 ______________________________Windows для профессионалов 


    События с автосбросом 

    Объекты «события с автосбросом» больше похожи на семафоры и мьютексы, чем 
на события со сбросом вручную. Когда поток вызывает SetEvent, чтобы освободить 
событие, оно остается в таком состоянии, пока не пробудится другой поток, 
ожидающий тот же объект. За мгновение до возобновления работы потока система 
автоматически сбрасывает событие в занятое состояние. Применение объекта 
«событие с автосбросом» позволяет возобновлять выполнение лишь какого-то 
одного из ожидающих потоков. Остальные по-прежнему спят и ждут. Решение о том, 
какой именно из ждущих потоков возобновит выполнение, система принимает без 
Вашего ведома. Впрочем, это относится не только к событиям, но и ко всем 
синхронизирующим объектам. Одно можно сказать наверняка: если у ждущих потоков 
разный приоритет, первым возобновится тот, у кого он (приоритет) выше. 
    Этим типом событий, как и событиями со сбросом вручную, управляют функции 
SetEvent, ResetEvent и PulseEvent. ResetEvent обычно не применяют, поскольку 
система перед возвратом из WaitEorSingleObjectn 
И^я^РогМыйр^ОЬуес^автоматически сбрасывает (переводит в занятое состояние) эти 
объекты.                                ~"' 
    PulseEvent выполняет над событиями с автосбросом те же операции, что и над 
событиями со сбросом вручную: освобождает событие, разрешает исполнение 
ждущему этот объект потоку и вновь переводит событие в занятое состояние. Но 
одно небольшое отличие здесь все же есть: при вызове этой функции 
применительно к событию с автосбросом возобновляется выполнение лишь одного из 
ожидающих событие потоков, тогда как в случае события со сбросом вручную 
выполнение могли бы продолжить все ожидающие его потоки. 

    Программа-пример DocStats 

    Программа DocStats (DOCSTATS.EXE) (см. листинг на рис. 10-6) демонстрирует 
применение событий с автосбросом. Файлы исходного кода и ресурсов для этой 
программы находятся в каталоге DOCSTATS на компакт-диске, прилагаемом к книге. 
После запуска DocStats на экране появляется диалоговое окно: 
    Прежде чем проводить статистический анализ какого-либо документа, Вы 
должны ввести полное (вместе с путем) имя интересующего Вас файла или выбрать 
его, щелкнув кнопку Browse (обзор). Программа DocStats обрабатывает только 
ANSI-файлы и даст некорректные результаты для двоичного или Unicode-файла. 
    DocStats анализирует указанный файл и сообщает, сколько символов, слов и 
строк он содержит. Но самое интересное в программе не это, а то, как она 
выполняет эту героическую задачу. 
    Когда Вы выбираете кнопку Calculate Document Statistics (статистика 
документа), программа создает 3 потока — по одному на каждый тип 
подсчитываемых элементов (символов, слов й~строк). Потоки тут же 
приостанавливаются, пока в глобальном буфере не окажутся данные, подлежащие 
обработке. Далее программа открывает указанный файл и за

    Глава 10. Синхронизация потоков 297 

    гружает в буфер первые 1024 байта. Теперь данные готовы для обработки, и 
программа уведомляет приостановленные потоки о том, что теперь они могут 
возобновить исполнение и приступить к обработке глобальных данных. 
    В момент обработки этими потоками файловых данных первичный поток 
останавливается, чтобы не загрузить преждевременно следующие 1024 байта из 
файла. Если бы первичный поток не стал этого дожидаться, он испортил бы работу 
потоков, обрабатывающих данные, — вышло бы черт знает что. Наконец, загрузив 
из файла последнюю порцию данных, первичный поток закрывает файл, считывает 
результаты вычислений, выполненных тремя вторичными потоками, и выводит их на экран. 
    ""Самое интересное в программе DocStats то, как она синхронизирует 
выполнение первичного потока с тремя вторичными. В момент запуска первичный 
поток создает 6 событий с автосбросом — по 2 на каждый вторичный поток. Один 
объект из каждой пары уведомляет соответствующий вторичный поток о том, что 
первичный поток считал данные из файла и что теперь их можно обрабатывать. 
Описатели этих событий хранятся в массиве g_hEventsDataReady. В момент 
создания эти события переводятся в занятое состояние, указывая тем самым, что 
буфер данных не готов к обработке. 
    Второй объект из каждой пары служит индикатором того, что вторичный поток 
уже обработал данные, содержащиеся в глобальном буфере, и что он приостановил 
свое выполнение, ожидая сигнала от первичного потока, который должен 
подготовить к обработке новую порцию данных. Описатели этих объектов хранятся 
в массиве g_hEventsProddle. В момент создания эти события переводятся в 
свободное состояние, указывая тем самым, что вторичные потоки простаивают. 
    Далее первичный поток создает 3 вторичных потока. Все они сразу начинают 
выполнение (действуя совершенно одинаково). Их описатели помещаются в массив 
hThreads. В самом начале эти потоки входят в цикл и на каждой его итерации 
обрабатывают очередную порцию данных, загруженную первичным потоком. Однако 
прежде чем любой из вторичных потоков приступит к обработке содержимого 
глобального буфера, он должен дождаться инициализации буфера. Поэтому в цикле 
первым делом вызывается WaitForSingleObject, которой передается описатель 
соответствующего события из массива g_hEventsDataReady. 
    Затем первичный поток открывает файл и ждет, когда все три вторичных 
потока закончат обработку данных в глобальном буфере. На первой итерации цикла 
все события, описатели которых содержатся в массиве g_hEventsProddle, 
свободны, поэтому в данный момент первичный поток не впадает в ожидание. Он 
считывает первые 1024 байта в глобальный буфер и сигнализирует трем ожидающим 
потокам, что все готово. Для этого он трижды вызывает функцию SetEvent — по 
разу на каждое событие, описатель которого хранится в массиве g_hEventsDataReady. 
    Когда эти объекты становятся свободны, вторичные потоки пробуждаются и 
начинают обработку файловых данных. Так как упомянутые объекты — события с 
автосбросом, они самостоятельно возвращаются в занятое состояние, показывая 
тем самым, что данные для обработки не готовы. Но потоки уже исполняются и 
состояние этих событий больше не проверяют. Они работают, полагая, что данные 
готовы, — и это действительно так. 
    Работая над этой программой, я хотел обойтись всего одним событием, 
которое сигнализировало бы о готовности данных к обработке. Ведь программа 
использует единственный блок данных, и мне казалось, что одного события хватит 
для уведомления всех вторичных потоков. Не тут-то было! Когда какой-нибудь 
вторичный поток узнавал о готовности данных, событие тут же переходило в 
занятое состояние (я ведь использовал объект «событие с автосбросом»). И это 
происходило до того, как остальные вторичные потоки узнавали об освобождении 
объекта. Пришлось использовать 3 аналогичных события, чтобы каждый поток 
следил только за своим объектом и не влиял на работу остальных потоков. 

    298 ___________________________Windows для профессионалов 

    Окончив обработку буфера, каждый вторичный поток вызывает SetEventfins 
соответствующего события, описатель которого хранится в массиве 
g_hEventsProc[dle. Тем самым вторичный поток сигнализирует первичному, что 
обработка закончена. Первичный поток ждет таких сигналов от всех вторичных — 
через WaitForMultipkObjects (обращаясь к ней в начале своего цикла). Только 
потом он считывает следующую порцию данных из файла и загружает ее в буфер. 
    Когда WaitForMultipleObjects в первичном потоке возвращает управление, 
события g_hEventsProcIdle автоматически сбрасываются в занятое состояние, 
сообщая, что вторичные потоки больше не простаивают и занимаются обработкой 
данных. (На самом деле они еще ожидают освобождения событий g_hEventsDataReady.) 
    А если система автоматически не сбросит события g_hEventsProcIdle в 
занятое состояние? Допустим также, что программа выполняется в 
однопроцессорной системе, а потоки могут работать хоть целый час без вытеснения. 
    В этом случае, как только первичный поток считает данные из файла, события 
g_hEventsDataReady станут свободны, и он вновь вернется в начало своего цикла. 
На этот раз первичный поток должен был бы ожидать освобождения трех событий 
g_hEventsProddle, но, если они не сброшены в занятое состояние автоматически, 
его ожидание тут же и закончится. Он считает новую порцию данных, а вторичные 
потоки даже не успели приступить к обработке первой порции — вот Вам и «жучок»! 
    Конечно, я утрирую, предлагая представить, будто единственный поток 
выполняется в однопроцессорной системе целый час без вытеснения, но при 
разработке многопоточных приложений всегда полезно посильнее выпятить любую 
проблему: ее суть становится четче, и с ней легче справиться. А вообще 
написать и отладить многопоточное приложение — дело очень и очень непростое. 
Вы еще не раз прибегнете к моему методу «раздувания слона из мухи», чтобы 
операционная система не занялась этим за Вас. 
    Мне пришлось немало повозиться с программой DocStats, прежде чем она 
заработала корректно. Вытеснения потоков каждые 20 мс уже достаточно, чтобы 
программа рухнула. 
    Но хватит о грустном — вернемся к самой программе. После того, как 
первичный поток закончит чтение данных из файла, вторичные должны сообщить 
результаты подсчетов. Для этого каждый исполняет оператор return в конце своей 
функции потока и возвращает через него результат в первичный поток. Тот вновь 
обращается к WaitForMultipleObjects, чтобы дождаться завершения всех вторичных 
потоков, но ожидает он не самих событий, а описателей потоков, содержащихся в 
массиве hThreads. 
    По завершении всех вторичных потоков первичный, вызывая GetExitCodeThread, 
получает коды завершения каждого потока. Далее он вызывает CloseHandle для 
всех 6 событий и 3 потоков, чтобы освободить системные ресурсы. И, наконец, 
программа выводит результаты на экран. 
    DOCSTATS.C 
    Модуль; DocStats.С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    #include "..\CmnHdr,H"           /* см. приложение В */ 
    Sinclude <windows.h> 
    «include <windowsx.h> 
    ftinclude <tchar.h> 
    Sinclude <stdio.h>               // для sprintf 

    см. слео. стр. 

    Рис. 10-6. Листинг программы DocStats 

    _________________________Глава 10. Синхронизация потоков 299 


    РИС. 10-6 (продолжение) 

    «include <string. h> 
    «include <process.h>                // для _beginthreadex 
    ftinclude "Resource.H" 
    ////////////////////////////////1/////////////////////////////.'/' / / •• 
    typedef enum { 
    STAT_FIRST = 0, 
    STAT_LETTERS = STAT_FIRST, 
    STAT_WORDS, 
    STAT_LINES, 
    STAT_LAST = STAT_LINES 
    } STATTYPE: 
    HANDLE g_hEventsDataReady[STAT_LAST - STAT_FIRST +1]: 
    HANDLE g_hEventsProcIdle[STAT_LAST - STAT_FIRST +1]; 
    BYTE g_bFile8uf[1024]; 
    DWORD g_dwNumBytes!nBuf; 
    
    // упреждающая ссылка 
    WORD WINAPI StatThreadFunc(LPVOID IpvParam); 
    ///////////////////////////////
    BOOL DocStats(LPCTSTR pszPathname, PDWORD pdwNumLetters, 
    PDWORD pdwNumWords, PDWORD pdwNumLines) { 
    HANDLE hThreads[STAT_LAST - STAT_FIRST + 1]; 
    HANDLE hFile; 
    DWORD dwThreadID; 
    STATTYPE StatType; 
    *pdwNumLetters = 0; 
    *pdwNumWords = 0; 
    *pdwNumLines = 0; 
    
    // открываем файл для чтения 
    hFile = CreateFile(pszPathname, GENERIC_READ, 0, 
    NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL): 
    if (hFile == INVALID_HANDLE_VALUE) { 
    // файл открыть не удалось 
    chMB(__TEXT("File could not be opened.")); 
    return(FALSE); 
    } 
    
    for (StatType = STAT_FIRST; StatType < STAT_LAST + 1; StatType++) { 
    // свободны, когда поток не обрабатывает буфер 
    g_hEventsDataReady[StatType] = CreateEvent(NULL, FALSE, FALSE, NULL): 
    
    
    g_hEventsProcIdle[StatType] = CreateEvent(NULL. FALSE, TRUE. NULL): 
    
    ••i. след. стр. 

    300 _______________________________Windows для профессионалов 


    РИС. 10-6 (продолжение) 
    

    // Создаем все потоки, Их НАДО создать ПОСЛЕ создания 
    // объектов-событий. 
    
    
    hThreads[StatType] = chBEGINTHREADEX(NULL, О, 
    StatThreadFunc, (PVOID) StatType, 0, &dwThreadIO); 
    
    do { 
    
    
    // ждем. когда рабочие потоки начнут простаивать 
    WaitForMultipleObjects(STAT_LAST - STAT_FIRST + 1, 
    9_hEventsProcIdle, TRUE, INFINITE); 
    
    
    // считываем часть файла в глобальный буфер 
    ReadFile(hFlle, g_bFileBuf, chDIMOF(g_bFileBuf), 
    &g_dwNufflBytesInBuT, NULL): 
    
    
    // сигнализируем "работягам", что данные для обработки готовы 
    for (StatType = STAT_FIRST; StatType < STAT_LAST + 1; StatType++) { 
    
    
    SetEvent(g_hEventsDataReady[StatType]); 
    
    
    } while (g_dwNumBytesInBuf != 0); 
    
    
    // вся статистика по файлу собрана; пора приступить к очистке 
    CloseHandle(hFile): 
    
    
    // ждем "отдачи" от всех потоков 
    WaitForMultipleObjects(STAT_LAST - STAT_FIRST + 1, hThreads, TRUE, INFINITE); 
    
    
    // получаем статистику от каждого потока 
    GetExltCodeThread(hThreads[STAT_LETTERS], pdwNumLetters); 
    
    
    GetExitCodeThread(hThreads[STAT_WORDS], pdwNumWords): 
    
    
    GetExitCodeThread(hThreads[STAT_LINES], pdwNumLines); 
    
    
    // закрываем описатели для всех потоков 
    for (StatType = STAT_FIRST; StatType < STAT_LAST + 1; StatType++) { 
    
    
    CloseHandle(hThreads[StatType]); 
    
    
    CloseHandle(g_hEventsDataReadу[StatType]): 
    
    
    CloseHandle(g_hEventsProcIdle[StatType]); 
    
    
    } 
    
    
    return(TRUE); 
    
    
    } 
    
    
    ////////////////////////
    
    DWORD WINAPI StatThreadFunc(LPVOID IpvParam) { 
    STATTYPE StatType = (STATTYPE) IpvParam; 
    
    
    DWORD dwNumBytesInBuf, dwByteIndex, dwNumStat = 0; 
    
    
    BYTE bByte; 
    
    
    BOOL fInWord = FALSE. fIslriordSep; 
    
    см. след. стр. 

    __________________________Глава 10. Синхронизация потоков 301 


    РИС. 10-6 (продолжение) 
    1! ()   ( 
    
    // ждем, когда будут готовы данные 
    WaitForSingleObject(g_hEventsDataReady[StatType], INFINITE); 
    
    
    dwNumBytesInBuf = g_dwNumBytesInBuf; 
    
    
    dwByteIndex = 0: 
    
    
    for (: dwByteIndex < dwNumBytesInBuf; dwByte!ndex++) { 
    
    
    bByte = g_bFileBuf[dwByte!ndex]; 
    
    
    // эта программа работает только с ANSI-файлами независимо 
    // от того, компилировали мы ее исходный код для ANSI или Unicode 
    switch (StatType) { 
    case STAT_LETTERS: 
    
    
    if (IsCharAlphaA(bByte)) 
    dwNumStat++; 
    
    
    break; 
    
    
    case STAT_WORDS: 
    
    
    fIsWordSep = (strchr(" \t\n\r", bByte) != NULL); 
    
    
    if (!fInWord && !fIsWordSep) { 
    dwNumStat++; 
    
    
    fInWord = TRUE; 
    
    
    } else { 
    
    
    if (fInWord && fIsWordSep) 
    fInWord = FALSE: 
    
    
    break; 
    
    
    case STAT_LINES: 
    
    
    if ('\n' == bByte) 
    dwNumStat++; 
    
    
    break: 
    
    
    i 
    i 
    
    
    II данные обработаны; сигнализируем, что мы закончили 
    SetEvent(g_hEventsProddle[ StatType]); 
    
    
    } while (dwNumBytesInBuf > 0); 
    
    
    return(dwNumStat); 
    
    
    } 
    
    ////////////////////////
    BOOL Dlg_0nlnit0ialog (HWND hwnd, HWND hwndFocus, LPARAM IPararn) {     
    
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_DOCSTATS, IDI_DOCSTATS); 
    
    см. след. стр. 

    302 _______________________________Windows для профессионалов 


    РИС. 10-6 (продолжение) 

    II инициализируем диалоговое окно, отключая все недоступные пока 
    // элементы управления 
    EnableWindow(GetDlgItem(hwnd, IDC_DODOCSTATS), FALSE); 
    return(TRUE); 
    } 
    void Dlg_OnCommand (HWNO hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    TCHAR szPathname[_MAX_PATH]; 
    OPENFILENAME ofn; 
    DWORD dwNumLetters, dwNumWords, dwNurnLines: 
    switch (id) { 
    
    case IDC_FILENAME: 
    
    
    EnableWindow(GetDlgItem(hwnd, IDC_DODOCSTATS), 
    Edit_GetTextLength(hwndCtl) > 0); 
    
    
    break; 
    
    
    case IDC_FILESELECT: 
    
    
    chINITSTRUCT(ofn, TRUE); 
    
    
    ofn.hwndOwner = hwnd; 
    
    
    ofn.IpstrFile = szPathname; 
    
    
    ofn,lpstrFile[0] = 0: 
    
    
    orn.nMaxFile = chDIMOF(szPathname); 
    
    
    ofn.IpstrTitle = __TEXT("Select file for reversing"); 
    
    
    Ofn,Flags = OFN_EXPLORER : OFN_FILEMUSTEXIST; 
    
    
    GetOpenFileName(&ofn); 
    
    
    SetDlgItemText(hwnd, IDC_FILENAME, ofn,IpstrFile); 
    
    
    SetFocus(GetDlgItem(hwnd, IDC_DODOCSTATS)): 
    
    
    break; 
    
    
    case IDC_DODOGSTATS: 
    
    
    GetDlgItemText(hwnd, IDC_FILENAME, szPathname, chDIMOF(szPathname)); 
    
    
    if (DocStats(szPathname, &dwNumLetters, &dwNumWords, &dwNumLines) ) { 
    
    
    SetDlgItemInt(hwnd, IDC_NUMLETTERS, dwNumLetters, FALSE); 
    
    
    SetDlgItemInt(hwnd, IDC_NUMWORDS, dwNumWords, FALSE); 
    
    
    SetDlgItemInt(hwnd, IDC_NUMLINES, dwNumLines, FALSE): 
    
    
    i 
    
    
    break; 
    
    
    case IDCANCEL: 
    
    EndDialog(hwnd, id); 
    
    
    break; 
    
    
    / 
    
    
    ////////////////////

    см. след. en Ц1 

    ___________________________Глава 10. Синхронизация потоков 303 
    РИС. 10-6 (продолжение) 
    
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    
    
    chHANDLE_DLGMSG(hwnd, WM_INITDIALOG,  Dlg_OnInitDialog); 
    
    
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dlg_OnCommand): 
    
    
    } 
    return(FALSE): 
    
    
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    
    
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    
    
    ChWARNIFUNICODEUNDERWIN95(); 
    
    
    DialogBox(hinstExe, MAKEINTRESOURCE(IDDJ)OCSTATS), NULL, Dlg_Proc); 
    
    
    return(O); 
    
    
    } 
    
    
    ////////////////////////////////// конец файла I I I I I I / I I I I / / I 
    
    
    Ожидаемые таймеры 
    
    Ожидаемые таймеры (waitable timers) — новинка Windows NT 4. Это объекты 
ядра, самостоятельно переходящие в свободное состояние в определенное время 
и/или через регулярные промежутки времени. Чтобы создать ожидаемый таймер, 
достаточно вызвать функцию Create WaitableTimer. 
    
    HANDLE CreateWaitableTimer-(LPSECURITY_ATTRIBUTES Ipsa, 
    BOOL bManualReset, LPCTSTR IpszTimerName); 

    Параметры Ipsa и IpszTimerName определяют соответственно атрибуты защиты и 
имя данного объекта ядра. Если параметр bManualReset равен TRUE, когда таймер 
свободен, пробуждаются все потоки, ожидающие этот таймер, а если FALSE — 
пробудится только один поток (когда таймер перейдет в свободное состояние). 

    XWINDOWS/ В Windows_95^ нет ни одной из функций, о которых рассказано в 
этом разделе. Если Вы напишете программу, используя эти функции, то в Windows 
95 она даже не загрузится. 

    Разумеется, любой процесс может получить свой («процессо-зависимый») 
описатель существующего объекта «ожидаемый таймер», вызвав OpenWaitableTimer. 

    HANDLE OpenWaitableTimer(DWORD dwDesiredAccess, BOOL bInhentHandle, 
    LPCTSTR IpszTimerName); 
    Получив описатель ожидаемого таймера, процесс может настроить этот таймер вызовом: 
    BOOL SetWaitableTimer(HANDLE hTimer, 
    const LARGE_INTEGER *pDueTime, LONG IPeriod, 
    PTIMERAPCROUTINE pfnCompletionRoutine, 
    LPVOID IpArgToCompletionRoutine, BOOL fResume); 


    304 ______________________________Windows для профессионалов 

    Эта функция принимает довольно много параметров, в которых легко 
запутаться. Очевидно, что hTimer определяет таймер, который Вы хотите 
настроить. Следующие 2 параметра (pDueTime и IPeriod) используются совместно: 
первый задает, когда таймер должен сработать в первый раз, второй определяет, 
насколько часто это должно происходить в дальнейшем. Попробуем для примера 
установить таймер так, чтобы в первый раз он сработал 1 января 1999 года в 
1:00 РМ, а затем срабатывал каждые 6 часов: 
    
    // объявляем свои локальные переменные 
    HANDLE hTimer; 
    SYSTEMTIME st; 
    FILETIME ftLocal, ftUTC: 
    LARGE_INTEGER liUTC: 
    
    
    // создаем таймер: он автоматически сбрасывается после того, как 
    // ожидавший его поток обнаруживает, что таймер перешел в свободное 
    // состояние (т. е. "сработал") 
    hTimer = CreateWaitableTimer(NULL, FALSE. NULL); 
    
    
    // таймер должен сработать в первый раз 1 января 1999 года в 1:00 РМ 
    // по местному времени 
    st.wYear        = 1999: 
    
    
    st.wMonth       = 1;   // январь 
    st.wDayOfWeek   = 0;   // игнорируется 
    st.wDay         = 1;   // первое число месяца 
    st.wHour        = 13; // 1 РМ 
    st.wMinute      =0;   //0 минут 
    st.wSecond      =0;   //0 секунд 
    st.wMilliseconds= 0;   // 0 миллисекунд 
    
    
    SystemTimeToFileTime(&st, SftLocal); 
    
    
    // преобразуем местное время в UTC-время 
    LocalFileTimeToFileTime(&ftLocal, &ftUTC); 
    
    
    // преобразуем FILETIME в LARGE_INTEGER из-за различий в выравнивании данных 
    liUTC.LowPart = ftUTC.dwLowDateTime; 
    
    
    HUTC.HighPart = ftUTC.dwHighDateTime; 
    
    
    // устанавливаем таймер 
    SetWaitabieTimer(hTimer, &liUTC, 6 * 60 * 60 * 1000, NULL, NULL, FALSE); 
    
    
    Этот фрагмент кода сначала инициализирует структуру SYSTEMTIME, определяя 
время первого срабатывания таймера (его перехода в свободное состояние). Я 
установил это время как местное. Второй параметр представляется как 
LARGE_INTEGER * и поэтому не позволяет напрямую использовать структуру 
SYSTEMTIME. Однако двоичные форматы структур FILETIME и LARGE_INTEGER 
идентичны: обе содержат по два 32-битных значения. Таким образом, мы можем 
преобразовать структуру SYSTEMTIME в FILETIME. Другая проблема заключается в 
том, что функция SetWaitableTimer ждет передачи времени_в формате UTC 
(Coordinated Universal Time). Нужное преобразование легко осуществляется 
вызовом LocalFileTimeToFileTime. Поскольку двоичные форматы структур FILETIME 
и LARGE_INTEGER идентичны, у Вас может появиться искушение передать в 
SetWaitableTimer адрес структуры FILETIME напрямую: 

    __________________________ Глава 10. Синхронизация потоков 305 


    // устанавливаем таймер 
    
    SetWaitableTimei-ChTimer, (PLARGE_INTEGER) &ftUTC, 
    6 * 60 * 60 * 1000. NULL, NULL, FALSE): 
    
    В сущности, разбираясь с этой функцией, я так и поступил. Но это большая 
ошибка! Хотя двоичные форматы структур FILETIME и LARGE_INTEGER совпадают, 
выравнивание этих структур осуществляется по-разному. Адрес любой структуры 
FILETIME должен соответствовать~четной 32-битной границе, а адрес любой 
структуры LARGE_INTEGER — четной 64-битной границе. Вызов SetWaitableTimer с 
передачей ей структуры FILETIME может сработать корректно, но может и не 
сработать — все зависит от того, попадет ли начало структуры FILETIME на 
четную 64-битную границу. В то же время компилятор гарантирует, что структура 
LARGE_INTEGER всегда будет начинаться на четной 64-битной границе, и поэтому 
правильнее скопировать элементы FILETIME в элементы LARGE_INTEGER, а затем 
передать в SetWaitableTimer адрес именно структуры LARGE_INTEGER. 

    /ВНИМАИИЕ\ 

    Процессоры х86 всегда «молча» обрабатывают ссылки на невыровненные дан- i 
ные. Поэтому передача в SetWaitableTimer адреса структуры FILETIME будет 
срабатывать, если приложение выполняется на машине с процессором х86. Однако 
другие процессоры (Alpha, MIPS и PowerPC) в таких случаях генерируют 
исключение EXCEPTION_DATATYPE_MISALIGNMENT, которое приводит к завершению 
Вашего процесса. Ошибки, связанные с выравниванием данных, — самый серьезный 
источник проблем при переносе программного кода (корректно работавшего на 
процессорах х86) на другие процессорные платформы. Так что, обратив внимание 
на проблемы выравнивания данных сейчас. Вы сэкономите себе месяцы труда при 
переносе программы на другие платформы в будущем! 

    Чтобы разобраться в том, как заставить таймер срабатывать каждые 6 часов 
(начиная с 1:00 РМ 1 января 1999 года), рассмотрим параметр IPeriod функции 
SetWaitableTimer. Этот параметр определяет последующую частоту срабатывания 
таймера (в миллисекундах). Чтобы установить 6 часов, я передал значение, 
равное 21 600 000 мс (т. е. 6 часов * 60 минут * 60 секунд * 1000 миллисекунд). 
    О последних трех параметрах функции SetWaitableTimer мы поговорим ближе к 
концу этого раздела, а сейчас продолжим обсуждение второго и третьего 
параметров. Вместо того, чтобы устанавливать время первого срабатывания 
таймера в абсолютных единицах, Вы можете задать его в относительных (в 
наносекундах); при этом число должно быть отрицательным. Следующий код 
демонстрирует, как установить таймер на первое срабатывание через 5 секунд 
после вызова SetWaitableTimer. 
    
    // объявляем свои локальные переменные 
    HANDLE hTimer; 
    LARGE_INTEGER li; 
    // создаем таймер', он автоматически сбрасывается после того, как 
    // ожидавший его поток обнаруживает, что таймер перешел в свободное 
    // состояние (т, е. "сработал") 
    hTi^er"= CreaLeWaitableTimer(NULL, FALSE, NULL): 
    // таймер должен сработать через 5 секунд после вызова SetWaiLableTimer 
    const int nNanosecondsPerSecond = 1000000: 
    __int64 qwTlmeFroniNowInNanoseconds = 5 * nNanosecondsPerSecond; 
    
    
    см. след. стр. 

    306 _______________________________Windows для профессионалов 
    

    // делаем полученное значение отрицательным, чтобы SetWaitableTimer 
    // знала: нам нужно относительное, а не абсолютное время 
    qwTimeFromNowInNanoseconds = -qwTimeFromNowInNanoseconds; 
    
    
    // копируем относительное время из учетверенного слова (__lnt64) 
    
    
    // в LARGE_INTEGER 
    
    
    li.LowPart = (DWORD) (qwTimeFromNowInNanoseconds & OxFFFFFFFF); 
    
    
    li.HighPart = (LONG) (qwTimeFromNowInNanoseconds » 32); 
    
    
    // передача отрицательного значения во втором параметре сообщает 
    // функции, что таймер должен сработать через время, относительное 
    // моменту вызова функции 
    SetWaitableTimer(hTimer, &li, 6 * 60 * 60 * 1000, NULL, NULL, FALSE): 
    
    
    Иногда нужно, чтобы таймер сработал только раз. Для этого достаточно 
передать 0 в параметре IPeriod. В итоге через определенное (абсолютное или 
относительное) время таймер перейдет в свободное состояние и уже больше 
никогда не сработает. После этого можно либо вызвать CloseHandle, чтобы 
закрыть таймер, либо переустановить таймер в новый режим повторным вызовом 
SetWaitableTimer с другими параметрами. 
    0'кэй, теперь, когда Вы научились настраивать таймер, перейдем к следующим 
двум параметрам функции SetWaitableTimer. pfnCompletionRoutine и 
IpArgToCompletionRoutine. Обычно в них передают NULL — именно так я и поступил 
в предыдущих фрагментах кода. Но у Вас есть возможность поставить в очередь 
асинхронный вызов процедуры (asynchronous procedure call, APC) для потока, 
вызывающего SetWaitableTimer в момент, когда таймер свободен. (Подробное 
рассмотрение асинхронных вызовов процедур я отложу до главы 15.) Для этого 
нужно реализовать соответствующую АРС-функцию и передать ее адрес в 
SetWaitableTimer. Упомянутая функция должна выглядеть примерно так: 
    
    VOID APIENTRY TimerAPCRoutine(LPVOID IpArgToCompletionRoutine, 
    DWORD dwTimerLowValue, DWORD dwTimerHighValue) { 
    
    // здесь делаем то. что нужно 
    } 

    Я назвал эту функцию TimerAPCRoutine, но Вы можете назвать ее как угодно. 
Она вызывается с использованием того же потока, что обратился к 
SetWaitableTimer в момент срабатывания таймера, — но только если вызывающий 
поток находится в «тревожном» (alertable) состоянии, т. е. ожидает этого в 
вызове одной из функций: SkepEx, WaitForSingleObjectEx, 
WaitForMultipkObjectsEx, MsgWaitForMultipleObjectsEx или SignalObjectAndWait. 
Если же поток этого не ожидает в любой из перечисленных функций, система не 
поставит в очередь АРС-функцию таймера, и поток не узнает о данном событии, 
связанном с таймером. Для примера возьмем следующий код: 

    HANDLE hTimer = CreatewaitableTimer(NULL, FALSE, NULL); 
    SetWaitableTimer(hTimer. ..., TimerAPCRoutine, ...); 
    WaitForSingleObjectEx(hTimer. INFINITE, TRUE); 

    Никогда не пишите такой код, потому что вызов WaitForSingleObjectEx нагеле 
заставляет дважды ожидать таймер. Когда таймер перейдет в свободное состояние, 
поток пробудится, что выведет его из «тревожного» состояния, и вызова 
АРС-функции не последует. Но, как я уже говорил, АРС-функции редко используют 
совместно с ожидаемыми таймерами, так как всегда можно дождаться перехода 
таймера в свободное состояние, а затем сделать то, что нужно. Единственный 
случай, когда АРС-функции действительно следует использовать 

    _________________________Глава 10. Синхронизация потоков 307 

    совместно с таймерами, — программа требует, чтобы поток, если он занят 
какой-то работой, пропускал события таймера. 
    Если в момент срабатывания таймера Ваш поток находится в одной из 
перечисленных ранее функций, система заставляет его вызвать процедуру 
обратного вызова. Первый ее параметр совпадает с параметром 
IpArgToCompletionRoutine, передаваемым в функцию SetWaitable Timer. Это 
позволяет передавать в TimerAPCRoutine какие-либо данные (обычно указатель на 
определенную Вами структуру). Остальные два параметра, dwTimerLowValue и 
dwTimerHigh Value, задают время срабатывания таймера. Код, приведенный ниже, 
демонстрирует, как принять эту информацию и показать ее пользователю: 
    
    VOID APIENTRY TimerAPCRoutine(LPVOID IpArgToCompletionRoutine, 
    D^RD dwTimerLowValue, DWORD dwTimerHighValue) { 
    
    FILETIME ftUTC, ftLocal; 
    SYSTEMTIME st; 
    TCHAR szBuf[256]: 
    
    // записываем время в структуру FILETIME 
    ftUTC.dwLowDateTime = dwTimerLowValue; 
    
    ftUTC.dwHighDateTime = dwTimerHighValue; 
    
    // преобразуем UTC-время в местное 
    FileTimeToLocalFileTime(&ftUTC, &ftLocal); 
    
    // преобразуем структуру FILETIME в структуру SYSTEMTIME, 
    // как того требуют функции GetDateFormat и GetTimeFormat 
    FileTimeToSystemTime(&ftLocal, &st); 
    
    // преобразуем строку с датой и временем, в которое 
    // сработал таймер 
    GetDateFormat(LOGALE_USER_DEFAULT, DATE_LONGDATE, 
    &st, NULL, szBuf, sizeof(szBuf) / sizeof(TCHAR)); 
    _tcscat(szBuf. __TEXT(" ")); 
    GetTimeFormat(LOCALE_USER_DEFAULT, 0, 
    &st, NULL, _tcschr(szBuf, 0), 
    sizeof(szBuf) / sizeof(TCHAR) - _tcslen(szBuf)); 
    // показываем время пользователю 
    MessageBox(NULL, szBuf, "Timer went off at...", MB_OK); 
    } 

    И о последнем параметре функции SetWaitableTimer — fResume. Он полезен на 
компьютерах с поддержкйй режима сна. Обычно в нем передают FALSE, и в 
приведенных ранее фрагментах кода я тоже делал так. Но если Вы, скажем, пишете 
программу, подобную Schedule-1-, которая позволяет устанавливать таймеры для 
напоминания о запланированных встречах, то должны передавать в этом параметре 
TRUE. Когда таймер сработает, машина выйдет из режима сна (если она находилась 
в нем), и пробудятся потоки, ожидавшие этот таймер. Далее программа сможет 
проиграть какой-нибудь WAV-файл и вывести окно с напоминанием о предстоящей встрече. 
    Рассмотрение ожидаемых таймеров было бы неполным, пропусти мы функцию CancelWaitableTimer. 

    BOOL CancelWaitableTimer(HANDLE hTimer); 


    308 ._______________________________Windows для профессионалов 

    Эта очень простая функция принимает описатель таймера и отменяет его, 
после чего тот уже никогда не сработает, — если только Вы не переустановите 
его повторным вызовом SetWaitableTimer. Кстати, если Вам понадобится 
перенастроить таймер, то вызывать CancelWaitableTimer перед повторным 
обращением к SetWaitableTimer не требуется; каждый вызов SetWaitableTimer 
автоматически отменяет предыдущие настройки перед установкой новых. 
    Любой, мало-мальски опытный Windows-программист непременно поинтересуется 
различиями ожидаемых таймеров и таймеров User (устанавливаемых через функцию 
SetTimer). Так вот, главное отличие в том, что ожидаемые таймеры реализованы в 
ядре, а значит, не столь тяжеловесны, как таймеры User. Кроме того, это 
означает, что ожидаемые таймеры — объекты защищенные. 
    Таймеры User генерируют сообщения WM_TIMER, посылаемые тому потоку, 
который вызвал SetTimer (в случае таймеров с обратной связью) или создал 
определенное окно (в случае оконных таймеров). Таким образом, о срабатывании 
таймера User уведомляется только один поток. А ожидаемого таймера может ждать 
любое число потоков, и, если этот таймер со сбросом вручную, при его 
освобождении могут пробуждаться сразу несколько потоков. 
    Если в ответ на срабатывание таймера Вы собираетесь выполнять какие-то 
операции, связанные с пользовательским интерфейсом, то, по-видимому, будет 
легче структурировать код под таймеры User. Это связано с тем, что применение 
ожидаемых таймеров требует от потоков ожидания не только сообщений, но и 
объектов ядра4. Наконец, при использовании ожидаемых таймеров Вы будете 
получать уведомления скорее всего в режиме «реального времени», т. е. по 
истечении заданного интервала. Но сообщения WM_TIMER, как поясняется в главе 
11, всегда имеют наименьший приоритет и принимаются, только когда в очереди 
потока нет других сообщений. Помните, что ожидаемый таймер обрабатывается так 
же, как и любой другой объект ядра: если он сработал. Ваш поток, ожидающий 
его, обязательно пробуждается. 

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

    Обычно для приостановки потоков (вплоть до выполнения определенных 
условий) вызывают функции WaitForSingleObject или WaitForMultipleObjects. Но 
есть еще несколько функций, позволяющих добиться той же цели. Их-то мы сейчас 
и обсудим. 

    Функция Sleep 

    Простейшая из упомянутых функций: 

    VOID Sleep(DWORD cMilliseconds); 

    приостанавливает исполнение вызвавшего ее потока на столько миллисекунд, 
сколько задано в параметре cMilliseconds. Заметьте: она позволяет потоку 
отдавать системе остаток неиспользованного кванта процессорного времени. Даже 
вызов Sleep с параметром cMilliseconds, равным О, заставляет процессор 
прекратить исполнение текущего потока и переключиться на другой поток. Этот 
прием подробно рассмотрен в разделе, посвященном программе CritSecs. 
    4. Если Вы хотите реструктуризовать свой код, знайте, что функция 
MsgWaitForMultipleObjects как раз для этого и предназначена. 

    __________________________Глава 10. Синхронизация потоков 309 


    Асинхронный файловый ввод/вывод 

    При асинхронном файловом вводе/выводе поток начинает операцию чтения или 
записи и не ждет ее окончания. Например, если программе нужно загрузить в 
память большой файл, она может сообщить системе сделать это за нее. И пока 
система грузит файл в память, программа спокойно занимается другими задачами — 
создает окна, инициализирует внутренние структуры данных и т. д. Закончив, 
программа приостанавливает себя и ждет уведомления от системы о завершении 
загрузки файла. 
    Надо сказать, объекты «файл» являются синхронизируемыми объектами ядра; а 
это означает, что Вы можете вызывать WaitForSingleObject и передавать ей 
описатель какоголибо файла. Пока система занимается асинхронным 
вводом/выводом, объект «файл» пребывает в занятом состоянии. Как только 
файловая операция заканчивается, система изменяет состояние объекта «файл», он 
становится свободным, и поток узнает о завершении файловой операции. С этого 
момента поток возобновляет выполнение. 
    Асинхронный файловый ввод/вывод подробно рассматривается в главе 15. 

    Функция WaitForInputIdle 

    Поток может приостановить свое выполнение и вызовом WaitForInputIdle: 

    DWORD WaitForInputIdle(HANDLE hProcess, DWORD dwTimeout); 

    Эта функция ждет, пока у процесса, идентифицируемого описателем hProcess, 
не опустеет очередь_ввода^в.потоке,^ создавшем первое окно приложения. 
WaitForInputIdle полезна для применения, например, в родительском процессе, 
который порождает дочерний для выполнения какой-либо нужной ему работы. Когда 
один из потоков родительского процесса вызывает CreateProcess, он продолжает 
выполнение и в то время, пока дочерний процесс инициализируется. Этому потоку 
может понадобиться описатель окна, создаваемого дочерним процессом. Но тогда 
он должен ждать окончания инициализации, выполняемой дочерним процессом, и 
единственная возможность узнать о моменте окончания инициализации дочернего 
процесса — ждать, когда тот прекратит обработку любого ввода. Поэтому после 
вызова CreateProcess поток родительского процесса должен вызвать WaitForInputIdle. 
    Эту функцию можно применить и когда Вы хотите имитировать в программе 
нажатие каких-либо клавиш. Допустим, Вы отправили в основное окно приложения сообщения: 
    
    WM_KEYDOWN         с виртуальной клавишей VK_MENU 
    WM_KEYDOWN         с виртуальной клавишей VK_F 
    WM_KEYUP            с виртуальной клавишей VK_F 
    WM_KEYUP            с виртуальной клавишей VK_MENU 
    WM_KEYDOWN         с виртуальной клавишей VK_0 
    WM_KEYUP            с виртуальной клавишей VK_0 
    
    Эта последовательность дает тот же эффект, что и нажатие клавиш Alt+F, О, 
— в большинстве англоязычных приложений это вызывает команду Open из меню 
File. Выбор данной команды открывает диалоговое окно; но, прежде чем оно 
появится на экране, Windows должна загрузить шаблон диалогового окна из файла 
и «пройтись» по всем элементам управления в шаблоне, вызывая для каждого из 
них функцию CreateWindow. Разумеется, на это уходит какое-то время. Поэтому 
приложение, асинхронно отправившее сообщения типа WM_KEY*, теперь может 
вызвать WaitForInputIdle и таким образом перейти в режим ожидания до того, как 
Windows закончит создание диалогового окна и оно будет готово к приему данных 
от пользователя. Далее программа может передать диалоговому 

    310 ______________________________Windows для профессионалов 

    окну и его элементам управления сообщения о еще каких-то клавишах, что 
заставит диалоговое окно проделать те или иные операции. 
    С этой проблемой, кстати, сталкивались многие разработчики приложений для 
16разрядной Windows. Программам нужно было асинхронно передавать сообщения в 
окно, но получить точной информации о том, создано ли это окно и готово ли к 
работе, они не могли. Функция WaitForInputIdle решает эту проблему. 

    Функция MsgWaitForMultipleObjects 

    При ее вызове поток переходит в ожидание своих (предназначенных этому 
потоку) сообщений: 
    
    DWORD MsgWaitForMultipleOb]ects(DWORD dwCount, LPHANDLE IpHandles, 
    BOOL bWaitAll, DWORD dwMilliseconds, DWORD dwWakeMask); 
    
    Эта функция аналогична WaitForMultipleObjects, но имеет дополнительный 
параметр — dwWakeMask. Программа может использовать его, чтобы выяснить, нужно 
ли ей пробудиться и обработать определенные типы сообщений. Например, если 
поток хочет приостановить свое выполнение до того, как в его очереди ввода не 
появится сообщение от клавиатуры или мыши, он должен вызвать: 

    MsgWaitForMultipleObjects(0, NULL. TRUE, INFINITE, QS_KEY | QS_MOUSE); 

    В этом операторе мы не передаем в функцию описатели синхронизирующих 
объектов, так как dwCount равен 0, а IpHandles — NULL. Одновременно мы 
сообщаем функции ждать освобождения всех объектов. Но так как указан лишь один 
объект, параметр bWaitAll можно было бы заменить на FALSE, — в данном случае 
это не имеет значения. Кроме того, мы заставляем функцию ждать, пока в очереди 
ввода потока не появится сообщение от клавиатуры или мыши. 
    Последний параметр допускает те же значения, что передаются функции 
GetQueueStatus (о ней речь пойдет в главе 11). MsgWaitForMultipleObjects может 
пригодиться, когда программа должна ждать освобождения определенного объекта, 
а Вы хотите, чтобы у пользователя была возможность прервать это ожидание. Если 
программа ждет освобождения какого-либо объекта и пользователь нажимает ту или 
иную клавишу, поток пробуждается, и MsgWaitForMultipleObjects возвращает 
управление. Как Вы помните, при выполнении заданных критериев ожидания 
WaitForMultipleObjects обычно возвращает индекс освободившегося объекта (от 
WAIT_OBJECT_0 до WAIT_OBJECT_0+divCo«n<-l). Добавление параметра dwWakeMask 
подобно добавлению еще одного описателя при вызове функции. Если результат 
вызова MsgWaitForMultipleObjects удовлетворяет заданной маске, она возвращает 
WAIT_OBJECT_0+ dwCount. Как применять эту функцию на практике, Вы узнаете на 
примере программы FileChng в главе 14. 

    Функция WaitForDebugEvent 

    Операционные системы на базе Win32 предусматривают поддержку богатейших 
отладочных средств. Начиная исполнение, отладчик подключает себя к 
отлаживаемой программе, а потом просто ждет, когда система уведомит его о 
каком-нибудь отладочном событии, связанном с этой программой. Ожидание таких 
событий осуществляется через вызов: 

    BOOL WaitForDebugEvent(LPDEBUG_EVENT Ipde, DWORD dwTimeout); 

    Когда отладчик вызывает WaitForDebugEvent, его поток приостанавливается. 
Система уведомит поток об отладочном событии, разрешив функции 
WaitForDebugEvent вернуть управление. Структура, на которую указывает параметр 
Ipde, заполняется системой перед пробуждением потока отладчика. В этой 
структуре содержится информация, касающаяся только что произошедшего 
отладочного события. 

    ___________________________Глава 10. Синхронизация потоков 311 


    Функция SignalObjectAndWait 

    В Windows NT 4 появилась новая функция, которая переводит в свободное 
состояние один объект ядра и ждет другой объект ядра, проделывая все это как 
одну операцию на атомном уровне: 
    
    BOOL SignalObJectAndWait(HANDLE hObJectToSignal, HANDLE hObjectToWaitOn 
    DWORD dwMilliseconds, BOOL bAlertable); 
    
    Параметр hObjectToSignal должен идентифицировать мьютекс, семафор или 
событие; объекты любого другого типа заставят SignalObjectAndWait вернуть 
WAIT_FAILED, а функцию GetLastError— код 6 (ERROR_INVALID_HANDLE). Функция 
SignalObjectAndWait проверяет тип объекта и выполняет операции, аналогичные 
тому, что делают ReleaseMutex, ReleaseSemaphore (со счетчиком, равным 1) или ResetEvent. 

    VWINDOWS/ В Windows 95 функция SignalObjectAndWait не реализована — нет 
даже соответствующей заглушки. Если Вы напишете приложение, вызывающее эту 
функцию, Вы не сумеете загрузить его в Windows 95. 

    Параметр hObjectToWaitOn идентифицирует любой из следующих объектов ядра: 
мьютекс, семафор, событие, таймер, процесс, поток, уведомление об изменении 
файла (см. главу 14) или консольный ввод. Параметр dwMilliseconds, как обычно, 
определяет, сколько времени функция будет ждать освобождения объекта, а флаг 
bAlertable указывает, сможет ли поток в процессе ожидания обрабатывать 
пересылаемые в его очередь АРС-вызовы (АРС и «тревожный» ввод/вывод 
рассматриваются в главе 15). 
    В документации сказано, что эта функция возвращает значение типа BOOL, но 
на самом деле она возвращает одно из значений: WAIT_OBJECT_0, WAIT_TIMEOUT, 
WA"IT_FAILED, WAIT_ABANDONED или WAIT_IO_COMPLETION. 
    Функция SignalObjectAndWait — удачное добавление к Win32 API по двум 
причинам. Во-первых, освобождение одного объекта и ожидание другого — задача 
весьма распространенная, а значит, объединение двух операций в одной функции 
экономит определенное процессорное время. Каждый вызов функции, заставляющей 
поток переходить из кода, который работает в режиме User, в код, работающий в 
режиме ядра, требует примерно 600 инструкций процессора (на платформах х86), и 
поэтому для выполнения, например, такого кода: 

    ReleaseMutex(hMutex); 
    WaitForSingleObject(hEvent, INFINITE); 

    понадобится около 1200 инструкций процессора. В высокопроизводительных 
серверных приложениях SignalObjectAndWait даст заметную экономию процессорного времени. 
    Во-вторых, без функции SignalObjectAndWait ни у одного потока не было бы 
возможности узнать, что другой поток перешел в. состояние _ожидация. Знание 
таких вещей очень полезно для функций типа PulseEvent. Как я уже говорил в 
начале этой главы, PulseEvent переводит событие в свободное состояние и тут же 
сбрасывает его. Если ни один из потоков не ожидает данного объекта, событие не 
зафиксирует этот импульс (pulse). Я встречал программистов, которые пишут вот 
такой код: 

    // выполняем какие-то операции 
    SetEvent(hEventWorkerThreadDone); 
    WaitForSingleObJect(hEventMoreWorkToBeDone, INFINITE): 
    // выполняем еще какие-то операции 


    312 ______________________________Windows для профессионалов 

    Этот фрагмент кода выполняется рабочим потоком, который проделывает 
какие-то операции, а затем вызывает SetEvent, чтобы сообщить (другому потоку) 
об окончании своих операций. В то же время в другом потоке имеется код: 

    Wait Fo rSing leOb ject(hEventWo r kei-Th read Done); 
    PulseEvent(hEventMoreWorkToBeDone); 

    Приведенный ранее фрагмент кода рабочего потока порочен по самой своей 
сути, так как будет работать ненадежно. Ведь вполне вероятно, что после того, 
как рабочий поток обратится к SetEvent, немедленно пробудится другой поток и 
вызовет PulseEvent. Проблема здесь в том, что рабочий поток уже вытеснен и 
пока еще не получил шанса на возврат из вызова SetEvent, не говоря уж о вызове 
WaitForSingleObject. В итоге рабочий поток не сможет своевременно освободить 
событие hEventMoreWorkToBeDone. 
    Но если Вы перепишете код рабочего потока, применив SignalObjectAndWaih 

    II выполняем какие-то операции 

    SignalOb:]ectAndWait(hEventWorkerThreadDone, 
    hEventMoreWorkToBeDone, INFINITE, FALSE); 
    
    // выполняем еще какие-то операции 
    
    то код будет работать надежно, поскольку освобождение и ожидание 
реализуются на атомном уровне. И когда пробудится тот, другой поток, можете 
быть абсолютно уверены, что рабочий поток ждет события hEventMoreWorkToBeDone, 
а значит, он обязательно заметит импульс, «приложенный» к событию. 

    Семейство /nfer/oc/ced-функций 

    Последние 5 функций, которые мы рассмотрим: Interlockedlncrement, 
InterlockedDecrement, InterlockedExchange, InterlockedExchangeAdd и InterlockedCompareExchange. 

    LONG InterlockedIncrement(PLONG pIValue); 
    LONG InterlockedDecrement(PLONG pIValue); 
    LONG InterlockedExchange(PLONG pITarget, LONG IValue); 
    LONG InterlockedExchangeAdd(PLONG plAddend, LONG llncrement): 
    PVOID InterlockedCompareExchange(PVOID *Destination, PVOIO Ехспвиуи PVuiLi 
Luinpci n-iu/. 

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

    _________________________Глава 10. Синхронизация потоков 313 
    

    // переменная типа LONG, доступная нескольким потокам 
    LONG IValue; 
    // неправильный способ увеличения переменной на единицу 
    lValue++; 
    // правильный способ 
    Interlockedlncrement(&lValue); 
    
    Обычно переменные защищают от повреждения, применяя одну из форм 
синхронизации (например, с помощью мьютекса). Но так как переменными типа LONG 
манипулируют очень часто;,.Microsoft специально добавила в Win32 API пять 
упомянутых функций. 
    Вызовы функций Interlockedlncrement и InterlockedDecrement приводят 
соответственно к увеличению и уменьшению переменной типа LONG на 1; ее адрес 
передается в параметре pIValue. Они не возвращают нового значения переменной, 
а сообщают код сравнения этого значения с нулем. Если в результате увеличения 
или уменьшения переменная обнулилась, функции возвращают б. Если результат 
меньше или больше 0, они возвращают произвольное отрицательное или 
положительное число, которое почти никогда не совпадает с истинным значением 
самой переменной. Я использовал эти функции в модулях MULTINST.C и MODUSE.C 
(см. главу 12). 
    Функция InterlockedExchange позволяет заменить текущее значение переменной 
типа LONG (адрес которой передается в параметре pITarget) другим значением, 
передаваемым в параметре IValue. И опять-таки функция защищает общую 
переменную от попыток одновременного обращения к ней со стороны других потоков. 
    Еще одно важное соображение по поводу этих функций. Нет ни одной 
Interlockedфункции, позволяющей одному потоку прочитать значение переменной 
(типа LONG) в тот момент, когда другой пытается изменить ее. Почему? Да 
потому, что не нужно. Если один поток вызвал Interlockedlncrement в то время, 
как другой читает содержимое той же переменной, ее значение, прочитанное 
вторым потоком, всегда будет достоверным. Он получит значение переменной либо 
до, либо после того, как Interlockedlncrement изменит переменную. Поток, 
конечно, не знает, какое именно значение он прочитал, но главное, что оно 
корректно и не является некоей произвольной величиной, полученной из-за 
вмешательства в операцию увеличения. 
    Последние две функции — новинка Windows NT 4. Они используют преимущества 
команд, существующих только в процессорах Intel 486 и старше. Microsoft 
добавила эти ^TirKEmT^Windows NT 4 потому, что данная операционная система 
требует минимум 486го процессора; 386-е больше не поддерживаются. По этой же 
причине они, напротив, отсутствуют в Windows 95. 
    Функция InterlockedExchangeAdd аналогична Interlockedlncrement и 
InterlockedDecrement за тем исключением, что позволяет добавлять или вычитать 
из переменной типа LONG любое число, а не только 1. Чтобы вычесть какое-то 
число, Вы передаете во втором параметре соответствующее отрицательное значение: 

    LONG IVal = 5; 
    LONG lOngVal = InterlockedExchangeAdd(&lVal, -2); 
    // теперь lOngVal содержит 5, a IVal - 3 

    314 ______________________________Windows для профессионалов 

    Функция InterlockedCompareExchange выполняет операцию сравнения и 
присвоения на атомном уровне. Она сравнивает текущее значение переменной типа 
LONG (на которую указывает параметр Destination) со значением, передаваемым в 
параметре Comperand. Если значения совпадают, в параметр Destination заносится 
содержимое параметра Exchange, a если нет, значение Destination не меняется. 
    Показанный ниже код сравнивает текущее содержимое Destination с числом 5, 
обнаруживает, что они совпадают, и меняет значение этого параметра на 500: 

    LONG IVal = 5; 
    LONG lOrigVal = (LONG) InterlockedConipareExchange((PVOID) &lVal, 
    (PVOID) 500, (PVOID) 5); 
    // теперь lOrigVal содержит 5, a IVal - 500 

    А вот другой пример: 

    LONG IVal = 5; 
    LONG lOrigVal = (LONG) InterlockedCompareExchange((PVOID) &lVal, 
    (PVOID) 500, (PVOID) 10); 
    // теперь и Юг-igVal, и IVal содержат значение 5 
    
    Обратите внимание на приведение типов к PVOID в вызовах 
InterlockedCompareExchange. Увы, без этого не избежать предупреждений от 
компилятора. По неизвестным мне причинам Microsoft предпочла, чтобы у функции 
InterlockedCompareExchange был прототип, принимающий параметры типа PVOID 
вместо обычного PLONG. Кроме того, возвращаемое значение приходится приводить 
к типу LONG. 
