Г Л А В А 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, 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-области даст ошибку.
Обычно, когда в DLL применяется механизм TLS-памяти, вызов DllMain со значением DLL_PROCESS_ATTACH заставляет DLL обратиться к TlsAlloc, а вызов DlIMain со значением DLL_PROCESS_DETACH — к TlsFree Вызовы TlsSetVafae и TlsGetValue чаще всего происходят при обращении к функциям, содержащимся в DLL
Вот один из способов работы с TLS-памятью: Вы создаете ее только по необходимости. Например, в DLL может быть функция, работающая аналогично strtok При первом ее вызове поток передает этой функции указатель на 40-байтовую структуру, которую надо сохранить, чтобы ссылаться на нее при последующих вызовах. Поэтому Вы пишете свою функцию, скажем, так
DWORD g_dwTlsIndex;
// считаем, что эта переменная инициализируется
// в результате вызова функции TlsAllocvoid 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, могут загружаться и выгружаться из выполняемой программы в любой момент и без всяких проблем.