- •Создание эффективных win32-приложений
- •Глава 1. Обработка ошибок 8
- •Глава 2 Unicode 14
- •Глава 3 Объекты ядра 31
- •Часть II начинаем работать 51
- •Глава 4 Процессы 51
- •Глава 5 Задания 88
- •Глава 6 Базовые сведения о потоках 106
- •Глава 7 Планирование потоков, приоритет и привязка к процессорам 131
- •Глава 8 Синхронизация потоков в пользовательском режиме 159
- •Глава 9 Синхронизация потоков с использованием объектов ядра 181
- •Глава 10 Полезные средства для синхронизации потоков 211
- •Глава 14 Исследование виртуальной памяти 281
- •Глава 15 Использование виртуальной памяти в приложениях 292
- •Глава 16 Стек потока 309
- •Глава 17 Проецируемые в память файлы 318
- •Глава 18 Динамически распределяемая память 356
- •Часть IV динамически подключаемые библиотеки 370
- •Глава 19 dll: основы 370
- •Глава 20 dll: более сложные методы программирования 384
- •Глава 21 Локальная память потока 417
- •Глава 22 Внедрение dll и перехват api-вызовов 423
- •Часть V структурная обработка исключений 447
- •Глава 23 Обработчики завершения 447
- •Глава 24 Фильтры и обработчики исключений 463
- •Часть VI операции с окнами 510
- •Глава 2 Unicode
- •Глава 3 Объекты ядра
- •Часть II начинаем работать
- •Глава 4 Процессы
- •Глава 5 Задания
- •Глава 6 Базовые сведения о потоках
- •Глава 7 Планирование потоков, приоритет и привязка к процессорам
- •Глава 8 Синхронизация потоков в пользовательском режиме
- •Глава 9 Синхронизация потоков с использованием объектов ядра
- •Глава 10 Полезные средства для синхронизации потоков
- •Глава 11 Пулы потоков
- •Глава 12 Волокна
- •Часть III управление памятью
- •Глава 13 Архитектура памяти в Windows
- •Глава 14 Исследование виртуальной памяти
- •Глава 15 Использование виртуальной памяти в приложениях
- •Глава 16 Стек потока
- •Глава 17 Проецируемые в память файлы
- •Глава 18 Динамически распределяемая память
- •Часть IV динамически подключаемые библиотеки
- •Глава 19 dll: основы
- •Глава 20 dll: более сложные методы программирования
- •Глава 21 Локальная память потока
- •Глава 22 Внедрение dll и перехват api-вызовов
- •Часть V структурная обработка исключений
- •Глава 23 Обработчики завершения
- •Глава 24 Фильтры и обработчики исключений
- •Часть VI операции с окнами
- •Глава 26 Оконные сообщения
- •Глава 27 Модель аппаратного ввода и локальное состояние ввода
Глава 21 Локальная память потока
Когда данные удобно связывать с экземпляром какого-либо объекта. Например, чтобы сопоставить какие-то дополнительные данные с окном, применяют функции SetWindowWord и SetWindowLong. Локальная память потока (thread-local storage, TLS) позволяет связать данные и с определенным потоком (скажем, сопоставить с ним время его создания), а по завершении этого потока вычислить время его жизни.
TLS также используется в библиотеке С/С++. Но эту библиотеку разработали задолго до появления многопоточных приложений, и большая часть содержащихся в ней функций рассчитана на однопоточные программы. Наглядный пример — функция strtok. При первом вызове она получает адрес строки и зяпоминаст сго в собственной статической переменной Когда при следующих вызовахstrtok Вы передаете ей NULL, она оперирует с адресом, записанным в своей переменной.
В многопоточной среде вероятна такая ситуация; один поток вызывает strtok, и, не успел он вызвать cc повторно, как к ней уже обращается другой. Тогда второй поток заставит функцию занести в статическую переменную новый адрес, неизвестный первому. И в дальнейшем первый поток, вызываяstrtok, будет использовать строку, принадлежащую второму. Вот Вам и "жучок", найти который очень трудно
Чтобы устранить эту проблему, в библиотеке С/С++ теперь применяется механизм локальной памяти потока: за каждым потоком закрепляется свой строковый указатель, зарезервированный для strtok. Аналогичный механизм действует и для других библиотечных функций, в том числеasctime иgmtime
Локальная память потока может бьть той соломинкой, за которую придется ухватиться, если Ваша программа интенсивно использует глобальные или статические переменные К счастью, сейчас наметилась тенденция отхода от применения таких переменных и перехода к автоматическим (размещаемым в стеке) переменным и передаче данных через параметры функций. И правильно, ведь расположенные в стеке переменные всегда связаны только с конкретным потоком
Стандартная библиотека С существует уже долгие годы — это и хорошо, и плохо. Ее переделывали под многие компиляторы, и ни один из них без нсс нс стоил бы ломаного гроша Программисты пользовались и будут пользоваться ею, а значит, прототипы и поведение функций вроде strtok останутся прежними, Но если бы эту библиотеку взялись перерабатывать сегодня, ее построили бы с учетом многопоточности и уж точно не стали бы применять глобальные и статические переменные.
В своих программах я стараюсь избегать глобальных переменных. Если же Вы используете глобальные и статические переменные, советую проанализировать каждую из них и подумать, нельзя ли заменить cc переменной, размещаемой в стеке Усилия окупятся сторицей, когда Вы решите создать в программе дополнительные потоки; впрочем, и однопоточное приложение лишь выиграет от этого.
Хотя два вида TLS-памяти, рассматриваемые в этой главе, применимы как в приложениях, так и в DLL, они все же полезнее при разработке DLL, поскольку именно в
этом случае Вам нс известна структура программы, с которой они будут связаны. Если же Вы пишете приложение, то обычно знаете, сколько потоков оно сопдаст для чего. Поэтому здесь еще можно как-то вывернуться. Но разработчик DLL ничего этого не знает. Чтобы помочь ему, и был создан механизм локальной памяти потока. Однако сведения, изложенные в этой главе, пригодятся и разработчику приложений.
Динамическая локальная память потока
Приложение работает с динамической локальной памятью потока, оперируя набором из четырех функций. Правда, чаще с ними работают DLL-, а пе ЕХЕ-модули. На рис. 21-1 показаны внутренние структуры данных, используемые для управления TLS в Windows.
Рис. 21 -1. Внутренние структуры данных, предназначенные для управления локальной памятью потока
Каждый флаг выполняемого в системе процесса может находиться в состоянии FREE или INUSE, указывая, свободна или занята данная область локальной памяти потока (TLS-область). Microsoft гарантируетдоступность по крайней мере TLS_MINIMUM_AVAILABLE битовых флагов. Идентификатор TLS_MINIMUM_AVAILABLE определен в файле WinNT.h как 64. Но в Windows 2000 этот флаговый массив вмещает свыше 1000 элементов! Этого более чем достаточно для любого приложения.
Чтобы воспользоваться динамической TLS, вызовите сначала функцию TlsAlloc,
DWORD TlsAlloc();
Она заставляет систему сканировать битовые флаги в текущем процессе и искать флаг FREE. Отыскав, система меняет его на INUSE, a TlsAlloc возвращает индекс флага в битовом массиве. DLL (или приложение) обычно сохраняет этот индекс в глобальной переменной. Не найдя в списке флаг FREE,TlsAlloc возвращает код TLS_OUT_ OF_INDEXES (определенный в файле WinBase.h как 0xFFFFFFFF).
Когда TlsAlloc вызывается впервые, система узнает, что первый флаг — FREE, и немедленно меняет его на INUSE, aTlsAlloc возвращает 0. Вот 99 процентов того, что делаетTlsAlloc. Об оставшемся одном проценте мы поговорим позже
Создавая поток, система создает и массив из TT.S_MINIMUM_AVAILABLE элементов — значений типа PVOID; она инициализирует его нулями и сопоставляет с потоком Таким массивом (элементы которого могут принимать любые значения) располагает каждый поток (рис 21-1).
Прежде чем сохранить что-то в PVOID-массиве потока, выясните, какой индекс в нем доступен, — этой цели и служит предварительный вызов TlsAlloc. Фактически она резервирует какой-то элемент этого массива Скажем, если возвращено значение Л, то в Вашем распоряжении третий элемент PVOID-массива в каждом потоке данного процесса — не только в выполняемых сейчас, но и в тех, которые могут быть созданы в будущем.
Чтобы занести в массив потока значение, вызовите функцию TlsSetValue:
BOOL TlsSetValue( DWORD dwllsIndex, PVOID pvTlsValue);
Она помещает в элемент массива, индекс которого определяется параметром dwTlsIndex значение типа PVOID, содержащееся в параметреpvTlsValue. СодержимоеpvTlsValue сопоставляется с потоком, вызвавшимTlsSetValue В случае успеха возвращается TRUE.
Обращаясь к TlsSetValue, поток изменяет только свой PVOID-массив. Он не может что-то изменить в локальной памяти другого потрка. Лично мне хотелось бы видеть какую-нибудь TLS-функцию, которая позволила бы одному потоку записывать данные в массив другого потока, но такой нет. Сейчас единственный способ пересылки каких-либо данных от одного потока другому — передать единственное значение черезCreateThread или_begintbreadex Т.е. в свою очередь передают это значение функции потока
Вызывая TlsSetValue, будьте осторожны и передавайте только тот индекс, который получен предыдущим вызовомTlsAlloc Чтобы максимально увеличить быстродействие этих функций, Microsoft отказалась от контроля ошибок. Если Вы передадите индекс, не зарезервированный ранееTlsAlloc, система все равно запишет в соответствующий элемент массива значение, и тогда ждите неприятностей
Для чтения значений из массива потока служит функция TlsGetValue
PVOTD TlsGetValue(DWORD dwTlsIndex);
Она возвращает значение, сопоставленное с TLS-областью под индексом dwTlsIndex. Как иTlsSetValue, функцияTteGetValue обращается только к массиву, который принадлежит вызывающему потоку. Она тоже не контролирует допустимость передаваемого индекса
Когда необходимость в TLS-области у всех потоков в процессе отпадет, вызовите TlsFree
BOOL TlsFree(DWORD dwTlsIndex);
Этя функция просто сообщит системе, что данная область больше не нужна. Флаг INUSE, управляемый массивом битовых флагов процесса, установится как FREE, и в будущем, когда поток еще раз вызовет TlsAlloc, этот участок памяти окажется вновь доступен.TlsFree возвращает TRUE, если вызов успешен Попытка освобождения невыделенной TLS-области даст ошибку.
Использование динамической TLS
Обычно, когда в DLL применяется механизм TLS-памяти, вызов DllMain со значением DLL_PROCESS_ATTACH заставляет DLL обратиться кTlsAlloc, а вызовDlIMain со значением DLL_PROCESS_DETACH — кTlsFree ВызовыTlsSetVafae иTlsGetValue чаще всего происходят при обращении к функциям, содержащимся в DLL
Вот один из способов работы с TLS-памятью: Вы создаете ее только по необходимости. Например, в DLL может быть функция, работающая аналогично strtok При первом ее вызове поток передает этой функции указатель на 40-байтовую структуру, которую надо сохранить, чтобы ссылаться на нее при последующих вызовах. Поэтому Вы пишете свою функцию, скажем, так
DWORD g_dwTlsIndex; // считаем, что эта переменная инициализируется // в результате вызова функции TlsAlloc
void MyFunction(PSOMFSTRUCT pSomeStruct) {
if (pSomeStruct != NULL) {
// вызывающий погок передает в функцию какие-то данные
// проверяем, не выделена ли уже область для хранения этих данных
if (TLsGetValue(g_dwTlsIndex) == NULL) {
// еще не выделена, функция вызывается этим потоком впервые TlsSetValue(g_dwTlsIndex, HeapAlloc(GetProcessHeap(), 0, sizeof(*pSomeStruct));
}
// память уже выделена, сохраняем только что переданные значения memcpy(TlsGetValue(g_dwTlsIndex), pSomeStruct, sizeof(*pSomeStruct));
}
else {
// вызывающий код уже передал функции данные; // теперь что-то делаем с ними
// получаем адрес записанных данных pSomeStruct = (PSOMESTRUCT) TlsGetValue(g_dwTlsIndex);
// на эти данные указывает pSomeStruct; используем ее
}
}
Если поток приложения никогда не вызовет MyFunction, то и блок памяти никогда не будет выделен.
Если Вам показалось, что 64 TLS-области — слишком много, напомню, приложение может динамически подключать несколько DLL. Одна DLL займет, допустим, 10 TLS-индсксов, вторая — 5 и т д. Так что это вовсе не много — напротив, стремитесь к тому, чтобы DLL использовала минимальное число TLS-индексов И для этого лучше всего применять метод, показанный на примере функции MyFunction. Конечно, я могу сохранить 40-байтовую структуру в 10 TLS-индексах, но тогда не только будет попусту расходоваться TLS-массив, но и затруднится работа с данными Гораздо эффективнее выделить отдельный блок памяти для данных, сохранив указатель на него в одном TLS-индексе, — именно так и делается вMyFunction. Как я уже упомянул, в Windows 2000 количество TLS-областей увеличено до более чем 1000. Microsoft пошла на
это из-за того, что многие разработчики слишком бесцеремонно использовали TLSобласти и их не хватало другим DLL.
Теперь вернемсн к гому единственному проценту, о котором я обещал рассказать, рассматривая TlsAlloc Взгляните на фрагмент кода
DWORD dwTlsIntlex; PVOID pvSomeValue;
...
dwTlslndex = TlsAlloc();
TlsSetValue(dwTlsIndex, (PVOID) 12345);
TlsFree(dwTlsIndex);
// допустим, значение dwTlsIndex, возвращенное после этого вызова TlaAlloc, // идентично индексу, полученному при предыдущем вызове TlsAlloc dwTlsIndex = TlsAlloc();
pvSomeValue = TlsGetValue(dwTlsIndex);
Как Вы думаете, что содержится в pvSomeValue после выполнения этою кода? 12345? Нет — нуль. Прежде чем вернуть управление,TlsAttoc "проходит" по всем потокам в процессе и заносит 0 по только что выделенному индексу в массив каждого потока И прекрасно1Ведь не исключено, что приложение вызоветLoadLibrary, чтобы загрузить DLL, а последняя —TlsAlloc, чтобы зарезервировать какой-то индекс. Далее поток может обратиться кFreeLibrary и удалить DLL Последняя должна освободить выделенный ей индекс, вызвавTlsFree, по кто знает, какие значения код DLL занес в тот или иной TLS-массив? В следующее мгновение поток вновь вызываетLoadLibrary и загружает другую DLL, которая тоже обращается кTteAlloc и получает тот же индекс, что и предыдущая DI.T, И если быTlsAlloc не делала того, о чем я упомянул в самом начале, лоток мог бы получить старое значение элемента, и программа стала бы работдть некорректно.
Допустим, DLL, загруженная второй, решила проверить, выделена ли какому-то потоку локальная память, и вызвала TlsGetValue, как в предыдущем фрагменте кода. Если быTlsAlloc не очищала соответствующий элемент в массиве каждого потока, то в этих элементах оставались бы старые данные от первой DLL И тогда было бы вот что. Поток обращается кMyFunction, а та — в полной уверенности, что блок памяти уже выделен, — вызываетmemcpy и таким образом копирует новые данные в ту область, которая, как ей кажется, и является выделенным блоком. Результат мог бы быть катастрофическим К счастыо,TlsAlloc инициализирует элементы массива, и такое просто немыслимо.
Статическая локальная память потока
Статическая локальная память потока основана на той же концепции, что и динамическая, — она предназначена для того, чтобы с потоком можно было сопоставить те или иные данные Однако статическую TLS использовать гораздо проще, так как при этом не нужно обращаться к каким-либо функциям.
Возьмем такой пример. Вы хотите сопоставлять стартовое время с каждым потоком, создаваемым программой В этом случае нужно лишь объявить переменную для
хранения стартового времени:
__declspec(thread) DWORD gt_dwStartTime = 0;
Префикс _dectepec(thread) — модификатор, поддерживаемый компилятором Microsoft Visual C++. Он сообщает компилятору, что соответствующую переменную следует поместить в отдельный раздел EXE- или DLL-файла. Переменная, указываемая за __dectepec(thread), должна быть либо глобальной, либо статической внутри (или вне) функции. Локальпую переменную с модификатором __declspec(thread) объявить нельзя. Но это не должно Вас беспокоигь, ведь локальные переменные и тяк связаны с конкретным потоком. Кстати, глобальные TLS-переменные я помечаю префиксомgt_, а статические —sf_.
Обрабатывая программу, компилятор выносит все TLS-переменные в отдельный раздел, и Вы вряд ли удивитесь, что этому разделу присваивается имя .tls. Компоновщик объединяет эти разделы из разных объектных модулей и создаст в итоге один большой раздел .tls, помещаемый в конечный EXE- или DLL-файл.
Работа статической TLS строится на тесном взаимодействии с операционной системой Загружая приложение в память, система отыскивает в ЕХЕ-файле раздел .tls и динамически выделяет блок памяти для хранения всех статических TLS-переменных Всякий раз, когда Ваша программа ссылается на одну из таких переменных, ссылка переадресуется к участку, расположенному в выделенном блоке памяти. В итоге компилятору приходится генерировать дополнительный код для ссылок на статические TLS-переменные, что увеличивает размер приложения и замедляет скорость его работы В частности, на процессорах x86 каждая ссылка на статическую TLS-переменную заставляет генерировать три дополнительные машинные команды
Если в процессе создается другой поток, система выделяет еще один блок памяти для хранения статических переменных нового потока Только что созданный поток имеет доступ лишь к своим статическим TLS-переменным, и не может обратиться к TLS-переменным любого другого потока.
Вот так в общих чертах и работает статическая TLS-память. Теперь посмотрим, что происходит при участии DLL Ведь скорее всего Ваша программа, использующая статические TLS-персменные, связывается с какой-нибудь DLL, в которой тоже применяются переменные этого типа. Загружая такую программу, система сначала определяет объем ее раздела .rts, а затем добавляет эту величину к сумме размеров всех разделов .tls, содержащихся в DLL, которые связаны с Вашей программой При создании потоков система автоматически выделяет блок памяти, достаточно большой, чтобы в нем уместились все TLS-переменные, необходимые как приложению, так и неявно связываемым с ней DLL. Все так хорошо, что даже не верится'
И не верьте! Подумайте, что будет, если приложение вызовет LoadLibrary и подключит DLL, тоже содержащую статические TLS-переменные. Системе придется проверить потоки, уже существующие в процессе, и увеличить их блоки TLS-памяти, чтобы подогнать эти блоки под дополнительные требования, предъявляемые новой DLL Ну а если Вы вызоветеFreeLibrary для выгрузки DLL со статическими TLS-переменными, системе придется ужать блоки памяти, сопоставленные с потоками в данном процессе.
Это слишком большая нагрузка на операционную систему. Кроме того, допуская явную загрузку DLL, содержащих статические TLS-перемснные, система не в состоянии должным образом инициализировать TLS-данные, что при попытке обращения к ним может вызвать нарушение доступа. Это, пожалуй, единственный недостаток статической TLS; при использовании динамической TLS такой проблемы нет. DLL, работающие с динамической TLS, могут загружаться и выгружаться из выполняемой программы в любой момент и без всяких проблем.