Создание эффективных WIN32-приложений с учетом специфики 64-разрядной версии Windows

         

Динамическая локальная память потока


Приложение работает с динамической локальной памятью потока, оперируя набором из четырех функций. Правда, чаще с ними работают 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, a TlsAlloc возвращает 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, могут загружаться и выгружаться из выполняемой программы в любой момент и без всяких проблем.