Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009

.pdf
Скачиваний:
6265
Добавлен:
13.08.2013
Размер:
31.38 Mб
Скачать

Глава 6. Базовые сведения о потоках.docx 189

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

_exit(GetExceptionCode());

}

}

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

Новый поток начинает выполнение с RtlUserThreadStart (in NTDLLdll), а затем переходит в Jhreadstartex.

В качестве единственного параметра функции _threadstartex передается адрес блока _tiddata нового потока.

Windows-функция TkSetValue сопоставляет с вызывающим потоком значение, которое называется локальной памятью потока (Thread Local Storage, TLS) (о ней я расскажу в главе 21), а _threadstartex сопоставляет блок _tiddata с новым потоком.

Функция потока заключается в SEH-фрейм. Он предназначен для обработки ошибок периода выполнения (например, не перехваченных исключений С++), поддержки библиотечной функции signal и др. Этот момент, кстати, очень важен. Если бы вы создали поток с помощью CreateThread, а потом вызвали библиотечную функцию signal, она работала бы некорректно.

Далее вызывается функция потока, которой передается нужный параметр. Адрес этой функции и ее параметр были сохранены в блоке _tiddata в TLS функ-

цией _beginthreadex, извлекают их из TLS функцией _callthreadstartex.

Значение, возвращаемое функцией потока, считается кодом завершения этого потока. Обратите внимание: _callthreadstartex не возвращается в _threadstartex и затем в _RtlUserThreadStart. Иначе после уничтожения потока его блок _tiddata так и остался бы в памяти. А это привело бы к утечке памяти в вашем приложении. Чтобы избежать этого, _threadstartex вызывает другую библиотечную функцию, _endthreadex, и передает ей код завершения.

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

void __cdecl _endthreadex (unsigned retcode) {

_ptiddata ptd; // указатель на блок данных потока

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

//над числами с плавающей точкой (код не показан)

//определение адреса блока tiddata данного потока

ptd = _getptd_noexit ();

// высвобождение блока tiddata if (ptd !* NULL)

_freeptd(ptd);

190 Часть II. Приступаем к работе

// завершение потока

ExitThread(retcode);

}

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

Библиотечная функция _getptd_noexit обращается к Windows-функции TlsGetValue, которая сообщает адрес блока памяти _tiddata вызывающего потока.

Этот блок освобождается, и вызовом ExitThread поток разрушается. При этом, конечно, передается корректный код завершения.

Где-то в начале главы я уже говорил, что прямого обращения к функции Ex-

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

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

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

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

_CRTIMP extern int * __cdecl _errno(void); #define errno (*_errno())

int* __cdecl _errno(void) { _ptiddata ptd = _getptd_noexit(); if (!ptd) {

return &ErrnoNoMem; } else {

Глава 6. Базовые сведения о потоках.docx 191

return (Aptd->_terrno);

}

}

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

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

}

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

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

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

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

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

192 Часть II. Приступаем к работе

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

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

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

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

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

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

unsigned long _beginthread(

void (__cdecl *start_address)(void *), unsigned stack_size,

void *arglist);

и

void _endthread(void);

Первоначально они были созданы для того, чем теперь занимаются новые функции _beginthreadex и _endthreadex. Но, как видите, у _beginthread параметров меньше, и, следовательно, ее возможности ограничены в сравнении

Глава 6. Базовые сведения о потоках.docx 193

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

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

DW0RD dwExitCode;

HANDLE hThread * _beginthread(...);

GetExitCodeThread(hThread, &dwExitCode);

CloseHandle(hThread);

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

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

стоятельно вызывает _endthreadex, а _beginthread обращается к _endthread.

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

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

HANDLE GetCurrentProcess();

HANDLE GetCurrentThread();

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

GetLastError — ERROR_INVALID_HANDLE.

194 Часть II. Приступаем к работе

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

FILETIME ftCreationTime, ftExitTiroe, ftKernelTiroe, ftUserTime; GetProcessTimes(GetCurrentProcess(),

&ftCreationTime, &ftExitTiroe, &ftKernelTiroe, &ftUserTime);

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

тели, вызвав GetThreadTimes:

FILETIME ftCreationTime, ftExitTime, ftKernelTiroe, ftUserTiroe; GetThreadTimes(GetCurrentThread(),

&ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);

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

DWORD GetCurrentProcessId();

DWORD GetCurrentThreadId();

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

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

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

DW0R0 WINAPI ParentThread(PVOID pvParam) { HANDLE hThreadParent = GetCurrentThread();

CreateThread(NULL, 0, ChildThread, (PV0ID) hThreadParent, 0, NULL); // далее следует какой-то код...

}

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

FILETIME ftCreationTime, ftExitTime, ftKernelTiroe, ftUserTime; GetThreadTimes(hThreadParent,

&ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime); // далее следует какой-то код...

}

Вы заметили, что здесь не все ладно? Идея была в том, чтобы родительский поток передавал дочернему свой описатель. Но он передает псевдо-, а не настоящий описатель. Начиная выполнение, дочерний поток передает

Глава 6. Базовые сведения о потоках.docx 195

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

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

BOOL DuplicateHandle(

HANDLE hSourceProcess,

HANDLE hSource,

HANDLE hTargetProcess,

PHANDLE phTarget,

DWORD dwDesiredAccess,

BOOL bInheritHandle,

DW0RD dwOptions);

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

DW0RD WINAPI ParentThread(PVOID pvParam) {

HANDLE hThreadParent;

DuplicateHandle(

 

GetCurrentProcess(),

// описатель процесса.к которому

 

// относится псевдоописатель потока;

GetCurrentThread(),

// псевдоописатель родительского потока;

GetCurrentProcess(),

// описатель процесса, к которому

 

// относится новый, настоящий описатель потока;

&hThreadParent,

// даст новый, настоящий описатель,

 

// идентифицирующий родительский поток;

0,

// игнорируется из-за DUPLICATE_SAME_ACCESS;

FALSE,

// новый описатель потока ненаследуемый;

DUPLICATE_SAME_ACCESS);

// новому описателю потока присваиваются

 

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

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

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

}

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

FILETIME ftCreationTime, ftExitTime, ftKernelTlme, ftUserTime; GetThreadTimes(hThreadParent,

&ftCreationTlme, &ftExltTlme, &ftKernelTlme, &ftUserTime); CloseHandle(hTh readParent);

196Часть II. Приступаем к работе

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

}

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

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

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

HANDLE hProcess;

 

DuplicateHandle(

 

GetCurrentProcess(),

// описатель процесса, к которому

 

// относится псевдоописатель;

GetCurrentProcess(),

// псевдоописатель процесса;

GetCurrentProcess(),

// описатель процесса, к которому

 

// относится новый, настоящий описатель;

&hProcess,

// даст новый, настоящий описатель,

 

// идентифицирующий процесс;

0,

// игнорируется из-за DUPLICATE_SAME_ACCESS;

FALSE,

// новый описатель процесса ненаследуемый;

DUPLICATE_SAME_ACCESS);

// новому описателю процесса присваиваются

 

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

Оглавление

 

Г Л А В А 7 Планирование потоков, приоритет и привязка к процессорам..............................

238

Приостановка и возобновление потоков....................................................................................

241

Приостановка и возобновление процессов...............................................................................

242

Функция Sleep .....................................................................................................................................

244

Переключение потоков.....................................................................................................................

244

Переключение потоков на компьютерах с процессором, поддерживающим

 

HyperThreading................................................................................................................................

245

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

246

Структура CONTEXT ..........................................................................................................................

250

Приоритеты потоков .........................................................................................................................

255

Абстрагирование приоритетов ......................................................................................................

256

Программирование приоритетов ..................................................................................................

261

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

264

Подстройка планировщика для активного процесса ..........................................................

265

Приоритеты запросов ввода-вывода ......................................................................................

266

Программа-пример Scheduling Lab...........................................................................................

268

Привязка потоков к процессорам .................................................................................................

275

Г Л А В А 7

Планирование потоков, приоритет и привязка к процессорам

Глава 7. Планирование потоков, приоритет и привязка к процессорам.docx 239

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

В главе 6 мы уже обсудили структуру CONTEXT, поддерживаемую в объекте ядра «поток», и выяснили, что она отражает состояние регистров процессора на момент последнего выполнения потока процессором. Каждые 20 мс (или около того, как задано вторым параметром GetSystemTimeAdjustment) Windows просматривает все существующие объекты ядра «поток» и отмечает те из них, которые могут получать процессорное время. Далее она выбирает один из таких объектов и загружает в регистры процессора значения из его контекста. Эта операция называется переключением контекста (context switching). По каждому потоку Windows ведет учет того, сколько раз он подключался к процессору. Этот показатель сообщают специальные утилиты вроде Microsoft Spy++. Например, на иллюстрации ниже показан список свойств одного из потоков. Обратите внимание, что этот поток подключался к процессору 182524 раза.

Поток выполняет код и манипулирует данными в адресном пространстве своего процесса. Примерно через 20 мс Windows сохранит значения регистров процессора в контексте потока и приостановит его выполнение. Далее система просмотрит остальные объекты ядра «поток», подлежащие выполнению, выберет один из них, загрузит его контекст в регистры процессора, и все повторится. Этот цикл операций — выбор потока, загрузка его контекста, выполнение и сохранение контекста — начинается с момента запуска системы и продолжается до ее выключения.

Соседние файлы в предмете Программирование на C++