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

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

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

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

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

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

На рис. 6-1 показано, что именно должна сделать система, чтобы создать и инициализировать поток.

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

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

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

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

Указатель команд (IP) и указатель стека (SP) — два самых важных регистра в контексте потока. Вспомните: потоки выполняются в контексте процесса.

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

Соответственно эти регистры всегда указывают на адреса памяти в адресном пространстве процесса. Когда система инициализирует объект ядра «поток», указателю стека в структуре CONTEXT присваивается тот адрес, по которому в стек потока было записано значение pfhStartAddr, а указателю команд — адрес недокументированной (и неэкспортируемой) функции RtlUserThreadStart. Эта функция содержится в модуле NTDLLdll (см. рис. 6-1).

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

VOID RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam)

{

_try { ExitThread((pfnStartAddr)(pvParam));

}

__except(UnhandledExceptlonFilter(GetExceptionInformation())) { ExitProcess(GetExceptionCode());

}

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

}

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

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

Когда новый поток выполняет RtiUserThreadStort;nponcxojwr следующее.

Ваша функция потока включается во фрейм структурной обработки исключений (далее для краткости — SEH-фрейм), благодаря чему любое исключение, если оно происходит в момент выполнения вашего потока, получает хоть ка- кую-то обработку, предлагаемую системой по умолчанию. Подробнее о структурной обработке исключений см. главы 23,24 и 25.

Система обращается к вашей функции потока, передавая ей параметр pvParam, который вы ранее передали функции CreateThread.

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

Когда ваша функция потока возвращает управление, RtlUserThreadStart вызывает ExitThread, передавая ей значение, возвращенное вашей функцией. Счетчик числа пользователей объекта ядра «поток» уменьшается на 1, и выполнение потока прекращается.

Если ваш поток вызывает необрабатываемое им исключение, его обрабатывает SEH-фрейм, построенный функцией RtlUserThreadStart. Обычно в результате этого появляется окно с каким-нибудь сообщением, и, когда пользователь закрывает его, RtlUserThreadStart вызывает ExitProcess и завершает весь процесс, а не только тот поток, в котором произошло исключение.

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

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

При инициализации первичного потока его указатель команд устанавливается на ту же недокументированную функцию — RtlUserThreadStart.

Функция RtlUserThreadStart обращается к стартовому коду библиотеки С/С++, который выполняет необходимую инициализацию, а затем вызывает вашу входную функцию _tmain или _tWinMain. Когда входная функция возвращает управление, стартовый код библиотеки С/С++ вызывает ExitProcess. Поэтому первичный поток приложения, написанного на С/С++, никогда не возвращается в RtlUserThreadStart.

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

Майкрософт поставляет с Visual Studio шесть библиотек С/С++ (четыре «родные» и две — для управляемого мира .NET). Их краткое описание представлено в следующей таблице.

Табл. 6-1. Библиотеки С/С++, поставляемые с Visual Studio

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

Описание

LibCMt.lib

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

LibCMtD.lib

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

 

ных приложений

MSVCRt.lib

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

 

MSVCR80.dll; поддерживает как одно-, так и многопоточные приложения

 

(используется с новыми проектами)

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

Табл. 6-1. (окончание)

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

Описание

MSVCRtD.lib

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

 

MSVCR80D.dll; поддерживает как одно-, так и много поточные приложения

MSVCMRt.lib

Библиотека импорта для смешанного (управляемого и «родного») кода

MSVCURt.lib

Библиотека импорта для «чистого» MSIL-кода

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

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

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

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

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

if (fFailure) { switch (errno) {

case E2BIG: // список аргументов или размер // окружения слишком велик

break;

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

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

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

}

}

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

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

_doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime, gmtime, _ecvt, _fcvt — список можно продолжить.

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

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

_beginthreadex, а не Windows-функцией CreateThread.

unsigned long _beginthreadex(

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

void *security, unsigned stack_size,

unsigned (*start_address)(void *), void *arglist,

unsigned initflag, unsigned *thrdaddr);

У функции _beginthreadex тот же список параметров, что и у CreateThread, но их имена и типы несколько отличаются. (Группа, которая отвечает в Майкрософт за разработку и поддержку библиотеки С/С++, считает, что библиотечные функции не должны зависеть от типов данных Windows.) Как и CreateThread, функция _beginthreadex возвращает описатель только что созданного потока. Поэтому, если вы раньше пользовались функцией CreateThread, ее вызовы в исходном коде несложно заменить на вызовы _beginthreadex. Однако из-за некоторого расхождения в типах данных вам придется позаботиться об их приведении к тем, которые нужны функции _beginthreadex, и тогда компилятор будет счастлив. Лично я создал небольшой макрос, chBEGINTHREADEX, который и делает всю эту работу в исходном коде.

typedef unsigned (__stdcall *PTHREAD_START) (void *);

#define chBEGINTHREADEX(psa, cbStack, pfnStartAddr,

\

pvParam, fdwCreate, pdwThreadID)

\

((HANDLE) _beginthreadex(

\

(void *) (psa),

\

(unsigned) (cbStackSize),

\

(PTHREAD_START) (pfnStartAddr).

\

(void *) (pvParam),

\

(unsigned) (dwCreateFlags),

\

(unsigned *) (pdwThreadID)))

 

Поскольку Майкрософт поставляет исходный код библиотеки С/С++, несложно разобраться в том, что такого делает _begtnthreadex, чего не делает CreateThread. На жестком диске Visual Studio ее исходный код содержится в файле <Program Files>\Microsoft Visual Studio 8\VC\crt\src\Threadex.c. Чтобы не перепечаты-

вать весь код, я решил дать вам ее версию в псевдокоде, выделив самые интересные места.

uintptr_t __cdecl _beginthreadex ( void *psa,

unsigned cbStackSize,

unsigned (__stdcall * pfnStartAddr) (void *), void * pvParam,

unsigned dwCreateFlags, unsigned *pdwThreadID) {

 

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

_ptiddata ptd;

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

uintptr_t thdl;

// описатель потока

// выделяется блок данных для нового потока

if ((ptd = (_ptiddata)_calloc_crt(1, sizeof(struct _tiddata))) == NULL) goto error_return;

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

//Здесь запоминается нужная функция потока и параметр,

//который мы хотим поместить в блок данных.

ptd->_initaddr = (void *) pfnStartAddr; ptd->_initarg = pvParam;

ptd->_thandle = (uintptr_t)(-1);

// создание нового потока.

thdl * (uintptr_t) CreateThread((LPSECURITY_ATTRIBUTES)psa, cbStackSize, _threadstartex, (PVOID) ptd, dwCreateFlags, pdwThreadID);

if (thdl == 0) {

// создать поток не удалось; проводится очистка и сообщается об ошибке goto error_return;

}

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

error_return:

//Ошибка: не удалось создать блок данных или сам поток.

//GetLastError() сопоставлена соответствующим значениям errno,

//которые возвращаются при ошибке CreateThread.

_free_crt(ptd); return((uintptr_t)0L);

}

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

Каждый поток получает свой блок памяти tiddata, выделяемый из кучи, которая принадлежит библиотеке С/С++.

Адрес функции потока, переданный _beginthreadex, запоминается в блоке памяти _tiddata (определен в заголовочном файле Mtdll.h). Там же сохраняется и параметр, который должен быть передан этой функции.

Функция _beginthreadex вызывает CreateThread, так как лишь с ее помощью

операционная система может создать новый поток.

■ При вызове CreateThread сообщается, что она должна начать выполнение нового потока с функции _threadstartex, а не с того адреса, на кото-

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

рый указывает pfhStartAddr. Кроме того, функции потока передается не параметр pvParam, а адрес структуры _tiddata.

Если все проходит успешно, _beginthreadex, как и CreateThread, возвращает описатель потока. В ином случае возвращается 0.

struct _tiddata {

 

 

unsigned long

_tid;

/* идентификатор потока */

unsigned long

_thandle;

/* описатель потока */

int

_terrno;

/* значение errno */

unsigned

long

_tdoserrno;

/* значение _doserrno */

unsigned

int

_fpds;

/* сегмент данных Floating Point */

unsigned long

_holdrand;

/* зародышевое значение для rand() */

char*

 

_token;

/* указатель (ptr) на метку strtok()*/

wchar_t*

 

_wtoken;

/* ptr на метку wcstok() */

unsigned char*

_mtoken;

/* ptr на метку jnbstok() */

/* следующие указатели обрабатываются функцией roalloc в период выполнения */

char*

 

_errrosg;

/* ptr to strerror()/_strerror() buff*/

wchar_t*

 

_werrrosg;

/* ptr на буфер strerror()/_strerror()*/

char*

 

_namebuf0;

/* ptr на буфер tmpnam() */

wchar_t*

 

_wnamebuf0;

/* ptr на буфер _wtmpnam() */

char*

 

_namebuf1;

/* ptr на буфер tropfile() */

wchar_t*

 

_wnamebuf1;

/* ptr на буфер _wtmpfile() */

char*

 

_asctimebuf;

/* ptr на буфер asctiroe() */

wchar_t*

 

_wasctiroebuf;

/* ptr на буфер _wasctiroe() */

void*

 

_gmtiroebuf;

/* ptr на структуру gmtime() */

char*

 

_cvtbuf;

/* ptr на буфер ecvt()/fcvt */

unsigned char _con_ch_buf[MB_LEM_MAX];

 

 

 

/* ptr на буфер putch() */

unsigned short _ch_buf_used;/* используется ли _con_ch_buf */

/* следующие поля используются кодом _beginthread */

void*

_initaddr;

/* начальный адрес пользовательского потока*/

void*

_initarg;

/* начальный аргумент пользовательского потока */

/* следующие три поля нужны для поддержки функции signal и обработки ошибок, * возникающих в период выполнения */

void*

_pxcptacttab;

/*

ptr на таблицу "исключение-действие" */

void*

_tpxcptinfoptrs;

/*

ptr на указатели к информации об исключении */

int

_tfpecode;

/*

код исключения для операций над числами

 

 

*

с плавающей точкой */

/* указатель на копию мультибайтовых ресурсов потока */ pthreadrobcinfo ptnbcinfo;

 

 

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

/* указатель на копию локализованных ресурсов потока */

pthreadlocinfo ptlocinfo;

 

int

_ownlocale;

/* если равно 1, у потока собственные

 

 

региональные параметры */

/* следующее поле нужно подпрограммам NLG */

Unsigned

long

_NLG_dwCode;

/*

* данные для отдельного потока, используемые при обработке исключений в С++

*/

 

 

void*

_terminate;

/* подпрограмма terminate() */

void*

_unexpected;

/* подпрограмма unexpected() */

void*

_translator;

/* транслятор S.E. */

void*

_purecall;

/* для чисто виртуальных функций */

void*

_curexception;

/* текущее исключение */

void*

_curcontext;

/* контекст текущего исключения */

int

_ProcessingThrow;

/* для неперехваченных исключений */

void*

_curexcspec;

/* для обработки исключений, сгенерированных

 

 

std::unexpected */

#if defined (_M_IA64) || defined (_M_AMD64) void * _pExitContext;

void* _pUnwindContext; void* _pFrameInfoChain; unsigned _int64 _ImageBase;

#if defined(_M_IA64)

unsigned __int64 _TargetGp; #endif /* defined (JLIA64) */

unsigned __int64 _ThrowImageBase; void* _pForeignException;

#elif defined (_M_IX86)

void* _pFrameInfoChain; #endif /* defined (_M_IX86) */

_setloc_struct _setloc_data;

void*

_encode_ptr;

/* процедура EncodePointer()*/

void*

_decode_ptr;

/* процедура DecodePointer()*/

void*

_reserved1;

/* пусто */

void*

_reserved2;

/* пусто */

void*

_reserved3;

/* пусто */

int _

cxxReThrow;

/* для повторно сгенерированных исключений

 

 

устанавливается в True */

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

unsigned long __initDomain; /* домен, который _beginthread[ex]

изначально использует для управляемого кода */

};

typedef struct _tiddata * _ptiddata;

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

static unsigned long WINAPI _threadstartex (void* ptd) {

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

//блок tiddata сопоставляется с данным потоком

//_getptd() найдет его в _callthreadstartex TlsSetValue(__tlsindex, ptd);

//идентификатор этого потока записывается в tiddata ((_ptiddata) ptd)->_tid = GetCurrentThreadId();

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

//вызов вспомогательной функции

_callth read sta rtex();

// Сюда мы никогда не попадем - по завершении _callthreadstartex поток умирает return(0L);

}

static void _callthreadstartex(void) {

_ptiddata ptd; /* указатель на структуру _tiddata потока */

//получаем указатель на данные потока из TLS ptd = _getptd();

//Пользовательская функция потока включается в SEH-фрейм для обработки

//ошибок периода выполнения и поддержки signal.

__try {

//Здесь вызывается функция потока, которой передается нужный параметр;

//код завершения потока передается _endthreadex.

_endthreadex(

( (unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr) )

( ((_ptiddata)ptd)->_initarg ) ) ;

}

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

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