    потоки 

    глава  4 

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

    В каких случаях потоки создаются 

    Поток (thread) определяет последовательность исполнения кода в процессе. 
При инициализации процесса система всегда создает первичный поток. Начинаясь 
со стартового кода из стандартной библиотеки С (который в свою очередь 
вызывает функцию WinMain из Вашей программы), он «живет» до того момента, 
когда WinMain возвращает управление стартовому коду и тот вызывает функцию 
ExitProcess. Большинство приложений обходится единственным, первичным потоком. 
Однако процессы могут создавать дополнительные потоки, что позволяет 
добиваться минимального простоя процессора и работать эффективнее. 
    Например, в электронных таблицах нужно пересчитывать данные при изменении 
пользователем содержимого ячеек. Пересчет сложной таблицы может занять 
несколько секунд, но тщательно продуманное приложение не должно тратить время 
на эту операцию после каждого изменения. Вместо этого следует выделить 
функциональный блок повторных расчетов в отдельный поток с более низким (чем у 
первичного) приоритетом. Таким образом, пока пользователь набирает данные, 
исполняется первичный поток, т. е. система не выделяет процессорного времени 
потоку, отвечающему за пересчет. А возникнет пауза — даже крошечная, — система 
приостановит выполнение первичного потока, ожидающего ввода данных, и отдаст 
процессорное время другому потоку (в нашем случае — блоку повторных расчетов). 
При возобновлении ввода данных первичный поток, имеющий более высокий 
приоритет, вытеснит поток, занимающийся пересчетом. Создание дополнительного 
потока делает программу «отзывчивой» на действия пользователя — да и 
реализация такого алгоритма довольно проста. 
    С той же целью можно создать дополнительный поток и в текстовом процессоре 
для фоновой разбивки документа на страницы в паузах между вводом текста. 
Например, в 16разрядной Windows текстовому процессору Microsoft Word 
приходится моделировать многопоточность, но в версии для Win32 он просто 
порождает вспомогательный поток для раз^ивки~документа на страницы. Первичный 
поток отвечает за обработку информации, вводимой пользователем, а фоновый — за 
определение концов страниц. 

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

    Полезно создать отдельный поток и для обработки заданий на печать. Тогда 
пользователь, отправив документ на печать, продолжил бы работу с программой. 
    Еще пример. При выполнении длительной операции программы обычно открывают 
диалоговое окно, позволяющее эту операцию отменять. Скажем, при копировании 
файлов Explorer выводит на экран диалоговое окно, где, кроме индикатора 
прогресса операции, содержится и кнопка Cancel (Отмена). Щелкнув ее. Вы 
отмените копирование файла (или файлов). 
    В 16-разрядной Windows для этого приходилось из цикла File Copy 
периодически вызывать функцию PeekMessage, но делать это можно было только в 
паузах между чтением и записью. При считывании большого блока данных реакция 
программы на «нажатие» кнопки Cancel была слишком запоздалой: если файл 
считывался с дискеты, могло пройти несколько секунд. Из-за такого запаздывания 
пользователь, полагая, что система почему-то не «поняла» его, мог несколько 
раз щелкнуть кнопку — я и сам не раз ловил себя на этом. 
    Теперь представьте, что код, отвечающий за копирование файлов, выделен в 
свой поток. Вам больше не надо расставлять по всему коду вызовы PeekMessage — 
поток, обеспечивающий работу пользовательского интерфейса, действует 
независимо. А значит, щелчок кнопки Cancel даст немедленный результат. 
    На основе принципа многопоточности можно также создавать программы, 
моделирующие события реальной жизни. В главе 10 я продемонстрирую модель 
супермаркета. Каждый покупатель представлен в ней отдельным потоком, так что 
теоретически все они независимы друг от друга и входят, покупают и выходят 
тогда и как им угодно. 
    Хотя подобную модель в общем-то можно реализовать подобным образом, здесь 
нас подстерегает ряд потенциальных проблем. Во-первых, в идеале надо бы 
выполнять каждый поток (соответствующий одному покупателю) на отдельном 
процессоре. Но сами понимаете, это совершенно нереально, поэтому при 
моделировании нужно учитывать время, затрачиваемое системой на вытеснение 
первого потока и активизацию второго. Например, если в модели всего 2 потока, 
а у компьютера 8 процессоров, система сможет закрепить каждый поток за 
отдельным процессором. Если же в модели 1000 потоков, системе придется 
постоянно «коммутировать» их между 8 процессорами. Так что при распределении 
большого количества потоков между несколькими процессорами станет заметным 
время, требуемое на переключение потоков. Если моделируется продолжительный 
процесс, этот эффект проявляется относительно слабо. Но при моделировании 
быстротечных процессов перераспределение потоков может занять едва ли не 
львиную долю времени выполнения всей программы. 
    Во-вторых, операционной системе самой нужны потоки, исполняемые «вместе» с 
потоками, принадлежащими программам. Значит, надо учитывать и время, 
затрачиваемое на переключение этих дополнительных потоков; оно почти наверняка 
повлияет на общие результаты. 
    И, в-третьих, моделировать имеет смысл, только если Вы периодически 
фиксируете какие-то параметры процесса в его развитии. Скажем, в модели 
супермаркета каждый покупатель, входя в магазин, регистрируется в списке, а 
внесение элемента в список тоже занимает время (отнимая его у собственно 
моделируемого процесса). Вспомните, что, согласно принципу неопределенности 
Гейзенберга, чем точнее определяется один квант, тем больше ошибка при 
измерении другого'. Это в полной мере относится и к нашим рассуждениям. 

    И в каких случаях потоки не создаются 

    Ох как часто программисты, впервые получая доступ к среде, поддерживающей 
многопоточность, впадают чуть ли не в исступление: «Если бы я раньше мог 
работать с потоками, насколько проще было бы писать программы!» И по какой-то, 
непонятной мне причине они начинают дробить свои программы на куски, которые 
можно было бы исполнять как отдельные потоки. Но... 
    1. Вернер Гейзенберг, разумеется, разрабатывал свою теорию для квантовой 
механики, а не для компьютерной науки. 

    Глава 4. Потоки 53 

    Потоки — вещь невероятно полезная, когда ими пользуются с умом. Увы, решая 
старые проблемы, можно создать себе новые. Допустим, Вы разрабатываете 
текстовый процессор и хотите выделить функциональный блок, отвечающий за 
распечатку, в отдельный поток. Идея вроде неплоха: пользователь, отправив 
документ на распечатку, может сразу вернуться к редактированию. Но задумайтесь 
вот над чем: значит, информация в документе может быть изменена при распечатке 
документа? Как видите, теперь перед Вами совершенно новая проблема, с которой 
прежде сталкиваться не приходилось. Тут-то и подумаешь, а стоит ли выделять 
печать в отдельный поток, зачем искать лишних приключений? А что если при 
распечатке разрешить редактирование любых документов, кроме того, что в данный 
момент печатается, — иными словами, запретим изменение печатаемого документа. 
Или так: скопируем документ во временный файл и отправим на печать именно его, 
а пользователь пусть редактирует оригинал в свое удовольствие. Когда 
распечатка временного файла закончится, мы его удалим — вот и все. 
    Еще одно «узкое» место, где неправильное применение потоков может привести 
к появлению проблем, — разработка пользовательского интерфейса в приложении. В 
большинстве программ все компоненты пользовательского интерфейса (окна) 
формируются в одном потоке. Например, если Вы создаете диалоговое окно, какой 
смысл формировать список одним потоком, а кнопку — другим? 
    Рассмотрим эту проблему подробнее и предположим, что в программе имеется 
элемент управления — список, сортирующий данные всякий раз, как в него что-то 
добавляют (или удаляют). Сортировка может занять несколько секунд, и поэтому 
Вы, допустим, выделили этот элемент управления в отдельный поток. Тогда 
пользователь вроде бы может работать с другими элементами управления, пока 
поток, принадлежащий списку, занят сортировкой. 
    Но эта идея — не из лучших. Во-первых, каждый поток, создающий то или иное 
окно, должен содержать в себе цикл GetMessage. Во-вторых, если поток, 
принадлежащий списку, будет содержать этот цикл. Вы скорее всего столкнетесь с 
проблемами синхронизации потоков. Их в принципе можно решить, закрепив за 
списком специальный поток, который только и делает, что сортирует элементы в 
фоновом режиме. 
    Ну а теперь, после всего, что я тут наговорил, хочу Вас немного успокоить. 
Закрепление объектов пользовательского интерфейса за отдельными потоками редко 
приносит хоть какую-то пользу. Каждый процесс в системе управляет своим 
интерфейсом с помощью отдельного потока. Скажем, у приложения Calculator свой 
поток, который создает и манипулирует всеми окнами этой программы, а у 
приложения Paint — свой, с аналогичными функциями. Такая схема обладает 
наибольшей отказоустойчивостью. Если поток калькулятора войдет в бесконечный 
цикл, это не скажется на потоке Paint. Разительное отличие от 16-разрядной 
Windows, не так ли? В ней, если виснет одно приложение, виснет вся система. А 
системы, построенные на основе Win32, позволяют переключиться из зависшего 
приложения Calculator и перейти в тот же Paint. Но подробнее об этом — в главе 11. 
    Вероятно, лучший пример программы, создающей окна во множестве потоков, — 
Explorer. Если Вы используете одно окно Explorer и поток для этого окна входит 
в бесконечный цикл, оно становится «недееспособным», но другие окна Explorer 
остаются «на плаву». И это его свойство очень важно, потому что пользователи 
терпеть не могут, когда оболочка операционной системы перестает реагировать на 
их команды. 
    Еще одно применение многопоточности в компонентах GUI — приложения с 
многодокументным интерфейсом (multiple document interface, MDI), где каждое 
дочернее MDIокно поддерживается отдельным потоком. Если один из таких потоков 
входит в бесконечный цикл или начинает выполнять длительную операцию, 
пользователь может переключиться в другое дочернее MDI-OKHO и поработать с 
ним, пока предыдущее выполняет поставленную задачу. Это настолько удобно, что 
в Win32 даже есть специальная функция, 

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

    дающая результат, аналогичный тому, как если бы Вы создали дочернее 
MDI-OKHO, передав сообщение WMJUDICREATE окну MDIClient: 

    HWND CreateMDIWindow(LPTSTR IpszClassName, LPTSTR IpszWindowName, DWORD 
dwStyle, int x, int y, int nWidth, int nHeight, HWND hwndParent. HINSTANCE 
hinst, LONG IParam); 

    Отличие лишь в том, что CreateMDIWindow позволяет создавать дочернее 
MDI-OKHO вместе со своим потоком. 
    В общем, мораль этого затянувшегося вступления такова: многопоточность 
следует использовать разумно. Не создавайте несколько потоков только потому, 
что это возможно. Многие полезные и мощные программы по-прежнему строятся на 
основе одного первичного потока, принадлежащего процессу. И если у Вас еще не 
пропало желание работать с несколькими потоками, читайте дальше. 

    Ваша первая функция потока 

    Потоки начинают выполнение с некоей, определенной Вами функции. У нее 
должен быть такой прототип: 

    DWORD WINAPI YourThreadFunc(LPVOID IpvThreadParm); 

    Как и WinMain, эта функция операционной системой не вызывается. Вместо 
этого система обращается к внутренней функции, содержащейся в KERNEL32.DLL (а 
не в стандартной библиотеке С). Я называю эту функцию StartOfThread— как она 
называется на самом деле, значения не имеет. Вот ее синтаксис: 

    void StartOfThread(LPTHREAD_START_ROUTINE IpStartAddr. LPVOID 
IpvThreadParm) { 
    __try { 
    DWORD dwThreadExitCode = IpStartAddr(lpvThreadParm); 
    ExitThread(dwThreadExitCode); 
    } 
    „except (UnhandledExcept ion Filter (GetExceptionInformationO)) { 
    ExitProcess(GetExceptionCodeO); 
    } 
    ) 


    К чему приводит вызов StartOfThread^ 

    1. Ваша функция потока помещается во фрейм структурной обработки 
исключений (SEH-frame — далее для краткости SEH-фрейм), благодаря чему любое 
исключение — произойди оно в момент выполнения Вашего потока — получит хоть 
какую-то обработку, предлагаемую по умолчанию. Подробнее о структурной 
обработке исключений см. главу 16. 
    2. Система обращается к Вашей функции, передавая ей 32-битный параметр 
IpvThreadParm, который Вы ранее передали CreateThread. 
    3. Когда Ваша функция вернет управление, StartOfThread вызовет ExitThread, 
передав ей значение, возвращенное Вашей функцией. Счетчик числа пользователей 
объекта ядра «поток» уменьшится, и выполнение потока прекратится. 
    4. Если Ваш поток вызовет необрабатываемое им исключение, его обработает 
SEHфрейм, созданный «вокруг» StartOfThread. Обычно в результате этого 
появляется окно с каким-нибудь сообщением, и, когда негодующий пользователь 
щелкнет в нем кнопку ОК, StartOfThread вызовет ExitProcess и завершит весь 
процесс, а не только тот поток, в котором произошло исключение. 

    Глава 4. Потоки 55 

    Хоть я и не говорил об этом, исполнение первичного потока процесса на 
самом деле начинается с функции StartOfThread. Потом она передает управление 
стартовому коду из стандартной библиотеки С, который и вызывает WinMain из 
Вашего приложения. Но стартовый код никогда не'возвращается в StartOfThread, 
так как «под занавес» он явным образом вызывает ExitProcess. 
    А теперь мы рассмотрим атрибуты, «присуждаемые» новому потоку. 

    Стек потока 

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

    Структура CONTEXT 

    У каждого потока собственный набор регистров процессора, называемый 
контекстом потока. Эта структура с именем CONTEXT отражает состояние регистров 
процессора на момент последнего исполнения потока. Она — единственная 
структура данных в Win32, зависимая от типа процессора. В справочном файле по 
Win32 ее содержимое вообще не показано. Если Вас интересует, из каких 
элементов она состоит, загляните в файл WINNT.H — там Вы найдете несколько ее 
определений: для ;>с86, для MIPS, для Alpha и для PowerPC. Компилятор сам 
выбирает нужный вариант структуры в зависимости от типа процессора, для 
которого предназначен Ваш ЕХЕ- или DLL-модуль. 
    Когда потоку выделяется процессорное время, система инициализирует 
регистры процессора содержимым контекста и, разумеется, регистр — указатель 
команд идентифицирует адрес следующей машинной команды, необходимой для 
выполнения потока. Кроме того, в контекст включается указатель стека, который 
определяет адрес стека, принадлежащего потоку. 

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

    В многопоточной среде гораздо труднее определить время, затрачиваемое 
процессом на выполнение операций. Это связано с тем, что у процесса может быть 
поток, занятый в данный момент, скажем, пересчетом по какому-то сложному 
алгоритму, а параллельно за процессорное время конкурируют потоки из других 
процессов. И если Ваш поток постоянно вытесняют другие, Вам не удастся 
провести хронометраж своего алгоритма так же просто, как показано ниже: 
    DWORD dwStartTime = GetTickCountO; 
    // здесь выполняем какой-нибудь сложный алгоритм 
    DWORD dwElapsedTime = GetTickCountO - dwStartTime; 
    Здесь явно не помешает функция, сообщающая о времени, затраченном 
процессором на обработку данного потока, и, к счастью, в Win32 есть такая функция: 
    BODL GetThreadTimes(HANDLE hThread, LPFILETIME IpCreationTlme. 
    LPFILETIME IpExltTime, LPFILETIME IpKernelTime, 
    LPFILETIME IpUserTime); 


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


    GetThreadTimes возвращает 4 временных параметра: 

    Показатель времени              Описание 

    Время создания (Creation Time)      Момент создания данного потока. 
    Время завершения (Exit Time)       Момент выхода из данного потока. Если 
поток все еще выполняется, этот показатель имеет неопределенное значение. 
    Время выполнения ядра (Kernel Time) Время, затраченное этим потоком на 
выполнение кода операционной системы. 
    Время выполнения User (U^er Time) Время, затраченное потоком на выполнение 
кода программы. 
    С помощью этой функции можно определить время, необходимое для выполнения 
сложного алгоритма: 

    __int64 FileTimeToQuadWord(PFILETIME pFileTime) { 
    __int64 qw: 
    qw = pFileTime->dwHighDateTinie; 
    qw «= 32: 
    qw ;= pFileTime->dwLowDateTime: 
    return(qw): 
    PFILETIME QuadWordToFileTime(__int64 qw, PFILETIME pFileTime) { 
    pFileTime->dwHighDateTime = (DWORD) (qw » 32); 
    pFileTime->dwLowDateTiine  = (DWORD) (qw & OxFFFFFFFF); 
    return(pFileTime); 
    } 
    void Recalc() { 
    FILETIME ftKernelTimeStart, ftKernelTimeEnd; 
    FILETIME ftUser-TimeStart, ftUserTimeEnd: 
    FILETIME ftDummy, ftTotalTimeElapsed; 
    __int64 qwKernelTimeElapsed, qwUserTimeElapsed, 
    qwTotalTimeElapsed; 
    
    // получаем начальные показатели времени 
    GetThreadTimes(GetCurrentThread(), &rtDurnmy, &rtDummy, 
    &ftKernelTimeStart. &ftUserTimeStart): 
    // здесь выполняем сложный алгоритм 
    
    // получаем конечные показатели времени 
    GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, 
    SftKernelTimeEnd, &ftUserTimeEnd); 
    
    // получаем значения времени, затраченного на выполнение ядра и User, 
    // преобразуя начальные и конечные показатели времени из FILETIME 
    // в учетверенные слова, а затем вычитая начальные показатели из конечных 
    qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) 
    FileTimeToQuadWord(&ftKernelTimeStart): 


    _______________________________________ Глава 4. Потоки 57 
    

    qwUserTimeElapsed = FlleTimeToQuadWord(&ftUserTimeEnd) 
    FileTimeToQuadWord(&ft(JserTimeStart); 
    
    // получаем общее время, складывая время выполнения ядра и User 
    qwTotalTimeElapsed = qwKernelTi(neElapsed + qwUserTimeElapsed: 
    
    // преобразуем результат из учетверенного слова в FILETIME 
    QuadWordToFileTime(qwTotalTimeElapsed, SftTotalTimeElapsed); 
    
    // общее время записывается в qwTotalTimeElapsed 
    //ив ftTotalTimeElapsed; можно использовать любую форму 
    } 

    Позволю себе заметить, что существует еще одна функция, аналогичная 
GetThreadTimes и применимая ко всем потокам в процессе: 

    BOOL GetProcessTimes(HANDLE hProcess, LPFILETIME IpGreationTime, 
    LPFILETIME IpExitTime, LPFILETIME IpKernelTime, 
    LPFILETIME IpUserTime); 

    GetProcessTimes возвращает временные параметры, применимые ко всем потокам 
в указанном процессе. Так, время выполнения ядра будет суммой периодов 
времени, затраченного всеми потоками процесса на выполнение кода операционной системы. 
    VWINDOWS/ К сожалению, в Windows 95 вместо функций GetThreadTimes и 
GetProcessTimes \95/ стоят заглушки, и при вызове любой из них возвращается 
FALSE. Последующий вызов GetLastError дает код 120 
(ERROR_CALL_NOT_IMPLEMENTED), т. е. данные функции работают только в Windows NT. 
    Так что в Windows 95 нет надежного механизма, с помощью которого можно 
было бы определить, сколько процессорного времени выделяется потоку или процессу. 

    Функция CreateThread 

    Мы уже говорили, как при вызове CreateProcess появляется на свет первичный 
поток процесса. Если же Вы хотите, чтобы первичный поток породил 
дополнительные потоки, нужно воспользоваться другой функцией — CreateThread: 

    HANDLE CreateThread( 
    LPSECURITY_ATTRIBUTES Ipsa: 
    DWORD cbStack: 
    LPTHREAD_START_ROUTINE IpStartAddr: 
    LPVOID IpvThreadPariri; 
    DWORD fdwGreate; 
    LPDWORD IpIDThread); 

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

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

    2. Инициализирует код завершения потока (регистрируемый в объекте ядра 
«поток») идентификатором STILL_ACTIVE и присваивает счетчику простоя потока 
(thread's suspend count) единицу. Последний тоже запоминается в объекте ядра «поток». 
    3. Создает для нового потока структуру CONTEXT. 
    4. Формирует стек потока, для чего резервирует в адресном пространстве 
регион, передает ему 2 страницы физической памяти и присваивает им атрибут 
защиты PAGE_READWRITE, а второй странице (если считать снизу вверх) 
дополнительно устанавливает атрибут PAGE_GUARD. Подробнее о стеке потока см. 
главу 7. 
    5. Значения IpStartAddr и IpvThreadParm помещаются в самый верх стека — 
так, чтобы представить их параметрами, передаваемыми в StartOfThread. 
    6. Инициализирует регистры — указатель стека и указатель команд в 
структуре CONTEXT потока, так чтобы первый указывал на верхнюю границу стека, 
а второй — на внутреннюю функцию StartOfThread. 
    Ну вот, Вы и получили общее представление о функции Create Thread. А 
теперь подробно рассмотрим все ее параметры. 

    Параметр /psa 

    Параметр Ipsa является указателем на структуру SECURITY_ATTRIBUTES. Если 
Вы хотите, чтобы объекту «поток» были присвоены атрибуты защиты по умолчанию, 
передайте в этом параметре NULL. А чтобы дочерние процессы смогли наследовать 
описатель данного объекта «поток», определите структуру SECURITY_ATTRIBUTES и 
инициализируйте ее элемент bInheritHandle значением TRUE. 

    Параметр cbStack 

    Этот параметр определяет, какую часть адресного пространства поток сможет 
использовать под свой стек. Каждому потоку выделяется отдельный стек. 
CreateProcess, запуская приложение, вызывает функцию CreateThread, и та 
инициализирует первичный поток процесса. При этом CreateProcess заносит в 
параметр cbStack значение, хранящееся в самом исполняемом файле. Управлять 
этим значением позволяет ключ /STACK компоновщика: 

    /STACK: [reseri/e] [, commit} 

    Аргумент reserve определяет объем адресного пространства, который система 
должна зарезервировать под стек потока (по умолчанию 1 Мб). Аргумент commit 
задает объем физической памяти, изначально передаваемой зарезервированному 
региону стека (по умолчанию 1 страницу). (О резервировании и передаче памяти 
см. главу 7.) По мере исполнения кода в потоке Вам, весьма вероятно, 
понадобится отвести под стек больше одной страницы памяти. При переполнении 
стека возникнет исключение. (Об обработке исключений см. главу 16.) Перехватив 
это исключение, система передаст зарезервированному пространству еще одну 
страницу (или столько, сколько указано в аргументе commit). Такой механизм 
позволяет динамически увеличивать размер стека лишь по мере необходимости. 
    Обращаясь к CreateThread, можно обнулить значение параметра cbStack. В 
этом случае функция создает стек для нового потока, используя аргумент commit, 
внедренный компо_. новщиком в ЕХЕ-файл. Объем резервируемого пространства 
всегда равен 1 Мб. Это ограничение позволяет прекращать деятельность функций с 
бесконечной рекурсией. 
    Допустим, Вы пишете функцию, которая рекурсивно вызывает сама себя. 
Предположим также, что в ней «сидит жучок», приводящий к бесконечной рекурсии. 
Всякий раз, когда функция вызывает сама себя, в стеке создается новый стековый 
фрейм. И если бы система не ограничивала максимальный размер стека, 
рекурсивная функция так и вызывала бы сама себя до бесконечности, а стек 
поглотил бы все адресное пространство. Задавая же 

    ______________________________________ Глава 4. Потоки 59 

    определенный предел, система, во-первых, предотвращает разрастание стека 
до гигантских объемов и, во-вторых, позволяет Вам гораздо быстрее убедиться в 
наличии ошибок в своей программе. Приложение-пример SEHSum в главе 16 
продемонстрирует, как перехватывать и обрабатывать переполнение стека в программах. 

    Параметры IpStartAddr и IpvThreadParm 

    Параметр IpStartAddr определяет адрес функции потока, с которой должен 
будет начать работу создаваемый поток. Вполне допустимо и даже полезно 
создавать несколько потоков, у которых в качестве входной точки используется 
адрес одной и той же стартовой функции. Например, можно создать 
MDI-приложение, в котором все дочерние окна ведут себя совершенно одинаково, 
но каждое оперирует в своем потоке. Ваша функция потока должна иметь следующий прототип: 

    DWORD WINAPI ThreadFunc(LPVOID IpvThreadParm) { 
    DWORD dwResult = 0; 
    return(dwResult); 

    Параметр IpvThreadParm функции потока идентичен параметру IpvThreadParm, 
первоначально передаваемому Вами в Create Thread. Последняя лишь передает этот 
параметр по эстафете той функции, с которой начинается выполнение создаваемого 
потока. Таким образом, данный параметр позволяет передавать функции потока 
какое-либо инициализирующее значение. Оно может представлять собой или просто 
32-битное значение, или 32битный указатель на структуру данных с 
дополнительной информацией. 

    Параметр fdwCreate 

    Этот параметр определяет дополнительные флаги, управляющие созданием 
потока. Он принимает одно из двух значений: 0 (исполнение потока начинается 
немедленно) или CREATE_SUSPENDED. В последнем случае система создает поток, 
затем его стек, инициализирует элементы соответствующей структуры CONTEXT и, 
приготовившись к исполнению первой команды из функции потока, «придерживает» 
поток до последующих указаний. 
    Сразу после возврата из CreateThread и пока еще исполняется вызвавший ее 
поток, исполняется и новый поток — если только при его создании Вы не указали 
флаг CREATE_SUSPENDED.2 А поскольку новый поток выполняется одновременно с 
потоком, его породившим, вероятно возникновение ряда проблем. Рассмотрим код: 

    DWORD WINAPI FirstThread(LPVOID IpvThreadParm) { 
    int x = 0: 
    DWORD dwResult = 0, dwThreadId; 
    HANDLE hThread: 
    hThread = CreateThread(MULL, 0, SecondThread, (LPVOID) &x, 0, &dwThreadId); 
    CloseHandle(hThread); 
    return(dwResult); 
    } 

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

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

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


    DWORD WINAPI Secondrnread(LPVOID IpvTnreadParm) { DWORD dwResult = 0; 
    // выполняем здесь какую-нибудь длительную операций 
    * ((int *) IpvThreadParm) = 5; 
    return(dwResuit): 
    } 

    He исключено, что в приведенном коде FirstThread закончит свою работу до 
того, как SecondThread присвоит значение 5 переменной х из FirstThread. Если 
так и будет, SecondThread не узнает, что FirstThread больше не существует, и 
попытается изменить содержимое какого-то участка памяти с недействительным 
теперь адресом. Это неизбежно вызовет нарушение доступа: ведь стек первого 
потока был уничтожен по завершении FirstThread. Что же делать? Можно объявить 
х статической переменной, и компилятор отведет память для хранения переменной 
хне в стеке, а в разделе данных приложения (application's data section). Но 
тогда функция станет нереентерабельной. Иначе говоря, в этом случае Вы не 
смогли бы О создать два потока, выполняющих одну и ту же функцию, так как оба 
потока совместно использовали бы статическую переменную. 
    Другое решение проблемы (и его более сложные варианты) состоит в 
применении синхронизирующих объектов, речь о которых пойдет в главе 10. 

    Параметр /p/DThread 

    Последний параметр функции CreateThread — это адрес переменной типа DWORD, 
в которой функция вернет идентификатор, приписанный системой новому потоку. 
    В Windows 95 передача NULL вместо этого параметра даст ошибку. В Windows 
NT до версии 4 этот параметр также не мог быть NULL — иначе система попыталась 
бы записать идентификатор потока по адресу 0х00000000, что привело бы к 
нарушению доступа. Однако Windows NT 4 разрешает передавать NULL в параметре 
IpIDThread, если Вас не интересует идентификатор потока.                       
   Такое несоответствие между Windows 95 и Windows NT может создать 
массу про/ • \ блем для разработчиков приложений. Допустим, Вы пишете и 
тестируете програм/ВНИМАНИЕ\ ^у g windows NT 4 и при этом пользуетесь тем, что 
она допускает указывать NULL в параметре IpIDThread функции CreateThread. 
Отлично. Но вот Вы запускаете приложение в Windows 95, и оно, естественно, не 
работает. Вывод один: Вы обязаны тщательно тестировать свое приложение как в 
Windows 95, так и в Windows NT — причем в разных их версиях. 

    Завершение потока 

    Как и процесс, поток можно завершить тремя способами: 
    1. Поток самоуничтожается вызовом ExitThread (самый распространенный способ). 
    2. Один из потоков данного или стороннего процесса вызывает 
TerminateThread (этого надо избегать). 
    3. Завершается процесс, содержащий данный поток. 

    ______________________________________ Глава 4. Потоки 61 

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

    Функция ExitThread 

    Поток завершается, когда вызывается ExitThread: 

    VOID ExitThreaddlINT fuExltCode): 

    У этой функции нет возвращаемого значения, ведь после ее вызова поток 
перестает существовать. В параметр fuExitCode она помещает код завершения потока. 
    Этот способ завершения потока применяют чаще потому, что после передачи 
управления от функции потока внутрисистемной функции StartOfThread вызывается 
именно ExitThread, которой передается значение, возвращенное функцией потока. 

    Функция TerminateThread 

    Вызов этой функции также завершает поток: 

    BOOL TerminateThreacKHANDLE hThread, DWORD dwExitCode): 

    Функция завершает поток, идентифицируемый параметром hThread, и помещает 
код завершения в dwExitCode. Ее используют лишь в крайнем случае, когда 
управление потоком потеряно и он ни на что не реагирует. 
    «Гибель» потока при вызове ExitThread приводит к разрушению его стека. Но 
если он завершен функцией TerminateThread, система не уничтожает стек, пока не 
завершится и процесс, которому принадлежал этот поток. Так сделано потому, что 
другие потоки могут использовать указатели, ссылающиеся на данные в стеке 
завершенного потока. Если бы они обратились к несуществующему стеку, произошло 
бы нарушение доступа. 
    Когда поток прекращается, система уведомляет об этом все DLL-модули, 
подключенные к процессу — владельцу завершенного потока. Но при вызове 
TerminateThread такого не происходит, и процесс может быть завершен 
некорректно. Например, какой-то DLLмодуль при отключении от потока должен был 
бы сбросить все данные в дисковый файл. Не получив уведомления об отключении — 
а именно так и будет после вызова TerminateThread, — он не выполнит свою задачу. 

    Если завершается процесс 

    Функции ExitProcess и TerminateProcess, рассмотренные в главе 3, тоже 
завершают потоки. Единственное отличие в том, что они прекращают выполнение 
всех потоков, принадлежавших завершенному процессу. 

    Что происходит при завершении потока 

    А происходит вот что: 
    1. Освобождаются все описатели объектов User, принадлежавших потоку. В 
Win32 большинство объектов принадлежат процессу, содержащему поток, из 
которого они были созданы. Однако поток может владеть двумя объектами User: 
окнами и ловушками (hooks). Когда поток, создавший такие объекты, завершается, 
система уничтожает их автоматически. Прочие объекты разрушаются, только когда 
завершается владевший ими процесс. 
    2. Объект ядра «поток» переводится в свободное состояние. 

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

    3. Код завершения потока меняется со STILL_ACTIVE на код, переданный в 
функцию ExitThread или TerminateThread. 
    4. Если данный поток — последний активный поток в процессе, завершается и 
сам процесс. 
    5. Счетчик числа пользователей объекта ядра «поток» уменьшается на 1. 
    При завершении потока сопоставленный с ним объект ядра «поток» не 
освобождается, пока не будут закрыты все внешние ссылки на этот объект. 
    Когда поток завершился, толку от его описателя другим потокам в системе в 
общем немного. Единственное, что они могут сделать, — вызвать 
GetExitCodeThread и проверить, завершен ли поток, идентифицируемый описателем 
hThread, и, если да, определить его код завершения: 

    BOOL GetExitCodeThread(HANDLE hThread. LPDWORD IpdwExitCode); 

    Код завершения возвращается в переменной типа DWORD, на которую указывает 
IpdwExitCode. Если поток не завершен на момент вызова GetExitCodeThread, 
функция записывает в эту переменную идентификатор STILL_ACTIVE (0х103). При 
успешном вызове функция возвращает TRUE. К использованию описателя для 
определения факта завершения потока мы еще вернемся в главе 10. 

    Как узнать о себе 

    Некоторые \Ут32-функции требуют в качестве параметра передавать описатель 
какоголибо процесса. Поток может получить описатель своего процесса, вызвав 
функцию GetCurren tProcess: 

    HANDLE GetCurrentProcess(VOIO): 

    Она возвращает псевдоописатель процесса и, не создавая нового описателя, 
не увеличивает счетчик числа пользователей объекта «процесс». Если вызвать 
CloseHandle и передать ей этот псевдоописатель, она проигнорирует вызов и 
просто вернет управление. 
    Псевдоописатели можно использовать для вызова функций, которым нужен 
описатель процесса. Например, показанная ниже строка меняет класс приоритета 
вызывающего процесса на HIGH_PRIORITY_CLASS: 

    SetPriorityClass(GetCurrentProcess(),HIGH_PRIORITY_GLASS); 

    В Win32 API включено также несколько функций, требующих идентификатор 
процесса. Поток может запросить идентификатор у своего процесса, вызвав GetCurrentProcessId: 

    DWORD GetCurrentProcessId(VOID); 

    Она возвращает уникальный общесистемный идентификатор текущего процесса. 
    При обращении к CreateThread вызывающему потоку возвращается описатель 
только что созданного потока, но новый поток знать не знает о том, каков его 
собственный описатель. Получить его он может с помощью: 

    HANDLE GetCLirrentThread(VOID); 

    Как и GetCurrentProcess, функция GetCurrentThread возвращает 
псевдоописатель, имеющий смысл только при использовании его в контексте 
текущего потока. Счетчик объекта «поток» при этом не увеличивается, и вызов 
CloseHandle с передачей ей этого псевдоописателя ничего не дает. 
    Поток может запросить свой идентификатор вызовом: 

    DWORD GetCurrentThreadId(VOID); 


    ______________________________________- Глава 4. Потоки 63 

    Иногда нужно выяснить «настоящий», а не псевдоописатель потока. Под 
«настоящим» я подразумеваю описатель, который однозначно идентифицирует 
уникальный поток. Вдумайтесь в такой фрагмент: 

    DWORD WINAPI ParentThread(LPVOID IpvThreadParm) { DWORD IDTI-iread; 
    HANDLE hThreadParent = GetCurrentThread(); 
    CreateThread(NULL, 0, ChildThread, (LPVOID) hThreadParent, 0, SIDThread); 
    // далее следует какой-то код... 
    DWORD WINAPI ChildThread(LPVOID IpvThreadParm) { 
    HANDLE hThreadParent = (HANDLE) IpvThreadParm; 
    SetThreadPriority(hThreadParent, THREAD_PRIORITY_NORMAL); 
    // далее следует какой-то код... } 

    Вы заметили, что здесь не все ладно? Идея была в том, чтобы родительский 
поток передавал дочернему свой описатель. Но он передает псевдо-, а не 
«настоящий» описатель. Начиная выполнение, дочерний поток передает этот 
псевдоописатель функции SetThreadPriority, а она вследствие этого меняет 
приоритет дочернего — а вовсе не родительского! — потока. Происходит так 
потому, что псевдоописатель является описателем того потока, что вызывает эту функцию. 
    Чтобы исправить приведенный выше фрагмент кода, превратим псевдоописатель 
в «настоящий» через функцию DuplicateHandle: 

    BOOL DuplicateHahdle( 
    HANDLE hSourceProcess, 
    HANDLE hSource, 
    HANDLE hTargetProcess, 
    LPHANDLE IphTarget, 
    DWORD fdwAccess, 
    BOOL finhent, 
    DWORD fdwOptions): 

    Обычно она используется для создания нового «процессо-зависимого» 
описателя из описателя объекта ядра, значение которого увязано с другим 
процессом (см. главу 2). А мы воспользуемся DuplicateHandle не совсем по 
назначению и скорректируем с ее помощью приведенный выше фрагмент кода так: 
    
    DWORD WINAPI ParentThread(LPVOID IpvThreadParm) { 
    DWORD IDThread: 
    HANDLE hThreadParent; 
    DuplicateHandle( 
    GetCurrentProcess(),       // описатель процесса, к которому 
    // относится псевдоописатель потока; 
    GetCurrentThread(),        // псевдоописатель родительского потока; 
    GetCurrentProcess(),      // описатель процесса, к которому 
    // относится новый, "настоящий" 
    // описатель потока; 
    &hThreadParent,           // даст новый, "настоящий" описатель, 
    // идентифицирующий родительский поток; 
    см. след. стр. 


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


    О,                         // игнорируется из-за 
    // DUPLICATE_SAME_ACCESS; 
    FALSE,     .               // новый описатель потока ненаследуемый; 
    DUPLICATE_SAME_AGCESS);   // новому описателю потока присваиваются 
    // те же атрибуты защиты, 
    // что и псевдоописателю 
    CreateThread(NULL, 0, ChildThread, (LPVOID) hThreadParent, 0, &IDThread): 
    // далее следует какой-то код... 
    } 
    DWORD WINAPI ChildThread(LPVOID IpvThreadParm) { 
    HANDLE hThreadParent = (HANDLE) IpvThreadParm; 
    SetThreadPriority(hThreadParent, THREAD_PRIORITY_NORMAL); 
    CloseHandle(hThreadParent); 
    // далее следует какой-то код... 
    } 

    Теперь родительский поток преобразует свой «двусмысленный» псевдоописатель 
в «настоящий», однозначно определяющий родительский поток, и передаст его в 
CreateThread. Когда дочерний поток начнет выполнение, его параметр 
IpvThreadParm будет содержать «настоящий» описатель потока. В итоге вызов 
какой-либо функции с этим описателем уже повлияет не на дочерний, а на 
родительский поток. 
    Поскольку DuplicateHandle увеличивает счетчик указанного объекта ядра, то, 
закончив работу с продублированным описателем объекта, очень важно не забыть 
уменьшить счетчик; для этого описатель потока-приемника (в данном случае 
родительского потока) передается функции ChseHandle. Сразу после обращения к 
SetThreadPriority дочерний поток вызывает CloseHandk, тем самым уменьшая на 1 
счетчик объекта «родительский поток». В предыдущем фрагменте кода я исходил из 
предположения, что дочерний поток не вызывает других функций с передачей этого 
описателя. Ну а если ему нужно вызвать какие-то функции с передачей описателя 
родительского потока, то, естественно, к ChseHandle следует обращаться только 
после того, как необходимость в этом описателе у дочернего потока отпадет. 
    Надо заметить, что DuplicateHandle позволяет также преобразовать 
псевдоописатель процесса в «настоящий». Вот как это сделать: 

    HANDLE hProcess; 
    DuplicateHandle( 
    GetCurrentProcess(),       // описатель процесса, к которому 
    // относится псевдоописатель; 
    GetCurrentProcess(),       // псевдоописатель процесса; 
    GetCurrentProcess(),       // описатель процесса, к которому 
    // относится новый, "настоящий" 
    // описатель; 
    &hProcess,                 // даст новый, "настоящий" описатель, 
    // идентифицирующий процесс; 
    О,                         // игнорируется из-за 
    // DUPLICATE_SAME_ACCESS; 
    FALSE,                     // новый описатель ненаследуемый; 
    DUPLICATE_SAME_ACCESS);    // новому описателю процесса присваиваются 
    // те же атрибуты защиты, 
    // что и псевдоописателю 


    ______________________________________ Глава 4. Потоки 65 


    Распределение процессорного времени между потоками 

    Операционная система с вытесняющей многозадачностью должна использовать 
тот или иной алгоритм, позволяющий ей распределять процессорное время между 
потоками. Здесь мы рассмотрим алгоритм, применяемый в Windows NT и Windows 95. 
    /внимАниЕ\ 
    В этом разделе обсуждается обобщенный алгоритм распределения процессорного 
времени между потоками в операционных системах на базе Win32. Вы должны 
понимать, что Microsoft оставляет за собой право изменять какие-то нюансы 
этого алгоритма. В сущности Microsoft уже воспользовалась этим правом: в 
Windows 95, Windows NT 3.5 и Windows NT 4.0 алгоритмы распределения 
процессорного времени слегка различаются. Так что информацию, представленную в 
этом разделе, следует воспринимать как своего рода обобщение, а отдельные, 
более конкретные сведения могут со временем устареть. 
    Система выделяет процессорное время всем активным потокам, исходя из их 
уровней приоритета, которые варьируются от 0 (низший) до 31 (высший). Нулевой 
уровень присваивается в системе особому потоку обнуления страниц (zero page 
thread), обнуляющему свободные страницы при отсутствии других потоков, 
требующих внимания со стороны системы. Ни один поток, кроме него, не может 
иметь нулевой уровень приоритета. 
    Когда система подключает процессор к потоку, он обрабатывает потоки с 
одинаковым приоритетом как равноправные. Иначе говоря, на процессор подается 
первый поток с приоритетом 31, а по истечении его кванта времени система 
переключает процессор на выполнение следующего потока с тем же приоритетом. 
Как только все потоки с приоритетом 31 получат по кванту времени, система 
вновь подаст на процессор первый поток с приоритетом 31. Заметьте: если в 
Вашей системе за каждым процессором закреплен хотя бы один поток с приоритетом 
31, остальные потоки с более низким приоритетом никогда не получат доступ к 
процессору и поэтому не будут выполняться. Такая ситуация называется 
перегрузкой (starvation). Она наблюдается, когда некоторые потоки так 
интенсивно используют процессорное время, что остальные практически не работают. 
    При отсутствии потоков с приоритетом 31 система переходит к потокам с 
приоритетом 30; если отпала необходимость в выполнении и этих, к процессору 
подключаются потоки с приоритетом 29 и т. д. 
    На первый взгляд, в системе, организованной таким образом, у потоков с 
низким приоритетом (вроде потока обнуления страниц) нет ни единого шанса на 
исполнение. Но вот ведь в чем штука: зачастую потоки как раз и не нужно 
выполнять. Например, если первичный поток Вашего процесса вызывает GetMessage, 
а система «видит», что никаких сообщений пока нет, она приостанавливает его 
выполнение, отнимает остаток неиспользованного времени и тут же подключает к 
процессору другой, ожидающий поток. И пока в системе не появится сообщений для 
потока Вашего процесса, он будет простаивать — система не станет тратить на 
него время. Но вот в очереди этого потока появляется сообщение, и система 
сразу же подключает его к процессору (если только в этот момент не выполняется 
поток с более высоким приоритетом). 
    А теперь еще один момент. Допустим, процессор исполняет поток с 
приоритетом 5, и тут система обнаруживает, что поток с более высоким 
приоритетом готов к выполнению. Что будет? Система остановит поток с более 
низким приоритетом — даже если не истек отведенный ему квант процессорного 
времени — и подключит к процессору поток с более высоким приоритетом (и, между 
прочим, выдаст ему полный квант времени). Так что потоки с более высоким 
приоритетом всегда вытесняют потоки с более низким приоритетом независимо от 
того, исполняются последние или нет. 

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


    Присвоение уровней приоритета в Win32 

    Уровни приоритета созданным потокам присваивает сама система, и делается 
это в два этапа. На первом — процессу присваивается определенный класс 
приоритета, который говорит системе, какой приоритет нужен данному процессу по 
сравнению с другими выполняемыми процессами. А на втором — потокам, 
принадлежащим этому процессу, присваиваются относительные уровни приоритета. В 
следующих разделах мы и обсудим оба этапа более подробно.         """' 

    Классы приоритета процессов 

    Win32 поддерживает 4 класса приоритетов: idle (простаивающий), normal 
(обычный), high (высокий) и realtime (реального времени). Класс приоритета 
присваивается процессу объединением (операцией OR) одного из флагов функции 
CreateProcess с другими флагами fdwCreate. Вот какие уровни приоритета связаны 
с каждым классом приоритета: 

    Класс         Флаг в CreateProcess              Уровень 

    Idle           IDLE_PRIORITY_CLASS            4 
    Normal        NORMAL_PRIORITY_CLASS        8 
    High          HIGH_PRIORITY_CLASS           13 
    Realtime       REALTIME_PRIORITY_CLASS       24 

    Как видите, любой поток, созданный в процессе с обычным классом 
приоритета, получает уровень приоритета 8. 
    У меня нет слов, чтобы в полной мере выразить, насколько важна 
осторожность при выборе конкретного класса приоритета. Вызывая CreateProcess, 
большинство приложений должны либо использовать флаг NORMAL_PRIORITY_CLASS, 
либо вообще не указывать класс приоритета. В последнем случае система 
присваивает процессу класс normal — если только родительский процесс не имеет 
класса idle (тогда дочерний процесс тоже получает класс приоритета idle). 
    Когда пользователь работает с каким-то процессом, тот считается активным 
(foreground), а остальные процессы — фоновыми (background). Программы, 
запускаемые пользователем, в основном относятся к приложениям с обычным 
классом приоритета. И тут надо заметить, что процессы с обычным классом 
приоритета ведут себя несколько иначе, чем процессы остальных классов. 

    \WINDOWS/ \NT/ 
    В Windows NT, когда обычный процесс становится активным, система 
увеличивает кванты времени, выделяемые всем его потокам. Если, к примеру, 
потоки обычного процесса получают 15 мс процессорного времени, то, как только 
этот процесс становится активным, система выдает им 45 мс (увеличивая, таким 
образом, квант времени, выделяемый каждому потоку, в 3 раза). 

    VWINDOWS/ \95/ 
    В Windows 95, когда обычный процесс становится активным, система поступает 
иначе — повышает приоритет потока на 1, т. е. уровень приоритета потока \/    
увеличивается с 8 до 9. А когда процесс «уходит» в фон, приоритет потока 
автоматически снижается на 1. 
    Причина таких изменений в активных процессах очень проста: система 
добивается того, чтобы они быстрее реагировали на пользовательский ввод. Если 
бы приоритеты поть 

    Глава 4. Потоки 67 

    ков не менялись, то и обычный процесс фоновой печати, и обычный, но 
активный процесс, принимающий пользовательский ввод, — оба одинаково 
конкурировали бы за процессорное время. И тогда пользователь, набирая текст в 
активном приложении, заметил бы, что текст появляется на экране какими-то 
рывками. Но так как система повышает уровни приоритета активного процесса, они 
вытесняют потоки обычных процессов, работающих в фоновом режиме. 
    Работая с приложениями в Windows NT, пользователь может управлять 
способностью системы «ускорять» выполнение обычных, но активных процессов. Для 
этого нужно дважды щелкнуть значок System в Control Panel и выбрать вкладку 
Performance. В результате появится вот такое диалоговое окно: 
    Ползунок «Boost» показывает, на сколько увеличивается квант времени, 
выделяемый активному потоку. Если ползунок находится в позиции None, квант 
потока умножается на 1; ползунок в позиции Maximum заставляет умножать квант 
потока на 3, а в средней позиции — на 2. 
    Возможность таких изменений в Windows 95 отсутствует, поскольку эта 
операционная система не предназначена для выделенных серверов. А серверы под 
управлением Windows NT часто устанавливаются в отдельном помещении, в которое 
пользователи не допускаются. Так вот, когда машина под управлением Windows NT 
настраивается как выделенный сервер, администратор должен сместить упомянутый 
ползунок в позицию None, чтобы все потоки получали равные кванты процессорного времени. 
    Приоритет idle идеален для приложений, занимающихся мониторингом системы. 
Допустим, Вы написали программу, которая периодически сообщает объем свободной 
оперативной памяти. Но Вы ведь не хотите, чтобы она мешала работе остальных 
приложений? Значит, установите класс приоритета этого процесса как IDLE_PRIORITY_CLASS. 
    Еще один пример программы, использующей приоритет idle, — экранная 
заставка. Большую часть времени она просто отслеживает деятельность 
пользователя. Если в течение определенного времени никаких действий не было, 
заставка активизируется. Мониторинг незачем вести при очень высоком приоритете 
— достаточно и низкого, т. е. idle. 
    Класс приоритета high следует использовать только при крайней 
необходимости. Может, Вы этого и не знаете, но Explorer выполняется с высоким 
приоритетом. Большую часть времени его потоки простаивают, готовые 
пробудиться, как только пользователь на

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

    жмет какую-нибудь клавишу или щелкнет кнопку мыши. Пока потоки Explorer 
простаивают, система не выделяет им процессорного времени, что позволяет 
выполнять потоки с низким приоритетом. Но вот пользователь нажал, скажем, 
Ctrl+Esc, и система пробуждает поток Explorer. (Комбинация клавиш Ctrl+Esc 
попутно открывает меню Start.) Если в данный момент исполняются потоки с 
низким приоритетом, они немедленно вытесняются, и начинает работать поток 
Explorer. Microsoft разработала Explorer именно так потому, что любой 
пользователь — независимо от текущей ситуации в системе — ожидает мгновенной 
реакции оболочки на свои команды. В сущности, окна Explorer можно открывать, 
даже когда все потоки с низким приоритетом зависают в бесконечных циклах. 
Обладая более высоким приоритетом, потоки Explorer вытесняют поток, 
исполняющий бесконечный цикл, и дают возможность закрыть зависший процесс. 
    Надо отметить высокую степень продуманности Explorer. Основную часть 
времени он просто «спит», не требуя процессорного времени. Будь это не так, 
вся система работала бы гораздо медленнее, а многие приложения просто не 
отзывались бы на действия пользователя. 
    И, наконец, флагом четвертого по счету класса приоритета — 
REALTIME_PRIORITY_CLASS — почти никогда не стоит пользоваться. На самом деле в 
ранних бета-версиях Win32 API даже не предусматривалось присвоения этого 
приоритета приложениям, хотя операционная система поддерживала эту 
возможность. Realtime — чрезвычайно высокий приоритет, и поскольку большинство 
потоков в системе (включая те, что управляют самой системой) имеют более 
низкий приоритет, процесс с таким классом окажет на них сильное влияние. Так, 
системные потоки, контролирующие мышь и клавиатуру, фоновый сброс данных на 
диск и перехват Alt+Ctrl+Del, — все они оперируют при более низком классе 
приоритета. Если пользователь переместит мышь, поток, реагирующий на движение 
мыши, будет вытеснен потоком с приоритетом realtime. А это повлияет на 
характер перемещения курсора мыши: он станет двигаться не плавно, а рывками. 
Может случиться и кое-что похуже — вплоть до потери данных. 
    Класс приоритета realtime используют только в программе, напрямую 
обращающейся к оборудованию, или если приложение выполняет быстротечную 
операцию, которую нельзя прерывать ни при каких обстоятельствах. 
    VwiNDowsy Процесс с классом приоритета realtime нельзя запустить, если 
пользователь, \NT/ зарегистрировавшийся в системе, не имеет привилегии 
Increase Scheduling Prio\/    rity. По умолчанию такой привилегией обладает 
администратор или пользователь с соответствующими полномочиями. Но она может 
быть предоставлена и другим пользователям (или группам) с помощью User Manager. 

    Изменение класса приоритета процесса 

    Вам может показаться странным, что, создавая дочерний процесс, 
родительский сам устанавливает ему класс приоритета. За примером далеко ходить 
не надо — возьмем все тот же Explorer. При запуске из него какого-нибудь 
приложения новый процесс создается с обычным приоритетом. Но Explorer ведь не 
знает, что делает этот процесс и как часто его потокам надо выделять 
процессорное время. Поэтому в системе предусмотрена возможность изменения 
класса приоритета самим выполняемым процессом — вызовом SetPriorityClass: 

    BOOL SetPriorityClass(HANDLE hProcess, DWORD fdwPriority): 

    Эта функция меняет класс приоритета процесса, определяемого описателем 
hProcess, в соответствии со значением параметра fdwPriority. Этот параметр 
принимает одно из значений: IDLE_PRIORITY_CLASS, NORMAL_PRIORITY_CLASS, 
HIGH_PRIORITY_CLASS или 

    ___________________________________ Глава 4. Потоки 69 

    REALTIME_PRIORITY_CLASS. При успешном выполнении функция возвращает TRUE; 
в ином случае — FALSE. Поскольку SetPriorityClass принимает описатель 
процесса. Вы можете изменить приоритет любого процесса, выполняемого в 
системе, — если его описатель известен и у Вас есть соответствующие права 
доступа. "Парная ей функция GetPriorityClass позволяет узнать класс приоритета 
любого процесса: 

    DWORD GetPriorityClass(HANDLE hProcess): 

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

    C:\>START /LOW CALC.EXE 

    Команда START допускает также ключи /NORMAL, /HIGH и /REALTIME, 
позволяющие начать выполнение программы соответственно с обычным (как и по 
умолчанию), высоким приоритетом и приоритетом реального времени. Разумеется, 
после запуска программа может вызвать SetPriorityClass и установить себе 
другой класс приоритета. 

    \wiNDowsy В Windows 95 команда START не поддерживает ключи /LOW, /NORMAL, 
/HIGH \95^ и /REALTIME. Из оболочки командного процессора Windows 95 процессы 
все>/   гда запускаются с обычным классом приоритета. 


    Установка относительного приоритета потока 

    Когда поток только создается, уровень его приоритета соответствует классу 
приоритета процесса. Скажем, первичный поток процесса с классом 
HIGH_PRIORITY_CLASS получает начальное значение уровня приоритета 13. Но 
уровень приоритета отдельного потока можно и повысить, и понизить. В то же 
время приоритет потока всегда относителен классу приоритета его процесса. 
    Относительный приоритет потока в пределах одного процесса можно изменить 
функцией SetThreadPriority. 

    BOOL SetThreadPriority(HANDLE hThread, int nPriority); 

    Первый ее параметр, hThread, — это описатель потока, приоритет которого Вы 
изменяете, а nPriority принимает одно из значений, приведенных в таблице: 

    Идентификатор                        Описание 

    THREAD_PRIORITY_LOWEST            Приоритет потока должен быть на 2 
единицы ниже класса приоритета процесса. 
    THREAD_PRIORITY_BELOW_NORMAL    Приоритет потока должен быть на 1 единицу 
ниже класса приоритета процесса. 
    THREAD_PRIORITY_NORMAL            Приоритет потока должен соответствовать 
классу приоритета потока. 
    THREAD_PRIORITY_ABOVE_NORMAL     Приоритет потока должен быть на 1 единицу 
выше класса приоритета процесса. 
    THREAD_PRIORITY_HIGHEST            Приоритет потока должен быть на 2 
единицы выше класса приоритета процесса. 

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

    В момент создания потока начальное значение его относительного приоритета 
равно THREAD_PRIORITY_NORMAL. Правила установки приоритета для потоков в 
рамках какого-либо процесса аналогичны правилам установки приоритета для 
потоков разных процессов. Устанавливать приоритет THREAD_PRIORITY_HIGHEST 
следует, только если это абсолютно необходимо для корректного выполнения 
данного потока. Иначе потоки с более низкими приоритетами будут полностью 
вытеснены потоками с более высокими приоритетами. 
    Кроме упомянутых в таблице флагов, в функцию SetThreadPriority можно 
передать еще два (особых) флага: THREAD_PRIORITY_IDLE и 
THREAD_PRIORITY_TIME_CRITICAL. Первый устанавливает уровень приоритета потока 
равным 1 при классе приоритета данного процесса idle, normal или high. Если же 
класс приоритета процесса realtime, уровень приоритета потока приравнивается 
16. А второй флаг устанавливает уровень приоритета потока равным 15 при классе 
приоритета данного процесса idle, normal или high. Если же класс приоритета 
процесса realtime, уровень приоритета потока приравнивается 31. В таблице на 
рис. 4-1 показано, как система определяет базовый уровень приоритета потока, 
комбинируя класс приоритета процесса с относительным приоритетом потока. 


    Класс приоритета процесса 

    Относительный приоритет потока        Idle      Normal        High        Realtime 

    Time critical (критичный по времени)      15       15             15       
   31 
    Highest (высший)                       6        10             15          26 
    Above normal (выше обычного)           59              14          25 
    Normal (обычный)                      48              13          24 
    Below normal (ниже обычного)            37              12          23 
    Lowest (низший)                        26              11          22 
    Idle (простаивающий)                    11              1           16 

    Рис. 4-1. Так операционная система определяет базовый уровень приоритета потока 


    / ВНИМАНИЕ^ д^^ 
    Таблица на рис. 4-1 показывает приоритеты потоков для выполняемых в 
фоновом режиме процессов с обычным классом приоритета. В Windows NT приоритеты 
не изменяются, когда процесс становится активным, — изменяются только кванты 
времени, выделяемые потокам, В то же время Windows 95, когда процесс 
становится активным, увеличивает приоритет потоков lowest, below normal, 
normal и above normal на 1 единицу, но не меняет приоритет потоков idle и | 
time critical. 

    Функция GetThreadPriority, парная SetThreadPriority, позволяет узнать 
относительный приоритет потока: 

    int GetThreadPriority(HANDLE hThread); 

    Она возвращает один из перечисленных выше идентификаторов, а при 
ошибке — THREAD_PRIORITY,ERROR_RETURN. 
    Изменение класса приоритета процесса не сказывается на относительных 
приоритетах его потоков. Обратите внимание и на то, что повторные вызовы 
SetThreadPriority не дают кумулятивного эффекта. Например, если поток 
создается в процессе с высоким классом приоритета и исполняются следующие две 
строки кода: 

    ______________________________________ Глава 4. Потоки 71 


    SetThreadPriority(hThread, THREAD_PRIORITY_LOWEST); 
    SetTh readPriority(hTh read, THREAD_PRIORITY_LOWEST); 

    то поток получает уровень приоритета 11, а не 9. 


    /8нимАниЕ\ 
    Ни одна \Ут32-функция не возвращает уровень приоритета потока, т. е. в 
Win32 нет функций, которые принимали бы описатель потока и сообщали, что ' 
уровень приоритета данного потока равен, например, 8. Такая ситуация создана 
преднамеренно. Вспомните, что Microsoft может в любой момент изменить алгоритм 
распределения процессорного времени. Поэтому при разработке приложений не 
стоит опираться на какие-то нюансы этого алгоритма. Используйте классы 
приоритетов процессов и относительные приоритеты потоков, и все будет о'кэй. 


    Динамическое изменение уровней приоритетов потоков 

    Уровень приоритета, получаемый комбинацией относительного приоритета 
потока и класса приоритета процесса, которому принадлежит данный поток, 
называют базовым уровнем приоритета потока. Иногда система изменяет уровень 
приоритета потока. Обычно это происходит в ответ на некоторые события, 
связанные с вводом/выводом (например, оконные сообщения или чтение диска). 
Скажем, поток с относительным приоритетом normal, выполняемый в процессе с 
высоким классом приоритета, имеет базовый приоритет 13. 
    Если пользователь нажимает какую-нибудь клавишу, система помещает в 
очередь потока сообщение WM_KEYDOWN. А поскольку в очереди потока появилось 
сообщение, поток подключается к процессору и обрабатывает это сообщение. При 
этом система временно поднимает уровень его приоритета с 13 до 15 
(действительное значение может отличаться в ту или другую сторону). Этот новый 
уровень приоритета потока называется динамическим приоритетом потока. 
Процессор исполняет поток в течение отведенного отрезка времени, а по его 
истечении система снижает приоритет потока на 1, до уровня 14. Далее потоку 
вновь выделяется квант процессорного времени, по окончании которого система 
опять снижает уровень приоритета потока на 1. И теперь динамический приоритет 
потока снова соответствует базовому уровню его приоритета. При этом 
динамический приоритет никогда Ж-опускается ниже базового уровня приоритета. 
    Microsoft всегда старается тонко настраивать динамическое изменение 
приоритета потоков в операционной системе, чтобы та максимально быстро 
реагировала на действия конечного пользователя. Кстати, потоки с уровнями 
приоритетов реального времени (от 16 до 31) системой никогда не меняются. 
Описанный механизм действует лишь по отношению к потокам с уровнями 
приоритетов, попадающими в динамический диапазон. И, кроме того, система не 
допускает динамического повышения приоритета потока до уровней реального 
времени (более 15). 
    В Windows NT 4 появились две новые функции, позволяющие отключать 
автоматическое изменение приоритетов потоков: 

    BOOL SetProcessPriorityBoost(HANDLE hProcess, BOOL DisablePriorityBoost); 
    BOOL SetThreadPnorityBoost(HANDLE hThread, BOOL DisablePriorityBoost); 

    SetProcessPriority Boost заставляет систему включить или отключить 
изменение приоритетов всех потоков в указанном процессе, a 
SetThreadPriorityBoost действует применительно к отдельным потокам. Эти 
функции имеют свои аналоги, позволяющие определить, разрешено или запрещено 
изменение приоритетов: 

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

    BOOL GetProcessPriorityBoost(HANDLE hProcess, PBOOL pDisablePrlorityBoost); 
    BOOL GetThreadPriorityBoost(HANDLE hThread, PBOOL pDisablePriorityBoost); 

    Каждой из этих двух функций Вы передаете описатель нужного процесса или 
потока и адрес переменной типа BOOL, в которой и возвращается результат. 

    VWINDOWS/ В Windows 95 все четыре описанные функции не предусмотрены. В 
ней не \^/ удастся даже загрузить программу, вызывающую любую из этих функций. 


    Задержка и возобновление потоков 

    Я уже говорил, что создаваемый поток можно «попридержать», передав в 
CreateProcess или CreateThread флаг CREATE_SUSPENDED. В этом случае система 
создает объект ядра, идентифицирующий поток, формирует стек потока и 
инициализирует контекст (т. е. элементы структуры CONTEXT). Однако счетчику 
простоев объекта «поток» присваивается начальное значение 1, а это значит, что 
система не собирается подключать поток к процессору для выполнения. Чтобы 
такой поток начал работать, нужно из другого потока вызвать ResumeThread и 
передать ей описатель, возвращенный функцией CreateThread. Впрочем, описатель 
потока можно взять и из структуры, на которую указывает параметр IppiProdnfo, 
передаваемый в CreateProcess. 

    DWORD ResumeThread(HANDLE hThread); 

    Если вызов ResumeThread прошел успешно, она возвращает предыдущее значение 
счетчика простоев данного потока; в ином случае — OxFFFFFFFF. 
    Выполнение отдельного потока можно задерживать несколько раз. Если поток 
задержан 3 раза, то и возобновлен он должен быть тоже трижды — только тогда 
система выделит ему процессорное время. Выполнение потока можно приостановить 
не только при его создании с флагом CREATE_SUSPENDED, но и вызовом SuspendThread: 

    DWORD SuspendThread(HANDLE hThread); 

    Любой поток может вызвать эту функцию и приостановить выполнение другого 
потока. Хоть об этом нигде и не говорится (но я все равно скажу!), 
приостановить свое выполнение поток способен сам, а возобновить себя без 
посторонней помощи — нет. Как и ResumeThread, SuspendThread возвращает 
предыдущее значение счетчика простоев данного потока. Поток допустимо 
задерживать не более чем MAXIMUM_SUSPEND_COUNT раз (в файле WINNT.H это 
значение определено как 127). 

    Что происходит в системе 

    Чтобы выяснить, какие процессы загружаются в систему и какие потоки 
исполняются в каждом процессе, можно запустить поставляемую вместе с Visual 
C++ утилиту PSTAT.EXE. К сожалению, она не работает в Windows 95, но в этой 
операционной системе есть похожая утилита — PView95. На рис. 4-2 представлена 
распечатка, полученная с помощью PSTAT.EXE. В ней приведен список всех 
процессов и потоков, выполняемых в системе на момент проверки. В поле pid 
указывается идентификатор соответствующего процесса. Например, для Explorer 
(EXPLORER.EXE) этот идентификатор — 0х74. В поле pri (справа от идентификатора 
процесса) сообщается конкретное значение класса приоритета того или иного 
процесса. Скажем, для Explorer это значение равно 13, что говорит о его 
высоком приоритете. Поле HandleCount указывает текущее число записей в таблице 
описателей объектов ядра для данного процесса. 

    ____________________________________ Глава 4. Потоки 73 

    За каждой строкой, описывающей процесс, следует список его потоков. В 
частности, Explorer имеет 4 потока. Поле tid показывает идентификатор потока, 
a pri — номер приоритета потока. Поле cs сообщает о количестве переключении 
контекста данного потока. Статус потока приводится в конце строки. Слово Wait 
означает, что поток приостановлен и ожидает определенного события, после 
которого его выполнение будет продолжено. Там же сообщается и причина ожидания. 

    Pstat version 0.2: memory: 32176 kb uptime: 0 6:50:37.687 
    PageFile: \??\D:\pagefile.sys 
    Current Size: 44032 kb Total used: 10392 kb   Peak Used: 17888 kb 
    pid: 0 pri: 0 HandleCount:    0 (null) 
    tid: 0 pri:16 cs: 564389 Running 
    tid: 0 pri:16 cs: 510456 Running 
    pid: 2 pri: 8 HandleCount:   89 System 
    tid: 1 pri: 0 cs:   3730 Ready 
    tid: 3 pri:16 cs: 10128 Wait.'EventPairLow 
    tid: 4 pri:16 cs:   8928 Wait:EventPairLow 
    tid: 5 pri:16 cs: 15450 Wait:EventPairLow 
    tid: 6 pri:12 cs: 40614 Wait:EventPairLow 
    tid; 7 pri:12 cs:    313 Wait.'EventPairLow 
    tid: 8 pri:12 cs: 19352 Wait.'EventPairLow 
    tid: 9 pri:15 cs:    607 Wait:EventPairLow 
    tid: a pri:18 cs:   1300 Wait:VirtualMemory 
    tid; b pri: 17 cs:   1113 Wait.'FreePage 
    tid: с pri:16 cs: 26658 Wait:Executive 
    tid: d pri:23 cs:   9986 Wait:Executive 
    tid; e pri: 8 cs:      1 Wait:EventPairLow 
    tid: f pri:14 cs:      8 Wait:LpcReceive 
    tid: 18 pri:17 cs:      1 Wait:VirtualMemory 
    tid: 21 pri:19 cs: 268710 Wait:UserRequest 
    tid: 22 pri:16 cs:   8279 Wait:UserRequest 
    tid: 26 pri:16 cs: 10272 Wait:UserRequest 
    tid: 3d pri: 8 cs:      3 Wait:Executive 
    tid: 56 pri: 6 cs:      3 Wait:UserRequest 
    tid: 5a pri:16 cs:     45 Wait:Executive 
    tid: 58 pri: 9 cs:     78 Wait:EventPairLow 
    tid: 5b pri: 9 cs:      4 Wait:EventPairLow 
    tid: 60 pri: 9 cs:    111 Wait:EventPairLow 
    pid: 11 pri:11 HandleCount:   29 smss.exe 
    tid: 10 pri:14 cs:    819 Wait:UserRequest 
    tid: 12 pri:14 cs:      6 Wait .•LpcReceive 
    tid: 13 pri:11 cs:      3 Wait:LpcReceive 
    tid: 19 pri:14 cs:    192 Wait:LpcReceive 
    tid: 1a pri:12 cs:    101 Wait:LpcReceive 
    tid: 1b pri:11 cs:      1 Wait:LpcReceive 
    см. след. стр. 

    Рис. 4-2. Распечатка, полученная после запуска утилиты PSTAT.EXE 


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


    РИС. 4-2 (продолжение) 
    pid: 1d pri:13 HandleCount: 183 csrss.exe 
    tid: 1e pri:13 cs: 39 Wait:UserRequest 
    tid: 14 pri:14 cs: 48245 Wait:LpcReceive 
    tid; 15 pri:14 cs: 115 Wait:LpcReceive 
    tid: 16 pri:14 cs: 4 Wait:LpcReceive 
    tid: 20 pri:15 cs: 48293 Wait:LpcReceive 
    tid: 6f pri:14 cs: 48131 Wait:LpcReceive 
    tid: 8b pri:14 cs: 447 Wait:UserRequest 
    pid: 1f pri:13 HandleCount: 43 winlogon.exe 
    tid: 17 pri:15 cs; 8375 Wait:UserRequest 
    tid: 23 pri:13 cs: 6 Wait:UserRequest 
    pid: 25 pri: 9 HandleCount: 174 services.exe 
    tid: 37 pri:14 cs: 34 Wait:UserRequest 
    tid: 39 pri: 9 cs: 23 Wait:Executive 
    tid: 3c pri: 9 cs: 2 Wait:LpcReceive 
    tid: 45 pri: 9 cs: 213 Wait:UserRequest 
    tid: 47 pri: 9 cs: 18 Wait:UserRequest 
    tid: 6d pri: 9 cs: 4 Wait:Executive 
    tid: 6e pri: 9 cs: 20 Wait:UserRequest 
    tid: 6b pri:11 cs: 445 Wait:UserRequest 
    tid: 6c pri:11 cs: 413 Wait:UserRequest 
    tid: 46 pri:14 cs: 4 Wait:UserRequest 
    tid: 3b pri:15 cs: 909 Wait:UserRequest 
    tid: 72 pri:10 cs: 4 Wait:LpcReceive 
    pid: 28 pri: 9 HandleCount: 87 Isass.exe 
    tid: 2a pri:10 cs; 4 Wait:LpcReceive 
    tid: 2c pri:14 cs: 10 Wait:UserRequest 
    tid: 2d pri:10 cs: 13 Wait:LpcReceive 
    tid: 2e pri:10 cs: 15 Wait:LpcReceive 
    tid: 2f pri:10 cs: 7 Wait:LpcReceive 
    tid: 30 pri:10 cs: 8 Wait:LpcReceive 
    tid: 31 pri:10 cs: 5 Wait;LpcReceive 
    tid: 32 pri:14 cs: 6 Wait:LpcReceive 
    tid: 33 pri: 9 cs: 12 Wait:LpcReceive 
    tid: 34 pri: 9 cs: 9 Wait:Executive 
    tid: 2b pri: 9 cs: 85 Wait:UserRequest 
    pid: 27 pri: 8 HandleCount: 83 SPOOLSS.EXE 
    tid: 3e pri:14 cs: 16 Wait:Executive 
    tid: 3f pri: 8 cs: 2 Wait:UserRequest 
    tid: 41 pri:14 cs: 94 Wait:UserRequest 
    tid; 42 pri: 8 cs: 137 Wait:Executive 
    tid: 55 pri: 8 cs: 7 Wait:UserRequest 
    tid: 57 pri: 8 cs: 2 Wait:UserRequest 
    tid: 76 pri:11 cs: 236 Wait:LpcReceive 
    tid: 71 pri: 8 cs: 134 Wait:Executive 
    см. след. спц 


    ;_____________________________________ Глава 4. Потоки 75 


    РИС. 4-2 (продолжение) 
    pid: 49 pri: 8 HandleCount: 70 rpcss.exe 
    tid: 48 pri:14 cs: 24 Wait:Executive 
    tid; 4b pri: 8 cs: 421 Wait:DelayExecution 
    tid: 4c pri:14 cs: 232 Wait:LpcReceive 
    tid: 4d pri: 8 cs: 2 Wait:LpcReceive 
    tid: 5d pri:10 cs: 6 Wait.'UserRequest 
    tid: 80 pri:15 cs: 294 Wait:LpcReceive 
    tid: 61 pri: 8 cs: 495 Wait:DelayExecution 
    pid: 3a pri: 8 HandleCount: 74 tapisrv.exe 
    tid: 4e pri:14 cs 18 Wait:Executive 
    tid: 4f pri: 8 cs 13 Wait:UserRequest 
    tid: 50 pri: 9 cs 3 Wait:UserRequest 
    tid: 51 pri: 8 cs 2 Wait:UserRequest 
    tid: 52 pri:14 cs 311 Wait:LpcReceive 
    tid: 5f pri:14 cs 6 Walt;LpcReceive 
    tid: 63 pri: 9 cs 2 Walt:EventPairLow 
    tid: 64 pri: 9 cs 3 Wait:UserRequest 
    tid: 65 pri: 9 cs 3 Wait:EventPairLow 
    tid: 66 pri: 9 cs: 3 Wait:EventPairLow 
    pid: 54 pri: 8 HandleCount: 121 RASMAN.EXE 
    tid; 53 pri: 8 cs 18 Wait:Executive 
    tid: 59 pri:14 cs 65 Wait:UserRequest 
    tid: 5e pri:14 cs 22 Wait:UserRequest 
    tid: 67 pri: 8 cs 2 Wait:UserRequest 
    tid: 68 pri: 9 cs 1053 Wait:UserRequest 
    tid: 69 pri: 8 cs 24 Wait:UserRequest 
    tid: 7c pri: 9 cs 51 Wait:UserRequest 
    tid: 5c pri:14 cs 30 Wait:UserRequest 
    tid: 83 pri: 9 cs 2 Wait:UserRequest 
    tid: 85 pri:14 cs 88 Wait:UserRequest 
    tid: 88 pri: 9 cs 4 Wait:UserRequest 
    tid: 89 pri: 8 cs 3 Wait:UserRequest 
    tid: 8a pri:10 cs 1991 Wait:DelayExecution 
    pid: 29 pri: 8 HandleCount: 16 nddeagnt.exe 
    tid: 6a pri:14 cs: 78 Wait:UserRequest 
    pid: 74 pri:13 HandleCount: 69 explorer.exe 
    tid: 73 pri:15 cs: 230614 Wait.'UserRequest 
    tid: 7e pri:13 cs: 324 Wait:UserRequest 
    tid: 81 pri:13 cs: 1 Walt:LpcReceive 
    tid: 9e pri:13 cs: 29 Wait .•LpcReceive 
    pid: 7f pri:13 HandleCount: 31 taskmgr.exe 
    tid: 24 pri:14 cs: 265873 Wait:UserRequest 
    tid: 84 pri:15 cs: 58241 Wait:UserRequest 
    tid: 86 pri:14 cs: 86841 Wait:UserRequest 
    , •:. (лгд. ci4'i\ 


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


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

    pid: 70 pri: 8 HandleCount:   94 WINWORD.EXE 
    tid: 4a pri: 8 cs: 263093 Running 
    tid: 8e pri: 8 cs:      2 Wait:LpcReceive 
    tid: ad pri: 8 cs:      1 Wait:LpcReceive 
    pid: 93 pri: 8 HandleCount:   48 ntvdm.exe 
    tid: 96 pri:14 cs:   1074 Wait:UserRequest 
    tid: 90 pri:15 cs:   1125 Wait:UserRequest 
    pid: a3 pri: 8 HandleCount:   25 rundll32.exe 
    tid: a2 pri:14 cs:    468 Wait:UserRequest 
    pid: 7b pri: 8 HandleCount:   22 cmd.exe 
    tid: aa pri:15 cs:    190 Wait:UserRequest 
    pid: a8 pri: 8 HandleCount:   14 pstat.exe 
    tid: a5 pri:14 cs:      9 Running 
    
    
    Процессы, потоки и стандартная библиотека С 
    
    Microsoft поставляет вместе с Visual C++ 4.х шесть стандартных библиотек С 
(С run-time libraries). Вот их имена и краткое описание: 

    Библиотека       Описание 
    
    LIBC.LIB          Статически подключаемая библиотека для однопоточных приложений. 
    LIBCD.LIB         Отладочная версия статически подключаемой библиотеки для 
однопоточных приложений. 
    LIBCMT.LIB       Статически подключаемая библиотека для многопоточных приложений. 
    LIBCMTD.LIB      Отладочная версия статически подключаемой библиотеки для 
многопоточных приложений. 
    MSVCRT.LIB       Библиотека импорта для динамического подключения рабочей версий 
    MSVCRT40.DLL. Поддерживает как одно-, так и многопоточные приложения. 
    MSVCRTD.LIB     Библиотека импорта для динамического подключения 
отладочной версии 
    MSVCRT40.DLL. Поддерживает как одно-, так и многопоточные приложения. 

    Наверное, первый же Ваш вопрос: «А зачем мне отдельные библиотеки для 
однопоточных и многопоточных программ?» Отвечаю. Исходная стандартная 
библиотека С была разработана где-то в 1970 году — задолго до появления самого 
понятия многопоточности. Создатели этой библиотеки, само собой, не 
задумывались о проблемах, связанных с многопоточными приложениями. 
    Возьмем, к примеру, глобальную переменную еггпо из стандартной библиотеки 
С. Некоторые функции, если происходит какая-нибудь ошибка, записывают в нее 
соответствующий код. Допустим, у Вас есть такой фрагмент: 

    ______________________________________ Глава 4. Потоки 77 


    BOOL fFailure = (system("NOTEPAD.EXE README.TXT-) == -1); 
    if (fFailure) { 

    switch (errno) { 

    case E2BIG:     // список аргументов или размер окружения слишком велик 
    break; 
    
    
    case ENOENT:     // командный интерпретатор не найден 
    break; 
   
    
    case ENOEXEC:    // неверный формат командного интерпретатора 
    break; 
    
    
    case ENOMEM:     // недостаточно памяти для выполнения команды 
    break; 
    
    } 
    1 
    
    А теперь представим, что поток, выполняющий показанный выше код, прерван 
после вызова функции system и до оператора if. Допустим, он прерван для 
выполнения другого потока (в том же процессе), который обращается к одной из 
функций стандартной библиотеки С, и та тоже заносит какое-то значение в 
глобальную переменную errno. Смотрите, что получается: когда процессор 
вернется к выполнению первого потока, в переменной errno окажется вовсе не то 
значение, что записано функцией system. Поэтому для решения этой проблемы 
нужно закрепить за каждым потоком свою переменную errno. 
    Это лишь один пример того, что исходная стандартная библиотека С не 
рассчитана на многопоточные приложения. Кроме errno, в ней есть еще целый ряд 
переменных и функций, с которыми возможны проблемы в многопоточной среде: 
_doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, asctime, 
_wasctime, gmtime, _ecvt, _fcvt — список можно продолжить. 
    Чтобы многопоточные С/С++-программы, использующие стандартную библиотеку 
С, работали корректно, нужно создать специальную структуру данных и связать ее 
с каждым потоком, в котором есть вызовы библиотечных функций. А для этого 
потоки придется создавать библиотечной функцией _beginthreadex, а не 
\Уш32-функцией CreateThread: 

    unsigned long _beginthreadex(void «security, unsigned stack_size, 
    unsigned (*start_address)(void *), void *arglist, 
    unsigned initflag, unsigned *thrdaddr); 

    По большому счету у _beginthreadex тот же список параметров, что и у 
CreateThread, несмотря на разные имена и типы. Как и CreateThread, она 
возвращает описатель только что созданного потока. Однако, если при компиляции 
файла Windows.H Вы определите идентификатор STRICT, Вам придется здорово 
потрудиться, чтобы угодить компилятору. При вызове _beginthreadex: 
    1. Создает недокументированную внутреннюю структуру данных, в которой 
размещается вся информация, специфичная для данного потока. Например, туда 
заносится экземпляр переменной errno и указатель на буфер strtok. В этой 
структуре содержатся также 2 элемента, инициализируемые значениями параметров 
start_address и arglist, передаваемых Вами в _beginthreadex. 
    1. Обращается к \Уш32-функции CreateThread. и создает новый поток. Вызов 
CreateThread осуществляется так: 
    
    hThread = CreateThread(security. stack_size, _threadstart. 
    &PerThreadData, initflag, thrdaddr); 
    3. Возвращает описатель только что созданного потока — или 0 в случае ошибки. 

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

    Новый поток начнется с функции _threadstart, а не с той, что Вы передали в 
_beginthreadex. Функция _threadstart содержится в стандартной библиотеке С и: 
    1. Используя локальную динамическую память данного потока, связывает 
адрес, по которому располагается блок данных потока, с самим потоком 
(подробнее о локальной памяти потока см. главу 13). Функция _threadstart 
передает адрес этого блока памяти как свой параметр. 
    2. Инициализирует для нового потока поддержку операций с плавающей точкой 
(из стандартной библиотеки С). 
    3. Входит в SEH-фрейм для поддержки функции signal из стандартной 
библиотеки С. 
    4. Получает из элементов блока данных потока адрес Вашей функции потока и 
параметр, который Вы хотели в нее передать. Эти значения используются функцией 
_threadstart для вызова Вашей функции потока с передачей требуемого 32-битного параметра. 
    5. Когда Ваша функция потока возвращает управление, _threadstart вызывает 
еще одну функцию из стандартной библиотеки С — _endthreadex, передавая ей 
значение, возвращенное Вашей функцией. 
    После этого функция _endthreadex завершает поток, созданный 
_beginthreadex. Прототип _endthreadex выглядит так: 
    void _endthreadex(unsigned retval); 
    Параметр retval содержит код завершения потока. Функция _endthreadex: 
    1. Отключает поддержку операций с плавающей точкой для данного потока. 
    2. Получает адрес блока данных, связанного с завершаемым потоком. 
    3. Удаляет этот блок. 
    4. Завершает поток вызовом У\Гт32-функ.и.ии ExitThread, передавая ей 
содержимое своего параметра retval. 
    Функцию _endthreadex можно при желании вызвать и явным образом. Помните 
только, что по окончании работы Вашей функции потока стандартная библиотечная 
функция _threadstart сама вызывает _endthreadex. 
    Теперь Вы уже понимаете, зачем библиотечным функциям нужны отдельные блоки 
данных для каждого порождаемого потока и каким образом после вызова 
_beginthreadex происходит создание и инициализация этого блока данных, а также 
его связывание с только что созданным потоком. Кроме того, Вы уже должны 
разбираться в том, как функция _endthreadex освобождает этот блок данных по 
завершении потока. 
    Как только блок данных инициализирован и сопоставлен с конкретным потоком, 
любая стандартная библиотечная функция, к которой обращается поток, может 
легко узнать адрес его блока и таким образом получить доступ к данным, 
принадлежащим этому потоку. 
    Ладно, с функциями мы разобрались, теперь попробуем проследить, что 
происходит с глобальными переменными вроде еггпо. В стандартных заголовочных 
файлах эта переменная определена так: 

    #if defined(_MT) :: defined(_DLL) 
    extern int * __cdecl _errno(void); 
    ftdefine errno   (*_errno()) 
    #else     /* ndef _MT && ndef _DLL »/ 
    extern int errno; 
    ffendif    /* _MT :: _DLL */ 

    ______________________________________ Глава 4. Потоки 79 

    Создавая многопоточное приложение, надо указывать в командной строке 
компилятора один из ключей: /МТ (многопоточное приложение) или /MD 
(многопоточная DLL); тогда компилятор определит идентификатор _МТ. После 
этого, ссылаясь на еггпо. Вы на самом деле вызовете внутреннюю функцию _еггпо 
из стандартной библиотеки С. Она возвращает адрес элемента данных еггпо в 
блоке, сопоставленном с вызывающим потоком. Кстати, макрос еггпо составлен 
так, что позволяет получать содержимое памяти по этому адресу. А сделано это 
для того, чтобы можно было написать и такой код: 

    int *р = &errno; 
    if (*p == ENOMEM) { 
    } 

    Если бы внутренняя функция _еггпо просто возвращала значение еггпо, этот 
код не удалось бы скомпилировать. 
    Многопоточная версия стандартной библиотеки С, кроме того, «обертывает» 
некоторые функции синхронизирующими примитивами. Ведь если бы два потока 
одновременно вызывали функцию malloc, куча могла бы быть повреждена. Поэтому в 
многопоточной версии библиотеки потоки не могут одновременно «запустить руки» 
в эту кучу. Второй поток она заставляет ждать до тех пор, пока первый не 
выйдет из функции malloc, и лишь тогда второй поток получает доступ к malloc. 
(Подробнее о синхронизации потоков мы поговорим в главе 10.) 
    Конечно, все эти дополнительные операции не могли не отразиться на 
быстродействии многопоточной версии библиотеки. Поэтому Microsoft, кроме 
многопоточной, поставляет и однопоточную версию статически подключаемой 
стандартной библиотеки С. 
    Динамически подключаемая версия стандартной библиотеки С вполне 
универсальна: ее могут использовать любые выполняемые приложения и DLL-модули, 
которые обращаются к стандартным библиотечным функциям. По этой причине данная 
библиотека существует лишь в многопоточной версии. Поскольку она поставляется 
в виде DLL, ее код нет нужды включать непосредственно в ЕХЕ- и DLL-модули, что 
существенно уменьшает их размер. Кроме того, если Microsoft исправляет 
какую-то ошибку в такой библиотеке, то и программы, построенные на ее основе, 
автоматически избавляются от этой ошибки. 
    Наверное, Вам интересно узнать, что будет, если новый поток создать 
вызовом ЛУш32-функции CreateThread, а не стандартной библиотечной функции 
_begmthreadey?. Так вот, если поток, созданный CreateThread, обратится к 
стандартной библиотечной функции, требующей сопоставленного с потоком блока 
данных, произойдет следующее: 
    1. Библиотечная функция попытается получить адрес блока данных, 
сопоставленного с потоком. 
    2. Если адрес — NULL, то она, используя локальную память потока, создаст и 
инициализирует этот блок, а затем свяжет его адрес с потоком. (О локальной 
памяти потока см. главу 13.) 
    3. После этого функция сможет начать работу, поскольку теперь она 
располагает адресом блока данных потока. 
    Увы, не все так гладко. Во-первых, если поток использует функцию signal из 
стандартной библиотеки, это приведет к завершению всего процесса, так как 
SEH-фрейм не был подготовлен. Во-вторых, если поток завершится без вызова 
функции _endthreadex, блок данных не будет уничтожен и произойдет утечка 
памяти. Но с этим-то справиться как раз несложно — достаточно задействовать в 
программе DLL-вариант стандартной библиотеки. 

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

    Дело в том, что, завершаясь, поток уведомляет об этом DLL-модуль, и тот 
получает возможность уничтожить блок данных, принадлежащий потоку. Так что 
проблема с потерей контроля над частью памяти возникает лишь при использовании 
в приложении статически подключаемой версии библиотеки. А вообще в таких 
случаях нужно взять за правило обращаться не к Wn-132-функциям 
CreateThread/ExitThread, а к их библиотечным аналогам Jbeginthreadex/_endthreadex. 
    Как Вы, наверное, и предполагали, стартовый код из стандартной библиотеки 
С создает и инициализирует блок данных для первичного потока приложения. Это 
позволяет без всяких опасений вызывать из первичного потока любые стандартные 
библиотечные функции. А когда первичный поток заканчивает выполнение функции 
WinMain, блок данных завершаемого потока освобождается самой библиотекой. 
Более того, стартовый код делает все необходимое для структурной обработки 
исключений, благодаря чему из первичного потока можно спокойно обращаться и к 
стандартной библиотечной функции signal. 

    Стандартные библиотечные функции, которые лучше не вызывать 

    В стандартной библиотеке содержатся две функции: 

    unsigned long _beginthread(void (__cdecl *start_address)(void *), unsigned 
stack_size, void *arglist); 

    и 

    void _endthread(void); 

    Первоначально они были созданы для того, чем теперь занимаются новые 
функции _beginthreadex и _endthreadex. Но, как видите, у _beginthread 
параметров меньше, и, следовательно, ее возможности ограничены в сравнении с 
полнофункциональной _beginthreadex, Например, работая с _beginthread, нельзя 
создать поток с атрибутами защиты, отличными от присваиваемых по умолчанию, 
нельзя создать поток и тут же его задержать — нельзя даже получить 
идентификатор потока. С функцией _endthread та же история: у нее нет 
параметров, и при окончании потока она не дает его код завершения. 
    Однако с функцией _endthread дело обстоит куда хуже, чем кажется: перед 
вызовом ExitThread она обращается к CloseHandle и передает ей описатель 
«своего» потока. Чтобы разобраться, в чем тут проблема, взгляните на следующий код: 

    DWORD dwExitCode; 
    HANDLE hThread = _beginthread(,.,); 
    GetExitCodeThread(hThread, &dwExitCode); 
    CloseHandle(hThread); 

    Весьма вероятно, что созданный поток отработает и завершится еще до того, 
как первый поток успеет вызвать функцию GetExitCodeThread. Если так и 
случится, значение в hThread окажется недействительным — ведь _endthread уже 
закрыла описатель нового потока. И, естественно, вызов CloseHandle даст ошибку. 
    Новая функция _endthreadex не закрывает описатель потока, поэтому фрагмент 
кода, приведенный выше, будет нормально работать (если мы, конечно, заменим 
вызов _beginthread на вызов _beginthreadex). И в заключение напомню еще раз: 
как только функция потока возвращает управление, _beginthreadex самостоятельно 
вызывает _endthreadex, a _beginthread обращается к _endthread. 