Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
385
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

завершения процесса. Последствия могут быть плачевны ~ вплоть до потери данных. Вызывайте TerminateProcess только в самом крайнем случае!

На рис. 20-2 показаны операции, выполняемые при вызове LoadLibrary, а на рис. 20-3 -

при вызове FreeLibrary.

Рис. 20-3. Операции, выполняемые системой при вызове потоком функции FreeLibrary

Уведомление DLL_THREAD_ATTACH

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

DLL Работа его собственной (стартовой) функции начинается лишь после того, как все

DLL-модули oбpaбoтaют уведомление DLL_THREAD_ATTACH.

Если в момент проецирования DLL на адресное пространство процесса в нем выполняется несколько потоков, система не вызывает DllMain со значением DLL_ THREAD_ATTACH ни для одного из существующих потоков. Вызов DllMain c этим значением осуществляется, только если DLL проецируется на адресное пространство процесса в момент создания потока.

Обратите также внимание, что система не вызывает функции DllMain co значением DLL_THREAD_ATTACH и для первичного потока процесса. Любая DLL, проецируемая на адресное пространство процесса в момент его создания, получает уведомление

DLL_PROCESS_ATTACH, а не DLL_THREAD_ATTACH

Уведомление DLL_THREAD_DETACH

Лучший способ завершить поток — дождаться возврата из его стартовой функции, после чего система вызовет ExitThread и закроет поток. Эта функция лишь сообщает системе о том, что поток хочет завершиться, но система не уничтожает его немедленно. Сначала она просматривает все проекции DLL, находящиеся в данный момент в адресном пространстве процесса, и заставляет завершаемый поток вызвать DllMain в каждой из этих DLL со значением DLL_THREAD_DETACH Тсм самым она уведомляет DLL модули о необходимости очистки, связанной с данным потоком. Например, DLLверсия библиотеки С/С++ освобождает блок данных, используемый для управления многопоточными приложениями.

Заметьте, что DLL может не дать потоку завершиться. Например, такое возможно, когда функция DllMain, получив уведомление DLL_THREAD_DETACH, входит в бесконечный цикл. А операционная система закрывает поток только после того, как все DLL заканчивают обработку этого уведомления.

NOTE:

Если поток завершается из-за того, что другой поток вызвал для него TerminateThread, система не вызывает DllMain со значением DLL_THREAD_DETACH Следовательно, ни одна DLL, спроецированная на адресное пространство процесса, не получит шанса на выполнение очистки до завершения потока, что может привести к потере данных Поэтому TetminateThread, как и TerminateProcess, можно использовать лишь в самом крайнем случае!

Если при отключении DLL еще выполняются какие-то потоки, то для них DllMain не вызывается со значением DLL_THREAD_DETACH Вы можете проверить это при обработке DLL_PROCESS_DETACH и провести необходимую очистку.

Ввиду упомянутых выше правил не исключена такая ситуация: поток вызывает LoadIibrary для загрузки DLL, в результате чего система вызывает из этой библиотеки DllMain со значением DLL_PROCESS_ATTACH. (В этом случае уведомление DLL_ THREAD_ATTACH не посылается.) Затем поток, загрузивший DLL, завершается, что приводит к новому вызову DllMain — на этот раз со значением DLL_THREAD_DETACH Библиотека уведомляется о завершении потока, хотя она не получала DLL_ THREAD_ATTACH, уведомляющего о его подключении. Поэтому будьте крайне осторожны при выполнении любой очистки, связанной с конкретным потоком К счастью, большинство программ пишется так, что LoadLibrary и FreeLibrary вызываются одним потоком.

Как система упорядочивает вызовы DIIMain

Система упорядочивает вызовы функции DllMain. Чтобы понять, что я имею в виду, рассмотрим следующий сценарий Процесс А имеет два потока: А и В. На его адресное пространство проецируется DLL-модуль SomeDLL.dll. Оба потока собираются вызвать CreateThread, чтобы создать еще два потока: С и D.

Когда поток А вызывает для создания потока С функцию CreateThread, система обращается к DllMain из SomeDLL.dll со значением DLL_THREAD_АТТАСН. Пока поток С исполняет код DllMain, поток В вызывает CreateThread для создания потока D. Системе нужно вновь обратиться к DllMain со значением DLL_THREAD_ATTACH, и на этот раз код функции должен выполнять поток D. Но система упорядочивает вызовы DllMain. и поэтому приостановит выполнение потока D, пока поток С не завершит обработку кода DllMain и не выйдет из этой функции.

Закончив выполнение DllMain, поток С может начать выполнение своей функции потока. Теперь система возобновляет поток D и позволяет ему выполнить код DllMain, при возврате из которой он начнет обработку собственной функции потока

Обычно никто и не задумывается над тем, что вызовы DllMain упорядочиваются. Но я завел об этом разговор потому, что один мой коллега как-то раз написал код, в котором была ошибка, связанная именно с упорядочиванием вызовов DllMain, Его код выглядел примерно так:

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)

{

HANDLE hThread; DWORD dwThreadId;

switch (fdwReason)

{

case DLL_PROCESS_ATTACH:

//DLL проецируется на адресное пространство процесса

//создаем поток для выполнения какой-то работы hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId);

//задерживаем наш поток до завершения нового потока

WaitForSingleObject(hThread,

INFINITE);

//доступ к новому потоку больше не нужен

CloseHandle(hThread);

break;

case DLL_THREAD_ATTACH:

// создается еще один поток break;

case DLL_THREAD_DETACH:

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

break;

case DLL_PROCESS_DETACH:

// DLL выгружается из адресного пространства процесса

break;

}

return(TRUE);

}

Нашли «жучка»? Мы-то его искали несколько часов. Когда DllMain получаст уведомление DLL_PROCESS_ATTACH, создается новый поток. Системе нужно вновь вызвать эту же DllMain со значением DLL_THREAD_ATTACH Но выполнение нового потока приостанавливается ведь поток, из-за которого в DllMain было отправлено уведомление DLL_PROCFSS_ATTACH, свою работу еще не закончил. Проблема кроется в вызове WaitForSingleObject. Она приостанавливает выполнение текущего потока до тех пор, пока не завершится новый. Однако у нового потока нет ни единою шанса не только на завершение, но и на выполнение хоть какого-нибудь кода — он приостановлен в ожидании того, когда текущий поток выйдет из DllMain Вот Вам и взаимная блокировка

— выполнение обоих потоков задержано навеки!

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

DisableThreadLibraryCalls:

BOOl DisableThreadLibraryCalls(HINSTANCE hinstDll);

Вызывая ее, Вы сообщаете системе, что уведомления DLL_THREAD_ATTACH и DLL_ THREAD_DETACH не должны посылаться DllMain той библиотеки, которая указана в вызове Мне показалось логичным, что взаимной блокировки не будет, если система не стаиет посылать DLL уведомления. Но, проверив свое решение (см. ниже), я убедился, что это не выход.

BOOL WINAPI DllMain(HINSTANCE hinstDll, DWORD fdwReason, PVOID fImpLoad)

{

HANDLE hThread; DWORD dwThreadId;

switch (fdwReason)

{

case DLL_PROCESS_ATTACH.

//DLL проецируется на адресное пространство процесса

//предотвращаем вызов DllMain при создании

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

DisableThreadLibraryCalls(hinstDll);

//создаем поток для выполнения какой-то работы

hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId);

//задерживаем наш поток до завершения нового потока

WaitForSingleObject(hThread, INFINITE);

//доступ к новому потоку больше не нужен

CloseHandle(hThread);

break;

саsе DLL_THREAD_ATTACH:

// создается сщс один поток break;

case DLL_THREAD_DETACH:

// поток завершается корректно break;

case DLL_PROCESS_DETACH:

// DLL выгружается из адресного пространства процесса

break;

}

return TRUE;

}

Потом я понял, в чем лело Создавая процесс, система создает и объект-мьютекс. У каждого процесса свой объект-мьютекс — он не разделяется между несколькими процессами. Его назначение — синхронизация всех потоков процесса при вызове ими функций DllMain из DLL, спроецированных на адресное пространство данного процесса.

Когда вызывается CreateThread, стстема создает сначала объект ядра "поток" и стек потока, затем обращается к WaitForSingleObject, передавая ей описатель объектамьютекса данного процесса. Как только поток захватит этот мьютекс, система заставит его вызвать DllMain из каждой DLL со значением DLL_THREAD_ATTACH. И лишь тогда система вызовет ReleaseMutex, чтобы освободить объект-мьютекс Вот из-за того, что система работает именно так, дополнительный вызов DisableThreadLibraryCalls и не предотвращает взаимной блокировки потоков. Единственное, что я смог придумать, — переделать эту часть исходного кода так, чтобы ни одна DllMain не вызывала

WaitForSingleObject