ГЛАВА 6 Базовые сведения о потоках

Темашка, связанная потоками, очень важна, потому что в любом процессе должен быть хотя бы один поток В этой паве концепции потоков будут рассмотрены гopaз до подробнее В частности, я объясню, в чем разница между процессами и потоками и для чего они предназначены Также я расскажу о том, как система использует объек ты ядра "поток» для управления потоками Подобно процессам, потоки обладают оп ределенными свойствами, поэтому мы поговорим о функциях, позволяющих обра щаться к этим свойствам и при необходимости модифицировать их Кроме того, Вы узнаете о функциях, предназначенных для создания (порождения) дополнительных потоков в системе

В главе 4 я говорил, что процесс фактически состоит из двух компонентов объекта ядра "процесс" и адресного пространства так вот, любой поток тожс состоит из двух компонентов.

В той же главе 4 я упомянул, что процессы инертны Процесс ничего не исполня ет, он просто служит контейнером потоков Потоки всегда создаются в контексте какого-либо процесса, и вся их жизнь проходит только в его границах На практике это означает, что потоки исполняют код и манипулируют данными в адресном про странстве процесса Поэтому, если два и более потоков выполняется в контексте од ного процесса, все они делят одно адресное пространство Потоки могут исполнять один и тот же код и манипулировать одними и теми же данными, а также совместно использовать описатели объектов ядра, поскольку таблица описателей создается нс в отдельных потоках, а в процессах

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

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

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

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

Поток (thread) определяет последовательность исполнения кода в процессе. При инициализации процесса система всегда создает первичный поток Начинаясь со стартовою кодц из библиотеки С/С++, который в свою очередь вызывает входную функцию (WinMain, wWinMain, main или wmain) из Вашей программы, он живет до того момента, когда входная функция возвращает управление стартовому коду и тот вызывает функцию ExitProcess. Большинство приложений обходится единственным, первичным потоком. Однако процессы могут создавать дополнительные потоки, что позволяет им эффективнее выполнять свою работу

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

Одна важная вещь, на которую Вы должны были обратить внимание во всех этих примерах, заключается в том, что поддержка многопоточности позволяет упростить пользовательский интерфейс приложения Если компилятор ведет сборку Вашей про граммы в те моменты, когда Вы делаете паузы в наборе ее текста, отпадает необходи мость в командах меню Build. То же самое относится к командам Check Spelling и Check Grammar в текстовых процессорах.

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

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

В каждом процессе есть хотя бы один поток, Даже не делая ничего особенного в приложении, Вы уже выигрываете только от того, что оно выполняется в многопо точной операционной системе. Например, Вы можете собирать программу и одно временно пользоваться текстовым процессором (довольно часто я так и работаю) Если в компьютере установлено два процессора, то сборка выполняется на одном из них, а документ обрабатывается на другом. Иначе говоря, какого-либо падения про изводительности не наблюдается. И кроме того, если компилятор из-за той или иной ошибки входит в бесконечный цикл, на остальных процессах это никак не отражает ся. (Конечно, о программах для MS-DOS и 16-разрядной Windows речь не идет,)

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

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

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

Еще одно узкое место, где неправильное применение потоков может привести к появлению проблем, — разработка пользовательского интерфейса в приложении В подавляющем большинстве программ все компоненты пользовательского интерфей

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

Обычно в приложении существует один поток, отвечающий за поддержку пользо вательского интерфейса, — он создает все окна и содержит цикл GetMessage Любые другие потоки в процессе являются рабочими (т. e. отвечают за вычисления, ввод вывод и другие операции) и не создают никаких окон, Поток пользовательского ин терфейса, как правило, имеет более высокий приоритет, чем рабочие потоки, — это нужно для того, чтобы он всегда быстро реагировал на действия пользователя.

Несколько потоков пользовательского интерфейса в одном процессе можно об наружить в таких приложениях, как Windows Explorcr Он создаст отдельный поток для каждого окна папки. Это позволяет копировать файлы из одной папки в другую и попутно просматривать содержимое еще какой-то папки. Кроме того, если какая-то ошибка в Explorer приводит к краху одного из cro потоков, прочие потоки остаются работоспособны, и Вы можете пользоваться соответствующими окнами, пока не сде лаете что-нибудь такое, из-за чего рухнут и они. (Подробнее о потоках и пользова тельском интерфейсе см. главы 26 и 27.)

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

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

Каждый поток начинает выполнение с некоей входной функции. В первичном пото ке таковой является main, wmain, WinMain или wWinMain. Если Вы хотите создать вто ричный поток, в нем тоже должна быть входная функция, которая выглядит пример но так

DWORD WINAPI ThreadFunc(PVOID pvPararn)
{
DWORD rtwResult = 0;

return(dwResult);
}

Функция потока может выполнять любые задачи. Рано или поздно она закончит свою работу и вернет управление. В этот момент Ваш поток остановится, память, от веденная под его стек, будет освобождена, а счетчик пользователей его объекта ядра "поток" уменьшится на 1. Когда счетчик обнулится, этот объект ядра будет разрушен Но, как и объект ядра "процесс", он может жить гораздо дольше, чем сопоставленный с ним поток.

А теперь поговорим о самых важных вещах, касающихся функций потоков

wWinA4ain. Но функциям потоков передается единственный параметр, смысл которого определяется Вами, а не операционной системой Поэтому здесь нет проблем с ANSI/Unicode

Вот Вы и узнали, как должна быть реализована функция потока Теперь рассмот рим, как заставить операционную систему создать поток, который выполнит эту фун кцию.

Функция CreateThread

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

HANDlF CreateThread(
PSECURITY_ATTRIBUTES psa, DWORD cbStack,
PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam, DWORD tdwCreate, PDWORD pdwThreadID);

При каждом вызове этой функции система создает объект ядра "поток» Это не сам поток, а компактная структура данных, которая используется операционной систе мой для управления потоком и хранит счатистическую информацию о потоке. Так что объект ядра "поток" — полный аналог объекта ядра "процесс".

Система выделяет память под стек потока из адресного пространства процесса. Новый поток выполняется в контексте того же процесса, что и родительский поток. Поэтому он получает доступ ко всем описателям объектов ядра, всей памяти и стекам всех потоков в процессе. За счет этого потоки в рамках одного процесса могут легко взаимодействовшьдруг с другом.

NOTE:
CreateTbread - это Windows-функция, создающая поток. Но никогда не вы зывайте ее, если Вы пишете код на С/С++ Вместо нее Вы должны использо вать функцию beginthreadex из библиотеки Visual С++. (Если Вы работаете с другим компилятором, он должен поддерживать свой эквивалент функции CreateThread.) Что именно делает _beginthreadex и почему это так важно, я объясню потом.

О'кэй, общее представление о функции CreateThread Вы получили. Давайте рас смотрим все ее параметры.

Параметр psa

Параметр psa является указателем на структуру SECURITY_ATTRIBUTES Если Вы хо тите, чтобы объектуядра "поток" были присвоены атрибуты защиты по умолчанию (что чаще всего и бывает), передайте в этом параметре NULL A чтобы дочерние про цессы смогли наследовать описатель этого объекта, определите структуру SECURI TY_ATTRIBUTES и инициализируйте ее элемент hlnherttHandle значением TRUE (см. главу 3)

Параметр cbStack

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

/STACK.[reserve] [,commit]

Аргумент reserve определяет объем адресного пространства, который система должна зарезервировать под стек потока (по умолчанию — 1 Мб). Аргумент commit задает объем физической памяти, который изначально передается области, зарезер вированной под стек (по умолчанию — 1 страница). По мере исполнения кода в по токе Вам, весьма вероятно, понадобится отвести под стек больше одной страницы памяти. При переполнении стека возникнет исключение (О стеке потока и исключе ниях, связанных с его переполнением, см. главу 16, а об общих принципах обработ ки исключений — главу 23.) Перехватив это исключение, система передаст зарезер вированному пространству еще одну страницу (или столько, сколько указано в аргу менте commit) Такой механизм позволяет динамически увеличивать размер стека лишь по необходимости.

Если Вы, обращаясь к CreateThread, передаете в параметре cbStack ненулевое зна чение, функция резервирует всю указанную Вами память. Ее объем определяется либо значением параметра cbStack, либо значением, заданным в ключе /STACK компонов щика (выбирается большее из них). Но передается стеку лишь тот объем памяти, ко торый соответствует значению в cbStack Если же Вы передаете в параметре cbStack нулевое значение, CreateThread создает стск для нового потока, используя информа цию, встроенную компоновщиком в ЕХЕ-файл

Значение аргумента reserve устанавливает верхний предел для стека, и это огра ничение позволяет прекращать деятельность функций с бесконечной рекурсией. До пустим, Вы пишете функцию, которая рекурсивно вызывает сама себя Предположим также, что в функции есть "жучок», приводящий к бесконечной рекурсии. Всякий раз, когда функция вызывает сама себя, в стске создается новый стековый фрейм. Если бы система не позволяла ограничивать максимальный размер стека, рекурсивная функ ция так и вызывала бы сама себя до бесконечности, а стек поглотил бы все адресное пространство процесса. Задавая же определенный предел, Вы, во-первых, предотвра щаете разрастание стека до гигантских объемов и, во-вторых, гораздо быстрее узна ете о наличии ошибки в своей программе. (Программа-пример Summation в главе 16 продемонстрирует, как перехватывать и обрабатывать переполнение стека в прило жениях )

Параметры pfnStartAddr и pvParam

Параметр pfnStartAddr определяет адрес функции потока, с которой должен будет начять работу создаваемый поток, а параметр pvParam идентичен параметру рvРаrат функции потока. CreateTbread лишь передает этот параметр по эстафете той функ ции, с которой начинается выполнение создаваемого потока. Таким образом, данный параметр позволяет передавать функции потока какое-либо инициализирующее зна чение. Оно может быть или просто числовым значением, или указателем на структу ру данных с дополнительной информацией.

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

Учтите, что Windows — операционная система с вытесняющей многозадачностью, а следовательно, новый поток и поток, вызвавший CreateThread, могут выполняться одновременно В связи с этим возможны проблемы Остерегайтесь, например, такого кода.

DWORD WINAPI FirstThread(PVOID pvParam)
{
// инициализируем переменную, которая содержится в стеке

int x = 0;
DWORD dwThreadID;

// создаем новый поток
HANDLE hThread = CreateThread(NULL, 0, SecondThread, (PVOID) &x, 0, &dwThreadId);

// мы больше не слылаемся на новый поток,
// поэтому закрываем свой описатель этого потока
CloseHandle(hThread);

// Наш поток закончил работу.
// ОШИБКА, его стек будет разрушен, но SecondThread // может попытаться обратиться к нему return(0);
}

DWORD WINAPI SecondThread(PVOID pvParam) {
// здесь выполняется какая-то длительная обработка
// Пытаемся обратиться к переменной в стеке FirstThread,
// ПРИМЕЧАНИЕ- это может привести к ошибке общей защиты
// нарушению доступа * ((int *) pvParam) = 5;

relurn(0);
}

Не исключено, что в приведенном коде FirstThread закончит свою работу до того, как SecondThread присвоит значение 5 переменной x из FirstThread. Если так и будет, SecondThread не узнает, что FirstThread больше не существует, и попытается изменить содержимое какого-то участка памяти с недействительным теперь адресом. Это не избежно вызовет нарушение доступа: стек первого потока уничтожен по завершении

FirstThread. Что же делать? Можно объявить x статической переменной, и компиля тор отведет память для хранения переменной x не в стеке, а в разделе данных прило жения (application's data section). Ho тогда функция станет нереентерабельной. Ина че говоря, в этом случае Вы не смогли бы создачь два потока, выполняющих одну и ту же функцию, так как оба потока совместно использовали бы статическую перемен ную Другое решение этой проблемы (и его более сложные варианты) базируется па методах синхронизации потоков, речь о которых поЙдет в главах 8, 9 и 10.

Параметр fdwCreate

Этот параметр определяет дополнительные флаги, управляющие созданием потока. Он принимает одно из двух значений. 0 (исполнение потока начинается немедлен но) или CREATE_SlJSPENDED. В последнем случае система создает поток, инициали зирует его и приостанавливает до последующих указаний.

Флаг CREATE_SUSPENDED позволяет программе изменить какие-либо свойства потока перед тем, как он начнет выполнять код Правда, необходимость в этом воз никает довольно редко Одно из применений этого флага демонстрирует програм мa - пример JobLab из главы 5

Параметр pdwThreadlD

Последний параметр функции CreateTbread — это адрес переменной типа DWORD, в которой функция возвращает идентификатор, приписанный системой новому пото ку. (Идентификаторы процессов и но'юков рассматривались в главе 4.)

NOTE:
В Windows 2000 и Windows NT 4 в этом параметре можно передавать NULL (обычно так и делается). Тем самым Вы сообщаете функции, что Вас не инте ресует идентификатор потока Ilo в Windows 95/98 это приведет к ошибке, так как функция попытается записать идентификатор потока no нулевому адресу, что недопустимо. И поток не будет создан.

Такое несоответствие между операционными системами может создать разработчикам приложений массу проблем, Допустим, Вы пишете и тестируе те программу в Windows 2000 (которая создает поток, даже если Вы передаете NULL в pdwThreadID) Но вот Вы запускаете приложение в Windows 98, и фун кция CreateThread, естественно, дает ошибку. Вывод один: тщательно тестируй те свос приложение во всех операционных системах, в которых оно будет работать

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

Поток можно завершить четырьмя способами:

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

Возврат управления функцией потока

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

Функция ExitThread

Поток можно завершить принудительно, вызвав:

VOID ExitThread(DWORD dwExitCоde);

При этом освобождаются все ресурсы операционной системы, выделенные дан ному потоку, но C/C++ - pеcypcы (например, объекты, созданные из С++-классов) не очищаются Именно поэтому лучше возвращать управление из функции потока, чем самому вызывать функцию ExitThread. (Подробнее на эту тему см. раздел "Функция ExitProcess» в главе 4.)

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

NOTE:
ExitThread — это Windows-функция, которая уничтожает поток. Но никогда не вы зывайте ее, если Вы пишете код на С/С++ Вместо нее Вы должны использовать функцию _endthreadex из библиотеки Visual С++ (Если Вы работаете с другим компилятором, он должен поддерживать свой эквивалент функции ExitThread) Что именно делает _endthreadex и почему это так важно, и объясню потом.

Функция TerminateThread

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

BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode);

В отличие от ExitThread, которая уничтожает только вызывающий поток, эта фун кция завершает поток, указанный в параметре hThread. В параметр dwExitCode Вы помещаете значение, которое система рассматривает как код завершения потока. После того как поток будет уничтожен, счетчик пользователей его объекта ядра "по ток» уменьшится на 1

NOTE:
TerminateThread — функция ясинхронная, т, e. она сообщает системе, что Вы хотите завершить поток, но к тому времени, когда она вернет управление, поток может быть еще не уничтожен. Так что, если Вам нужно точно знать момент завершения потока, используйте WaitForSingleObject (см. главу 9) или аналогичную функцию, передав ей описатель этого потока

Корректно написанное приложение не должно вызывать эту функцию, поскольку поток не получает никакого уведомления о завершении; из-за этого он не может вы полнить должную очистку ресурсов.

NOTE:
Уничтожение потока при вызове ExitThread или возврате управления из функ ции потока приводит к разрушению его стека. Но если он завершен функцией TerminateThread, система не уничтожает стек, пока не завершится и процесс, которому принадлежал этот поток Так сделано потому, что другие потоки могут использовать указатели, ссылающиеся на данные в стеке завершенного потока. Если бы они обратились к несуществующему стеку, произошло бы на рушение доступа

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

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

Функции ExitProcess и TerminateProcess, рассмотренные в главе 4, тоже завершают потоки. Единственное отличие в том, что они прекращают выполнение всех потоков, принадлежавших завершенному процессу При этом гарантируется высвобождение любых выделенных процессу ресурсов, в том числе стеков потоков Однако эти две функции уничтожают потоки принудительно — так, будто для каждого из них вызы вается функция TerminateThread. А это означает, что очистка проводится некоррект но, деструкторы С++-объектов не вызываются, данные на диск не сбрасываются и т д

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

А происходит вот что.

При завершении потока сопоставленный с ним объект ядра "поток* не освобож дается до тех пор, пока не будут закрыты все внешние ссылки на этот объект.

Когда поток завершился, толку от его описателя другим потокам в системе в об щем немного. Единственное, что они могут сделать, — вызвать функцию GetExitCode Thread, проверить, завершен ли поток, идентифицируемый описателем hThread, и, если да, определить его код завершения.

BOOL GetExitCodeThread( HANDLE hThread, PDWORD pdwExitCode);

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

Кое-что о внутреннем устройстве потока

Я уже объяснил Вам, как реализовать функцию потока и как заставить систему создать поток, который выполнит эту функцию. Теперь мы попробуем разобраться, как сис тема справляется с данной задачей

На рис. 6-1 показано, что именно должна сделать система, чтобы создать и ини циализировать поток. Давайте приглядимся к этой схеме повнимательнее Вызов CreateThread заставляет систему создать объект ядра "поток». При этом счетчику чис ла его пользователей присваивается начальное значение, равное 2. (Объект ядра "по ток" уничтожается только после того, как прекращается выполнение потока и закры вается описатель, возвращенный функцией CreateThread) Также инициализируются другие свойства этого объекта счетчик числа простоев (suspension count) получает значение 1, а код завершения — значение STILL_ACTIVE (0x103) И, наконец, объект переводится в состояние "занято».

Создав объект ядра "поток», система выделяет стеку потока память из адресного пространства процесса и записывает в его самую верхнюю часть два значения (Сте ки потоков всегда строятся от старших адресов памяти к младшим) Первое из них является значением параметра pvParam, переданного Вами функции CreateThread, а второе — это содержимое параметра pfnStartAddr, который Вы тоже передаете в Create Thread

h5-5.jpg

Рис. 6-1. Так создается и инициализируется поток

У каждого потока собсвенный набор регистров процессора, называемый контек стом потока. Контекст отражает состояние регистров процессора на момент после днего исполнения потока и записывается в структуру CONTEXT (она определена в заголовочном файле WinNT.h). Эта структура содержится в объекте ядра "поток»

Указатель команд (IP) и указатель стека (SP) — два самых важных регистра в кон тексте потока. Вспомните: потоки выполняются в контексте процесса. Соответствен но эти регистры всегда указывают на адреса памяти в адресном пространстве про цесса. Когда система инициализирует объект ядра "поток", указателю стека в струк туре CONTEXT присваивается тот адрес, по которому в стек потока было записано зна чение pfnStartAddr, а указателю команд — адрес недокументированной (и неэкспор тируемой) функции BaseThreadStart. Эта функция содержится в модуле Kernel32.dll, где, кстати, реализована и функция CreateTbread.

Вот главное, что делает BaseThreadStart:

VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)
{

__try
{
ExitThread((pfnStartAddr)(pvParam));
}

_except(UnhandledExceptionFilter(GetExceptionInformation()))
{
ExitProcess(GetExceptionCode());
}

// ПРИМЕЧАНИЕ, мы никогда не попадем сюда
}

После инициализации потока система проверяет, был ли передан функции Create Thread флаг CREATE_SUSPENDED Если нет, система обнуляет его счетчик числа про стоев, и потоку может быть выделено процессорное время. Далее система загружает в регистры процессора значения, сохраненные в контексте потока С этого момента поток может выполнять код и манипулировать данными в адресном пространстве своего процесса.

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

Когда новый поток выполняет BaseThreadStart, происходит следующее.

Обратите внимание, что из BaseThreadStart поток вызывает либо ExitThread, либо ExitProcess А это означает, что поток никогда не выходит из данной функции; он все гда уничтожается внутри нее. Вот почему BaseThreadStart нет возвращаемого значе ния — она просто ничего не возвращает.

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

При инициализации первичного потока его указатель команд устанавливается на другую недокументированную функцию — BaseProcessStart Она почти идентична BaseThreadStart и выглядит примерно так:

VOID BaseProcessStart(PPROCESS_START_BOUTINE pfnStartAddr)
{

__try
{
ExitThread((pfnStartAdd r)());
}

_except(UnhandledFxceptionFilter(GetExceptionInformation()))
{
ExitProcess(GettxceptionCode());
}

// ПРИМЕЧАНИЕ, мы никогда не попадем сюда
}

Единственное различие между этими функциями в отсутствии ссылки на параметр pvParam. Функция BaseProcessStart обращается к стартовому коду библиотеки С/С++, который выполняет необходимую инициализацию, а затем вызывает Ramy входную функцию main, wmain, WinMain или wWinMain. Когда входная функция возвращает управление, стартовый код библиотеки С/С++ вызываст ExitProcess. Поэтому первич ный поток приложения, написанного на С/С++, никогда не возвращается в Base ProcessStart.

Некоторые соображения по библиотеке С/С++

Microsoft поставляет с Visual С++ шесть библиотек С/С++. Их краткое описание пред ставлено в следующей таблице.

Имя библиотеки

Описание

LibC.lib

Статически подключаемая библиотека для однопоточных приложений

LibCD.lih

Отладочная версия статически подключаемой библиотеки для однопо

LibCMt.lib

Статически подключаемая библиотека для многопоточных приложений

LibCMtD.lib

Отладочная версия статически подключаемой библиотеки для много

MSVCRt.lib

Библиотека импорта для динамического подключения рабочей версии

MSVCRtD.lib

Библиотека импорта дли динамического подключения отладочной версии MSVCRtD.dll; поддерживает как одно-, так и многопоточные приложения

При реализации любого проекта нужно знать, с какой библиотекой его следует связать. Конкретную библиотеку можно выбрать в диалоговом окне Project Settings: на вкладке С/С++ в списке Category укажите Code Generation, а в списке Use Run-Time Library — одну из шести библиотек.

h5-6.jpg

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

Возьмем, к примеру, глобальную переменную errno из стандартной библиотеки С. Некоторые функции, если происходит какая-нибудь ошибка, записывают в эту пере менную соответствующий код. Допустим, у Вас есть такой фрагмент кода:

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

if (fFailure)
{
switch (errno)
{
case E2BIG:
// список аргументов или размер окружения слишком велик
break;

case ENOENT:
// командный интерпретатор не найден
break;

case ENOEXEC;
// неверный формат командного интерпретатора
break;

case ENOMEM:
// недостаточно памяти для выполнения команды
break;
}

Теперь представим, что поток, выполняющий показанный выше код, прерван после вызова функции system и до оператора if. Допустим также, поток прерван для выпол

нения другого потока (в том же процессе), который обращается к одной из функций библиотеки С, и та тоже заиосит какое то значение в глобальную переменную errno Смотрите, что получается когда процессор вернется к выполнению первого потока, в переменной errno окажется вовсе не то значение, которое было записано функци ей system Поэтому для решения этой проблемы нужно закрепить за каждым потоком свою переменную errno Кроме того, понадобится какой-то механизм, который позво лит каждому потоку ссылаться на свою переменную errno и нс трогать чужую

Это лишь один пример того, что стандартная библиотека С/С++ не рассчитана на многопоточные приложения Кроме errno, в ней есгь еще целый ряд переменных и функций, с которыми возможны проблемы в многопоточной среде _doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, a<tcttme, _wascttme, gmttme, _ecvt, _Jcvt — спи сок можно продолжить

Чтобы многопоточные программы, использующие библиотеку С/С++, работали корректно, требуется создать специальную структуру данных и связать ее с каждым потоком, из которого вызываются библиотечные функции Более того, они должны знать, что, когда Вы к ним обращаетесь, нужно просматривать этот блок данных в вызывающем потоке чтобы не повредить данные в каком-нибудь другом потоке

Так откуда же система знает, что при создании нового потока надо создать и этот блок данных3 Ответ очень прост не знает и знать не хочет Вся ответственность — исключительно на Вас Если Вы пользуетесь небезопасными в многопоточной среде функциями, то должны создавать потоки библиотечной функцией _begmthreadex, а не Windows-функцией CreateThread

unsigned long _beginthreadex( void *secunty unsigned stack size unsigned (*start_address)(void *) void *arglist unsigned initflag unsigned *thrdaddr)

У функции _beginthreadGX тот же список параметров, что и у CreateTbread, но их имена и типы несколько отличаются (Группа, которая отвечает в Microsoft за разра ботку и поддержку библиотеки С/С++, считает, что библиотечные функции не долж ны зависеть от типов данных Wmdows) Как и CreateTbread, функция _beginthreadex возвращает описатель только что созданного потока Поэтому, если Вы раньше поль зовались функцией CreateThread, cc вызовы в исходном коде несложно заменить на вызовы _begtnthreadex Однако из-за некоторого расхождения в типах данных Вам придется позаботиться об их приведении к тем, которые нужны функции _begin threadex, и тогда компилятор будет счастлив Лично я создал небольшой макрос chBEGINTHREADEX, который и делает всю эту работу в исходном коде

typedef unsigned ( stdcall *PTHREAD START) (void *)

#define chBEGINTHREADEX(psa cbStack pfnStartAddr \
pvParam fdwCreate pdwThreadID) \
((HANDLE) _beginthreadex( \
(void *) (psa) \
(unsigned) (cbStack), \
(PTHREAD_START) (pfnStartAddr) \
(void *) (pvParam) \
(unsigned) (fdwCreate) \
(unsigned *) (pdwThreadID)))

Заметьте, что функция _beginthreadex существует только в многопоточных верси ях библиотеки С/С++. Связав проект с однопоточной библиотекой, Вы получите от компоновщика сообщение об ошибке "unresolved external symbol». Конечно, это сде лано специально, потому что однопоточная библиотека не может корректно рабо тать в мпогопоточном приложении. Также обратите внимание на то, что при созда нии нового проекта Visual Studio по умолчанию выбирает однопоточную библиоте ку. Этот вариант не самый безопасный, и для многопоточных приложений Вы долж ны сами выбрать одну из многопоточных версий библиотеки С/С++.

Поскольку Microsoft поставляет исходный код библиотеки С/С++, несложно разоб раться в том, что такого делает _beginthreadex, чего не делает CreateThread, На дистри бутивном компакт-диске Visual Studio ее исходный код содержится в файле Threadex.c. Чтобы нс перепечатывать весь код, я решил дать Вам cc версию в псевдокоде, выде лив самые интересные места.

unsigned long _cdocl _beginthreadex ( void *psa, unsigned cbStack,
unsigned (__stdcall * pTnStartAddr) (void *), void *pvParam, unsigned fdwCreate, unsigned *pdwThreadID)
{
_ptiddata ptd;
// указатель на блок данных потока unsigned long thdl,
// описатель потока
// выделяется блок данных для нового потока

if ((ptd = _calloc_crt(1, sizeof(struct tiddata))) == NULl)
goto error_returnж

// инициализация блока данных
initptd(ptd);

// здесь запоминается нужная функция потока и параметр,
// который мы хотим поместить в блок данных
ptd->_initaddr = (void *) pfnStartAddr;
ptd->_initarg = pvParam;

// создание Honoio потока

thdl = (unsigned long)
CreateThread(psa, cbStack, _threadstartex, (PVOID) ptd, fdwCreate, pdwThrcadID);

if (thdl == NULl) {
// создать поток не удалось, проводится очистка и сообщается об ошибке
goto error_return;
}

// поток успешно создан; возвращается его описатель
return(thdl);

error_return:
// ошибка! не удалось создать блок данных или сам поток
_free_crt(ptd);

return((unsigned long)0L);

}

Несколько важных моментов, связанных с _beginthreadex

Рис. 6-2. Локальная структура tiddata потока, определенная в библиотеке С/С++

Выяснив, как создается и инициализируется структура tiddata для нового потока, посмотрим, как она сопоставляется с этим потоком Взгляните на исходный код фун кции _threadstartex (который тоже содержится в файле Threadex с библиотеки С/С++) Вот моя версия этой функции в псевдокоде

static unsigned long WINAPI threadstartex (void* ptd)
{

// Примечание ptd - это адрес блока tiddata данного потока
// блок tiddata сопоставляется с данным потоком

TlsSetValue( __tlsindex ptd);

// идентификатор этого потока записывается в tiddata
((_ptiddata) ptd)->_tid = GetCurrentThreadId();
// здесь инициализируется поддержка операций над числами с плавающей точкой
// (код не показан)

// пользовательская функция потока включается в SEH-фрейм для обработки
// ошибок периода выполнения и поддержки signal
__try
{

// здесь вызывается функция потока, которой передается нужный параметр;
// код завершения потока передается _endthreadex
_endthreadex( ( (unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr) ) ( ((_ptiddata)ptd)->_initarg ) ) ;

}

_except(_XcptFilter(GetExceptionCode(), GetExceptionInformation()))
{
// обработчик исключений из библиотеки С не даст нам попасть сюда
_exit(GetExceptionGode());

}

// здесь мы тоже никогда не будем, так как в этой функции поток умирает

return(0L);
}

Несколько важных моментов, связанных со _threadstartex.

Последняя функция, которую нам нужно рассмотреть, — это _endthreadex (ее ис ходный код тоже содержится в файле Threadex.c). Вот как она выглядит в моей вер сии (в псевдокоде)

void _cdecl _endthreadex (unsigned retcode)
{
_ptiddata ptd;
// указатель на блок данных потока

// здесь проводится очистка ресурсов, выделенных для поддержки операций
// над числами с плавающей точкой (код не показан)

// определение адреса блока tiddata данного потока
ptd = _getptd();

// высвобождение блока tiddata
_freeptd(ptd);

// завершение потока
ExitThread(retcode);
}

Несколько важных моментов, связанных с _endthreadex

Где-то в начале главы я уже говорил, что прямого обращения к функции ExitThread следует иpбегать Это правда, и я не отказываюсь от своих слов. Тогда же я сказал, что это приводит к уничтожению вызывающего потока и не позволяет ему вернуться из выполняемой в данный момент функции А поскольку она не возвращает управление, любые созданные Вами С++-объекты не разрушаются. Так вот, теперь у Вас есть еще одна причина не вызывать ExitThread. она не дает освободить блок памяти tiddata потока, из-за чего в Вашем приложении может наблюдаться утечка памяти (до его pавершения)

Разработчики Microsoft Visual C++, конечно, прекрасно понимают, что многие все равно будут пользоваться функцией ExitThread, поэтому они кое-что сделали, чтобы свести к минимуму вероятность утечки памяти. Если Вы действительно так хотите самостоятельно уничтожить свой поток, можете вызвать из него _endthreadex (вмес то ExitTbread) и тем самым освободить его блок tiddata. И все же я не рекомендую этого

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

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

Ладно, с функциями все ясно, теперь попробуем проследить, что происходит с глобальными переменными вроде errno. В заголовочных файлах С эта переменная определена так:

#if defined(_MT) || defined(_DLL)
extern int * _cdecl _errno(void);
#define errno (*_еггпо())
#else /* ndef _MT && ndef _DLL */
extern int errno;
#endif /* MT | | _DLL */

Создавая многопоточное приложение, надо указывать в командной строке ком пилятора один из ключей /MT (многопоточное приложение) или /MD (многопоточ

ная DLL); тогда компилятор определит идентификатор _MT. После этого, ссылаясь на errno, Вы будете на самом деле вызывать внутреннюю функцию _errno из библиотеки С/С++. Она возвращает адрес элемента данных errno в блоке, сопоставленном с вы зывающим потоком. Кстати, макрос errno составлен так, что позволяет получать co держимое памяти по этому адресу А сделано это для того, чтобы можно было писать, например, такой код

int *p = &errno;

if (*p == ENOMEM){
...
}

Если бы внутренняя функция _errno просто возвращала значение errno, этот код не удалось бы скомпилировать.

Многопоточная версия библиотеки С/С++, кроме того, "обертывает" некоторые функции синхронизирующими примитивами Всдь если бы два потока одновремен но вызывали функцию malloc, куча могла бы быть повреждена. Поэтому в многопо точной версии библиотеки потоки не могут одновременно выделять память из кучи. Второй поток она заставляет ждать до тех пор, пока первый не выйдет из функции malloc, и лишь тогда второй поток получает доступ к malloc. (Подробнее о синхрони зации потоков мы поговорим в главах 8, 9 и 10.)

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

Динамически подключаемая версия библиотеки С/С++ вполне универсальна ее могут использовать любые выполняемые приложения и DLL, которые обращаются к библиотечным функциям. По этоЙ причине данная библиотека существует лишь в многопоточной версии. Поскольку она поставляется в виде DLL, ее код не нужно вклю чать непосредственно в EXE- и DLL-модули, что существенно уменьшает их размер. Кроме того, если Microsoft исправляет какую-то ошибку в такой библиотеке, то и программы, построенные на ее основе, автоматически избавляются от этой ошибки

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

Ой, вместо _beginthreadex я по ошибке вызвал CreateThread

Вас, наверное, интересует, что случится, если создать поток не библиотечной функ цией _begintbreadex, а Windows-функцией CreateThread Когда этот поток вызовет какую-нибудь библиотечную функцию, которая манипулирует со структурой tiddata, произойдет следующее. (Большинство библиотечных функций реентерабсльно и не требует этой структуры ) Сначала эта функция попытается выяснить адрес блока дан ных потока (вызовом TleGetValue). Получив NULL вместо адреса tiddata, она узнает, что вызывающий поток не сопоставлен с таким блоком. Тогда библиотечная функция тут

же создаст и инициализирует блок tiddata для вызывающего потока. Далее этот блок будет сопоставлен с потоком (через TlsSetValue) и останется при нем до тех пор, пока выполнение потока нс прекратится, С этого моменга данная функция (как, впрочем, и любая другая из библиотеки С/С++) сможет пользоваться блоком tiddata потока.

Как это ни фантасгично, но Ваш поток будет работать почти без глюков. Хотя некоторые проблемы все же появятся. Во-первых, если этот поток воспользуется биб лиотечной функцией signal, весь процесс завершится, так как SEH-фрейм не подго товлен. Во-вторых, если поток завершится, не вызвав endtbreadex, его блокданных не высвободится и произойдет утечка памяти. (Да и кто, интересно, вызовет end threadex иэ потока, созданного с помощью CreateTbread?)

NOTE:
Если Вы связываете свой модуль с многопоточной DLL версией библиотеки С/С++, то при завершении потока и высвобождении блока tiddata (если он был создан), библиотека получает уведомление DLL_THREAD_DETACH. Даже не смотря на то что это предотвращает утечку памяти, связанную с блоком tiddata, я настоятельно советую создавать потоки через _beginthreadex, а не с помощью CreateTbread.

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

В библиотеке С/С++ содержится две функции:

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

и

void _endthread(void);

Первоначально они были созданы для того, чем теперь занимаются новые функ ции _beginthreadex и _endthreadex. Нo, как видите, у _begintbread параметров меньше, и, следовательно, ее возможности ограничены в сравнении с полнофункциональной beginthreadex. Например, работая с _beginthread, нельзя создать поток с атрибутами защиты, отличными от присваиваемых по умолчанию, нельзя создать поток и тут же его задержать — нельзя даже получить идентификатор потока. С функцией _endthread та же история; она не принимает никаких параметров, а это значит, что по оконча нии работы потока его код завершения всегда равен 0.

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

DWORD dwExitCode;

HANDLE hThreatf = _beglntnread(...);

GetExitCodeThread(hThread &dwExitCode);

CloseHandle(hThread);

Весьма вероятно, что созданный поток отработает и завершится еще до того, как первый поток успеет вызвать функцию GetExitCodeThread. Если так и случится, значе ние в hThread окажется недействительным, потому что _endtbread уже закрыла опи сатель нового потока. И, естественно, вызов CloseHandle дает ошибку.

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

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

Потоки часто обращаются к Windows-функциям, которые меняют срсду выполнения. Например, потоку может понадобиться изменить свой приоритет или приоритет процесса. (Приоритеты рассматриваются в главе 7.) И поскольку это не редкость, когда поток модифицирует среду (собственную или процесса), в Windows предусмот рены функции, позволяющие легко ссылаться на объекты ядра текущего процесса и потока:

HANDLE GetCurrentProcess();
HANDLE GetCurrentThread();

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

Псевдоописатели можно использовать при вызове функций, которым нужен опи сатель процесса Так, поток может запросить все временные показатели своего про цесса, вызвав GetProcessTimes:

FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;
GetProcessTimes(GetCurrentProcess(), &ftCreationTime, &ftExirTime, &ftKernelTime, &ftUserTime);

Аналогичным образом поток может выяснить собственные временные показате ли, вызвав GetThreadTimes:

FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;
GetThreadTimes(GetCurrentThread(), &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);

Некоторые Windows-функции позволяют указывать конкретный процесс или по ток no его уникальному в рамках всей системы идентификатору. Вот функции, с по мощью которых поток может выяснить такой идентификатор — собственный или своего процесса:

DWORD GetCurrentProcessId();
DWORD GelCurrentThreadId();

По сравнению с функциями, которые возвращают псевдоописатели, эти функции, как правило, не столь полезны, но когда-то и они могут пригодиться.

Преобразование псевдоописателя в настоящий описатель

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

DWORD WINAPI ParentThread(PVOID pvParam)
{
HANDLE hThreadParent = GetCurrentThread();
CreateThread(NULL, 0, ChildThread, (PVOID) hThreadParent, 0, MULL);

// далее следует какой-то код
}

DWORD WINAPI ChildThread(PVOID pvParam)
{
HANDLE hThreadParent = (HANDLE) pvParam;

FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;

GetTh readTimes(hThreadParent, &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);

// далее следует какой-ro код.
}

Вы заметили, чго здесь не все ладно. Идея была в том, чтобы родительский поток передавал дочернему свой описатель. Но он передает псевдо-, а не настоящий описа тель Начиная выполнение, дочерний поток передает этот псевдоописатель функции GetThreadTimes, и она вследствие этого возвращает временные показатели своего — а вовсе не родительского потока. Происходит так потому, что псевдоописатель яв ляется описателем текущего потока, т e. того, который вызывает эту функцию.

Чтобы исправить приведенный выше фрагмент кода, превратим псевдоописатель в настоящий через функцию DuplicateHandle (о ней я рассказывал в главе 3):

BOOL DuplicateHandle( HANDLE hSourceProcess, HANDLE hSource, HANDLE hTargetProcess, PHANDLE phTarget, DWORD fdwAccess, BOOL bInhentHandle, DWORD fdwOpfions),

Обычно она используется для создания нового "процессо-зависимого» описателя из описателя объекта ядра, значепие которого увязано с другим процессом. А мы вос пользуемся DuplicateHandle не совсем по назначению и скорректируем с ее помощью наш фрагмент кода так

DWORD WINAPI ParentThread(PVOID pvParam)
{
HANDLE hThreadParent;

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

CreateThread(NULL, 0, ChildThread, (PVOID) hThreadParent, 0, NULL) ;

// далее следует какой-то код
}

DWORD WINAPI ChildThread(PVOID pvParam)
{

HANDLE hThreadParent = (HANDLE) pvParam;

FILETIME ftCreaUonTime, ftExitTime, ftKernelTime, ftUserTime;

GetThreadTimes(hThreadParent, &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);

CloseHandle(hThreadParent);

// далее следует какой-то код..
}

Тeпeрь родительский поток преобразует свой "двусмысленный» псевдоописатель в настоящий описатель, однозначно определяющий родительский поток, и передает его в CreateThread Когда дочерний поток начинает выполнение, его параметр pvParam содержит настоящий описатель потока. В итоге вызов какой-либо функции с этим описателем влияет не на дочерний, а на родительский поток

Поскольку DuplicateHandle увеличивает счетчик пользователей указанного объек та ядра, то, закончив работу с продублированным описателем объекта, очень важно не забыть уменьшить счетчик Сразу после обращения к GetThreadTimes дочерний поток вызывает CloseHandle, уменьшая тем самым счетчик пользователей объекта "ро дительский поток" на 1 В этом фрагменте кода я исходил из того, что дочерний по ток не вызывает других функций с передачей этого описателя Если же ему надо выз вать какие-то функции с передачей описателя родительского потока, то, естествен но, к CloseHandle следует обращаться только после тоoro, как необходимость в этом описателе у дочернего потока отпадет

Надо заметить, что DuphcateHandle позволяет преобразовать и псевдоописатель процесса. Вот как это сделать

HANDLE hProcess;

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

// те же атрибуты защиты, что и псевдоописателю