    ЛОКАЛЬНАЯ ПАМЯТЬ ПОТОКА 

    глава 13 

    Иногда данные удобно связывать с экземпляром какого-либо объекта. 
Например, чтобы сопоставить какие-то дополнительные данные с окном, применяют 
функции SetWindow~\'\rordH'~5etWmdowLong. Локальная память потока 
(thread-local storage, TLS) позволяет связать данные и с определенным потоком 
(скажем, сопоставить с ним время его создания) и — по завершении этого потока 
— узнать время его жизни. 
    TLS используется и в стандартной библиотеке С. Но ее разработали задолго 
до появления многопоточных приложений, так что большая часть содержащихся в 
ней функций рассчитана на однопоточные программы. Наглядный пример — функция 
strtok. При первом вызове она получает адрес строки и запоминает его в 
собственной статической переменной. Когда при следующих вызовах strtok Вы 
передаете ей NULL, она оперирует с адресом, записанным в своей переменной. 
    В многопоточной среде вероятна такая ситуация: один поток вызывает strtok, 
и, не успел он вызвать ее повторно, как к ней уже обращается другой. Тогда 
второй поток заставит функцию занести в статическую переменную новый адрес, 
неизвестный первому. И в дальнейшем первый поток, вызывая strtok, будет 
использовать строку, принадлежащую второму. Вот Вам и «жучок», найти который 
очень трудно. 
    Чтобы устранить эту проблему, в стандартной библиотеке С теперь 
применяется механизм локальной памяти потока: за каждым потоком закрепляется 
свой строковый указатель, зарезервированный для strtok. Аналогичный механизм 
действует и для функций asctime и gmtime стандартной библиотеки. 
    Локальная память потока может стать той соломинкой, за которую придется 
ухватиться, если Ваша программа интенсивно использует глобальные или 
статические переменные. К счастью, сейчас наметилась тенденция отхода от 
применения таких переменных и перехода ^"автоматическим (размещаемым в стеке) 
переменным и передаче данных через параметры функций. И правильно: ведь 
расположенные в стеке переменные всегда связаны только с конкретным потоком. 
    Стандартная библиотека С существует уже долгие годы — это и хорошо, и 
плохо. Ее переделывали под многие компиляторы, и ни один из них без нее не 
стоил бы ломаного гроша. Программисты использовали и будут использовать ее, а 
значит, прототипы и поведение функций вроде strtok останутся прежними. Но если 
бы эту библиотеку взялись перерабатывать сегодня, ее построили бы с учетом 
многопоточности и уж точно не стали бы применять глобальных и статических переменных. 
    '~В~свошГпрограммах я стараюсь избегать глобальных переменных. Если же Вы 
применяете глобальные и статические переменные, советую проанализировать 
каждую из них и подумать, нельзя ли заменить ее переменной, размещаемой в 
стеке. Усилия окупятся стори

    395 

    396 _____________________________Windows для профессионалов 

    цей, когда Вы решите создать в программе дополнительные потоки; впрочем, и 
однопоточное приложение от этого лишь выиграет. 
    Хотя два вида TLS-памяти, рассматриваемые в этой главе, применимы как в 
приложениях, так и в DLL, они все же полезнее при разработке DLL, так как 
именно в этом случае Вам неизвестна структура программы, к которой они будут 
подключены. Если же Вы пишете приложение, то обычно знаете, сколько потоков 
оно создаст и для чего. Поэтому здесь еще можно как-то вывернуться. Но 
разработчик DLL ничего этого не знает. Чтобы помочь ему, и был создан механизм 
локальной памяти потока. Однако сведения, изложенные в этой главе, пригодятся 
и разработчику приложений. 

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

    Приложение работает с динамической локальной памятью потока, оперируя 
набором из 4 функций. Правда, чаще с ними работают DLL-модули, а не ЕХЕ-программы. 
    На рис. 13-1 показаны внутренние структуры данных, используемые для 
управления локальной памятью в Microsoft Windows 95 и Microsoft Windows NT. 

    I—————————————————————————— ПРОЦЕСС ——————————————————————————I 
    Битовые флаги TLS от О до (TLS_MINIMUM_AVAILABLE - 1) 
    I I I I I I U---————I 
    ' л i———————————— Поток 1 —————————————, i———————————— Поток 1 i^-————————————, 
    /V 
    I'rft ,г; Индекс О                                  О    Индекс О 
    ;";©,: Индекс 1                                  0    Индекс 1 
    lllslltSj Индекс 2                                  0    Индекс 2 
    О    Индекс 3                                  0    Индекс 3 
    yji.S';-:;?! Индекс 4                                  0    Индекс 4 
    аИндексТ1.3_М1М1МиМ_АУА11.АВ1-Е-2    ~0| Индекс TLS_MINIMUM_A VAILABLE - 2 
    Индекс TLS_MtNlMUM_AVA4A6LE - 1       0 | Индекс TLS_MINIMUM_AVA1LABLE - 1 

    Рис. 13-1. Внутренние структуры данных, предназначенные для управления 
локальной памятью потока 

    Каждый флаг выполняемого в системе процесса может находиться в состоянии 
FREE или INUSE, тем самым указывая, свободна или занята данная область 
локальной памяти потока (TLS-область). Microsoft гарантирует, что по крайней 
мере TLS_MINIMUM_A VAILABLE битовых флагов будет доступно на всех 
Win32-плaтфopмax. (Кстати, идентификатор TLS_MINIMUM_A VAILABLE определен в 
файле WINNT.H как 64.) На некоторых платформах система способна расширять этот 
флаговый массив в соответствии с потребностями приложения или DLL. 
    Чтобы воспользоваться динамической TLS-памятью, вызовите сначала функцию 
TbAlloc. DWORD TlsAlloc(VOID); 

    _________________________Глава 13. Локальная память потока 397 

    Она заставляет систему сканировать битовые флаги в текущем процессе и 
искать флаг FREE. Отыскав, система меняет его на INUSE, a TIsAlloc возвращает 
индекс флага в битовом массиве. DLL (или приложение) обычно сохраняют этот 
индекс в глобальной переменной'. Не обнаружив в списке флага FREE, TIsAlloc 
возвращает код TLS_OUT_OF_INDEXES (определенный в файле WINBASE.H как OxFFFFFFFF). 
    Когда TIsAlloc вызывается впервые, система узнает, что первый флаг — FREE, 
и немедленно меняет его на INUSE, a TIsAlloc возвращает 0. Вот 99% того, что 
делает TIsAlloc. Об оставшемся 1% мы поговорим позже. 
    Создавая поток, система создает и массив из TLS_MINIMUM_AVAILABLE 
элементов — 32-битных значений типа EPVOID; она инициализирует его нулями и 
сопоставляет с потоком. Таким массивом (элементы которого могут принимать 
любые 32-битные значения) располагает каждый поток (см. рис. 13-1). 
    Прежде чем сохранить что-то в LPVOID-массиве потока, выясните, какой 
индекс в нем доступен, — этой цели и служит предварительный вызов TIsAlloc. 
Фактически она резервирует какой-то элемент этого массива. Скажем, если 
возвращено 3, то в Вашем распоряжении третий элемент LPVOID-массива в каждом 
потоке данного процесса — не только в тех, что исполняются сейчас, но и в тех, 
что могут быть созданы в будущем. 
    Чтобы занести в массив потока значение, вызовите функцию TIsSetValue: 

    BOOL TlsSetValue(DWORD dwTlsIndex, LPVOID IpvTIsValue): 

    Она помещает в элемент массива, индекс которого определяется параметром 
dwTlsIndex, значение типа LPVOID (или любое другое 32-битное значение), 
содержащееся в параметре IpvTIsValue. Содержимое IpvTIsValue сопоставляется с 
потоком, вызвавшим TIsSetValue. В случае успеха возвращается TRUE. 
    Обращаясь к TIsSetValue, поток изменяет только свой LPVOID-массив. Он не 
может что-то изменить в локальной памяти другого потока. Лично мне хотелось бы 
видеть какуюнибудь TLS-функцию, которая позволила бы одному потоку записывать 
данные в массив другого потока, но такой нет. Сейчас единственный способ 
пересылки каких-либо данных от одного потока другому — передать 32-битное 
значение через CreateThread или _beginthreadex~'Te в свою очередь передают это 
значение функции потока. 
    Вызывая TIsSetValue, будьте очень осторожны и передавайте только тот 
индекс, что получен предыдущим вызовом TIsAlloc. Чтобы максимально увеличить 
быстродействие этих функций, Microsoft отказалась от контроля ошибок. Если Вы 
передадите индекс, не зарезервированный ранее TIsAlloc, система все равно 
запишет в соответствующий элемент массива 32-битное значение, и тогда ждите неприятностей. 
    Для чтения значений из массива потока служит функция TIsGetValue: 

    LPVOID TlsGetValue(DWORD dwTlsIndex); 

    Она возвращает значение, сопоставленное с TLS-областью под индексом 
dwTlsIndex. Как и TIsSetValue, функция TIsGetValue обращается только к 
массиву, который принадлежит вызывающему потоку. Она тоже не контролирует 
допустимость передаваемого индекса. 
    Когда необходимость в TLS-области у всех потоков в процессе отпадет, 
вызовите TIsFrep. 

    BOOL TlsFree(DWORD dwTlsIndex); 

    Эта функция просто сообщит системе, что данная область больше не нужна. 
Флаг INUSE, управляемый массивом битовых флагов процесса, установится как 
FREE, и в будущем, когда поток еще раз вызовет TIsAlloc, этот участок памяти 
окажется вновь доступен. TIsFree возвращает TRUE, если вызов успешен. Попытка 
освобождения невыделенной TLS-области даст ошибку. 
    1. Это один из тех случаев, когда глобальная переменная — действительно 
лучший выбор, поскольку ее значение связано с процессом в целом и не зависит 
от отдельного его потока. 

    398 _____________________________Windows для профессионалов 


    Использование динамической локальной памяти потока 

    Обычно, когда в DLL применяется механизм TLS-памяти, вызов DllMain со 
значением DLL_PROCESS_ATTACH заставляет DLL обратиться к TIsAlloc, а со 
значением DLL_PROCESS_DETACH — к TIsFree. Вызовы TIsSetValue и TIsGetValue 
чаще всего происходят при обращении к функциям, содержащимся в DLL. 
    Вот один из способов работы с TLS-памятью: Вы создаете ее только по 
необходимости. Например, в DLL может быть функция, работающая аналогично 
strtok. При первом ее вызове поток передает этой функции указатель на 
40-байтовую структуру, которую надо сохранить, чтобы ссылаться на нее при 
последующих вызовах. Поэтому Вы пишете свою функцию, скажем, так: 

    DWORD g_dwTlsIndex; // допустим, что эта переменная инициализируется 
    // в результате вызова функции TIsAlloc 
    void MyFunction(LPSOMESTRUCT IpSomeStruct) { if (IpSomeStruct != NULL) { 
    // вызывающий поток передает в функцию какие-то данные 
    // проверяем, не выделена ли уже область для хранения этих данных 
    if (TlsGetVaiue(g_dwTlsIndex) == NULL) { 
    // еще не выделена; функция вызывается этим потоком впервые 
    TlsSetValue(g_dwTlsIndex, HeapAlloc(GetProcessHeap(), 0, 
    sizeof(*lpSomeStruct)); 
    } 
    // память уже выделена; сохраняем только что переданные значения 
    memcpy(TlsGetValue(g_dwTlsIndex), IpSomeStruct, sizeof(«IpSomeStruct)); 
    } else { 
    // вызывающий код уже передал функции данные; 
    '// теперь делаем с ними что-то 
    // получаем адрес записанных данных 
    IpSomeStruct = (LPSOMESTRUCT) TlsGetValue(g_dwTlsIndex); 
    // на эти данные указывает IpSomeStruct; используем ее 
    } 
    } 

    Если поток приложения никогда не вызовет MyFunction, то и блок памяти 
никогда не будет выделен. 
    Если Вам показалось, что 64 (как минимум) TLS-участка — слишком много, 
напомню: приложение может динамически подключать несколько DLL. Одна займет, 
допустим, 10 TLS-индексов, вторая — 5 и т. д. Так что это вовсе не много — 
напротив, стремитесь к тому, чтобы DLL использовала минимальное число 
TLS-индексов. И для этого лучше всего применять метод, показанный на примере 
функции MyFunction. Конечно, я могу сохранить 40-байтовую структуру в 10 
TLS-индексах, но тогда не только попусту будет 

    _________________________Глава 13. Локальная память потока 399 

    расходоваться TLS-массив, но и затруднится работа с данными. Гораздо 
эффективнее выделить отдельный блок памяти для данных, сохранив указатель на 
него в одном TLSиндексе, — именно так и делается в MyFunction. 
    Теперь вернемся к тому единственному проценту, о котором я обещал 
рассказать, рассматривая TIsAlloc. Взгляните на фрагмент: 

    DWORD dwTlsIndex; 
    LPVOID IpvSomeValue; 
    dwTlsIndex = TIsAllocO; 
    TlsSetValue(dwTlsIndex, (LPVOID) 12345); 
    TIsFree(dwTlsIndex); 
    // Допустим, значение dwTlsIndex, возвращенное после этого вызова TIsAlloc, 
    // идентично индексу, полученному при предыдущем вызове TIsAlloc 
    dwTlsIndex = TlsAlloc(); 
    IpvSomeValue = TlsSetValue(dwTlsIndex); 

    Как Вы думаете, что содержится в IpvSomeValue после выполнения этого кода? 
12345? Нет —0. Прежде чем вернуть управление, TIsAlloc «проходит» по всем 
потокам в процессе и заносит 0 по только что выделенному индексу в массив 
каждого потока. И прекрасно! Ведь не исключено, что приложение вызовет 
LoadLibrary, чтобы загрузить DLL, та — TIsAlloc, чтобы зарезервировать 
какой-то индекс. Далее поток может обратиться к FreeLibrary и удалить PLL. 
Последняя должна освободить выделенный ей индекс, вызвав TIsFree, но кто 
знает, какие значения код DLL занес в тот или иной TLS-массив? В следующее 
мгновение поток вновь вызывает LoadLibrary и загружает другую DLL, которая 
тоже обращается к TIsAlloc и получает тот же индекс, что и предыдущая DLL. И 
если бы TIsAlhc не делала того, о чем мы упомянули в самом начале, поток мог 
бы «взять» старое значение элемента, и программа стала бы работать некорректно. 
    Допустим, DLL, загруженная второй, «пожелала» проверить, выделена ли 
какому-то потоку локальная память, и вызвала TIsGetValue, как в предыдущем 
фрагменте кода. Если бы TIsAlloc не очищала соответствующий элемент в массиве 
каждого потока, то в этих элементах оставались бы старые данные от первой DLL. 
И тогда было бы вот что. Поток обращается к MyFunction, а та — в полной 
уверенности, что блок памяти уже выделен, — вызывает тетсру и таким образом 
копирует новые данные в ту область, что, «по мнению» MyFunction, и является 
выделенным блоком. Результат может быть катастрофическим. К счастью, TIsAlloc 
инициализирует элементы массива, и такое просто немыслимо. 

    Программа-пример TLSDyn 

    Программа TLSDyn (TLSDYN. EXE) (см. листинг на рис. 13-2) демонстрирует, 
как использовать преимущества динамической локальной памяти потока. Программа 
неявно связывается с SOMELIB.DLL (см. листинг на рис. 13-3). Последняя, 
получив уведомление DLL_PROCESS_ATTACH, резервирует единственный TLS-индекс, а 
получив уведомление DLL_PROCESS_DETACH, освобождает этот индекс. Файлы 
исходного кода и ресурсов для TLSDyn и SOMELIB.DLL находятся в каталоге TLSDYN 
на компакт-диске, прилагаемом к книге. 
    SOMELIB.DLL содержит функцию LoadResString. В момент вызова она (через 
TIsGetValue) проверяет, не обращался ли уже к ней данный поток: если 
возвращается NULL, зна

    400 _____________________________Windows для профессионалов 

    чит, поток вызывает LoadResString впервые. И тогда LoadResString выделяет 
блок памяти из кучи процесса, сохраняя его адрес в TLS-области вызывающего потока. 
    Но независимо от того, по какому разу данный поток обращается к 
LoadResString, в нашем распоряжении адрес блока памяти, выделенного потоку из 
кучи процесса. Далее LoadResString вызывает LoadString для загрузки в этот 
блок памяти строки из принадлежащей DLL таблицы строк. Теперь строка связана с 
вызывающим потоком. 
    И, наконец, LoadResString увеличивает внутренний счетчик (статическую 
переменную nStringId) и возвращает адрес загруженного ресурса — строки. Всякий 
раз, вызывая LoadResString, поток считывает из таблицы DLL какую-нибудь строку. 
    А сейчас посмотрим, как работает программа TLSDyn. После ее запуска на 
экране появляется окно: 

    шшааашЕЕЯм 
    String *0 
    [Ю 

    Начиная выполнение, функция WinMain программы вызывает LoadResString, 
которая загружает из принадлежащей DLL таблицы строк String #0 и сопоставляет 
данную строку с первичным потоком процесса. Тот в свою очередь вызывает 
MessageBox — показать, что операция выполнена успешно. 
    После щелчка кнопки ОК первичный поток создаст диалоговое окно и 5 
потоков. Каждый из них вызовет LoadResString и тоже получит по одной строке. 
Далее все они Выполнят цикл (из 4 итераций). На каждой итерации каждый поток 
создаст строку, содержащую номер потока и связанную с ним строку String #. 
Сформированная таким образом строка выводится в окно списка. Когда все потоки 
закончат свои циклы, диалоговое окно станет таким: 

    Execution log: 
    Thread iH: String #13 
    ! Thread »2: String #2 
    e Thread *3: String f3                    •!! t 
    , Thread #4: String #4                     ; i 
    I Thread »5: String »5                    -Is'•! 
    A Thread*): String #1                     ?1";' 
    ^Thread»!: String f\                     I'::-
    (: Thread tfZ: String »2                    ?; ,| 
    г Thread»!: String »1                     I I 
    1 Thread »3: String f3                    S ?1 
    Й Thread #4: String »4                    ;S|; 
    г Thread «2: String «2                      ':' 
    si Thread »5: String »5                      ? 
    I Thread f3: String f3                    ;1 ? 
    :'! Thread #2: String S2                     ;? I 
    Thread M: String »4                   1 ' 
    Thread #3: String Я                   ;'! 
    ;:; Thread »5: String #5                   J 
    Я Thread #4: String »4 
    iJThreadNl^tnna«5      .т.^м^г- -51 •• 

    На Вашем компьютере порядок строк может быть другим. Вы же просто 
отметьте, что LoadResString закрепляет за каждым потоком свою строку. Обратите 
внимание и на то, что если какой-то поток в программе никогда не обращается к 
этой функции, то и память ему из кучи не выделяется. Можно было бы выделять 
память в DllMain — в момент, когда программа получает уведомления 
DLL_PROCESS_ATTACH или DLL_PROCESS_DETACH, но это означало бы, что мы выделяем 
память из кучи сразу для всех потоков, созданных в 

    _________________________Глава 13. Локальная память потока 401 

    процессе, даже если ни один из них не обращался к LoadResString. Так что 
метод, используемый в SOMELIB.DLL, несколько эффективнее. По завершении 
программы на экране появляется другое окно: 
    Это окно — результат следующего вызова LoadResString из WinMain. Функция 
LoadResString связывает с первичным потоком новую строку, замещая исходную 
String #0. 
    Посмотрите, как DllMain (из SOMELIB.DLL) проводит за собой очистку, 
получив уведомление DLL_THREAD_DETACH или DLL_PROCESS_DETACH. В обоих случаях 
DLL проверяет, была ли выделена память для потока из кучи, и, если да, 
освобождает ее. Хотя память освободилась бы и автоматически по завершении 
программы TLSDyn, лучше сделать это самому. Но прежде обязательно проверьте: а 
выделена ли она (так, как это делает DllMain). Обратите внимание и на то, как 
вызывается TIsFree (когда DllMain уведомляется об отключении ее DLL от процесса). 
    Теперь допустим, что программа уже выполняется, и тут один из ее потоков 
вызывает LoadLibrary, подключая SOMELIB.DLL. DllMain этой библиотеки 
резервирует TLS-индекс, уникальный для всех потоков в процессе — как 
существующих, так и будущих. Пусть какие-то потоки обращаются к LoadResString, 
выделяя тем самым память, а какой-то еще поток вызывает FreeLibrary, чтобы 
отключить библиотеку SOMELIB.DLL. DllMain получает сообщение 
DLL_PROCESS_DETACH и освобождает блок памяти, связанный с вызывающим потоком. 
А как насчет других потоков? Они ведь тоже вызывали (или могли вызвать) 
LoadResString. Выделенная этими потоками память никогда не освободится, и мы 
столкнемся с серьезной проблемой утечки памяти. 
    Увы, решить эту проблему не так-то просто. Вашей DLL придется отслеживать 
все операции выделения с помощью какого-то массива; когда DLL получит 
уведомление DLL_PROCESS_DETACH, проверьте все указатели в этом массиве и 
вызовите — где нужно — HeapFree. 

    TLSDYN.C 
    /..„.****,....***.***.,*******.*.*.*****......*.**»*.,.....*.***„*.,.****.*., 
    Модуль: TLSDyn,С 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ...*.***.*..,,.,»*.*...,.****.....,**.*.*,.....*...*,..,.,***.,.*..*.,.******./ 
    ftinclude ".,\CmnHdr,H"           /* см. приложение В */ 
    ftinclude <windows.h> 
    ^include <windowsx.h> 
    ffinclude <tchar.h> 
    ftinclude <stdio.h>               // для sprintf 
    ftinclude <process.h>             // для _beginthreadex 
    ftinclude "..\SomeLib\SomeLib.H" 
    einclude "Resource.H" 

    см. след. стр. 

    Рис. 13-2. Листинг программы TLSDyn 

    402 ______________________________ Windows для профессионалов 


    РИС. 13-2  (продолжение) 

    1111111111111111111/1111111//1111111/11111/1111111111/1111111111111111111111111 
    HWND g_hwndLog = NULL; 
    /////////////////////////////////////////////
    DWORD WINAPI ThreadFunc (LPVOID IpvThreadParm) { 
    int nThreadNum = (int) IpvThreadParm; 
    int nNumCycles = 4: 
    TCHAR szBuf[100]; 
    LPCTSTR szString = LoadResString(); 
    while (nNumCycles--) { 
    _stprintf(szBuf, „TEXT( "Thread ff%d: %s"), nThreadNum, szString); 
    ListBox_Add8tring(g_hwndLog, szBuf); 
    Sleep(nThreadNum * 50); 
    \ i 
    return(O); 
    i 
    f 
    ////////////////////////////////////////////
    BOOL Dlg_0nlnit0ialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    int nThreadNuffi; 
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_TLSDYN, IDI_TLSDYN); 
    g_hwndLog = GetDlgItem(hwnd, IDC_LOG); 
    for (nThreadNum = 1; nThreadNum <= 5; nThreadNum++) { 
    HANDLE hThread; 
    DWORD dwIDThread; 
    hThread = chBEGINTHREADEX(NuLL, 0. ThreadFunc, 
    (LPVOID) nThreadNum, 0, &dwIDThread); 
    CloseHandle(hThread); 
    } 
    return(TRUE): 
    ) 
    /////////////////////////////////////////
    void Dlg_OnCommand (HWND hwnd, int id, HWND hwndCti, UINT codeNotify) { 
    switch (id) { 
    case IDCANCEL: 
    EndDialog(hwnd, id); 

    см. след. стр. 

    _________________________Глава 13. Локальная память потока 403 


    РИС. 13-2  (продолжение) 

    break; 
    \ 
    f 
    \ 
    /////////////////////////////////////
    BOOL CALLBACK Dlg_Proc (HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM 
IParam) { 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd. WM_INITOIALOG, Dlg_OnInitDialog); 
    chHANDLE_DLGMSG(hwnd. WM_COMMAND, Dlg_OnCommand); 
    } 
    return(FALSE); 
    i 
    i 
    /////////////////////////////////////////////////////////////////////////////// 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) < 
    ChWARNIFUNICODEUNDERlAlIN95(): 
    MessageBox(Nl)LL, LoadResString(), __TEXT( "String Before Dialog Box"), MBJ)K); 
    DialogBox(hinstExe, MAKEINTRESOURCE(IDD_TLSDYN), NULL, Dlg_Proc); 
    MessageBox(NULL, LoadResString(), __TEXT("String After Dialog Box"), MB_OK); 
    return(O); 
    } 
    ////////////////////////////////// конец файла I I I I i I I I I H t i I I 
    SOMELIB.C 
    Модуль: SomeLib.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    ((include ". .\CrnnH(3r. H"           /* см. приложение В */ 
    ((include <windows.h> 
    ((include "Resource.h" 
    ((define SOMELIBAPI __declspec(dllexport) 
    ((include "SomeLib.H" 

    см. след. стр. 

    Рис. 13-3. Листинг SomeLib.DLL 

    404 _____________________________Windows для профессионалов 


    РИС. 13-3  (продолжение) 

    ///////////////////////////////////////
    DWORD g_dwTls!ndex = TLS_OUT_OF_INDEXES; 
    HINSTANCE g_hinstDll = NULL; 
    // этот мьютекс нужен для разграничения доступа к статической 
    // переменной nStringId в функции LoadResString 
    HANDLE g_hMutex = NULL; 
    ////////////////////////////////////////////////'///////7///7//1'.//////7/7/////'/// 
    BOOL WINAPI DllMain (HINSTANCE hinstDll. DWORD fdwReason, LPVOID IpvReserved) { 
    LPTSTR IpszStr; 
    switch (fdwReason) { 
    case DLL_PROCESS_ATTACH: 
    7/XL подключается к адресному пространству 
    // текущего процесса 
    g_hinstDll = hinstDll; 
    
    // резервируем индекс в массиве локальной памяти потока 
    g_dwTls!ndex = TIsAllocO; 
    if (g_dwTlsIndex == TLS_OUT_OF_INDEXES) { 
    // TLS-индекс зарезервировать не удалось - пусть 
    // DLL сообщит о неудаче 
    return(FALSE); 
    } 
    g_hMutex = CreateMutex(NULL, FALSE, NULL): 
    if (g_hMutex == NULL) { 
    // создать мьютекс не удалось - пусть 
    // DLL сообщит о неудаче 
    return(FALSE); 
    i 
    break; 
    case DLL_THREAD_ATTACH: 
    // в процессе создается новый поток 
    break; 
    case DLL_THREAD_DETACH: 
    // поток завершается корректно 
    // убедимся, что TLS-индекс действительно был зарезервирован 
    if (g_dwTlsIndex != TLS_OUT_OF_INDEXES) { 

    см. слеч. стр. 

    _______________________Глава 13. Локальная память потока 405 


    РИС. 13-3  (продолжение) 

    II получаем указатель на выделенную память 
    IpszStr = TlsGetValue(g_dwTlsIndex); 
    // проверяем, выделялась ли когда-нибудь память 
    // для этого потока 
    if (IpszStr != NULL) { 
    HeapFree(GetProcessHeap(), 0, IpszStr); 
    } 
    / 
    break; 
    case DLL_PROCESS_DETACH: 
    // вызывающий процесс отключает DLL от своего 
    // адресного пространства 
    // убедимся, что TLS-индекс действительно был зарезервирован 
    if (g_dwTlsIndex != TLS_OUT_OF_INDEXES) < 
    // получаем указатель на выделенную область памяти 
    IpszStr = TlsGetValue(g_dwTlsIndex); 
    // проверяем, выделялась ли когда-нибудь память 
    // для этого потока 
    if (IpszStr != NULL) { 
    HeapFree(GetProcessHeap(). 0, IpszStr); 
    } 
    // освобождаем TLS-индекс 
    TlsFree(g_dwTlsIndex); 
    } 
    // убедимся, что мьютекс был создан 
    if (g_hMutex != NULL) { 
    CloseHandle(g_nMutex); 
    } 
    break; 
    } 
    return(TRUE); 
    I 
    /////////////////////////////////////////////////////////////////////////////// 
    ftdefine RESSTR_SIZE       (1000 * sizeof(TCHAR)) 
    SOMELIBAPI LPCTSTR LoadResString (void) { 
    static int nStringId = 0; 
    LPTSTR IpszStr = TlsGetValue(g_dwTlsIndex); 

    см. след. стр. 

    406 _____________________________ Windows для профессионалов 

    РИС. 13-3  (продолжение) 

    if (IpszStr == NULL) { 
    IpszStr = HeapAlloc(GetProcessHeap(), 0, RESSTR_SIZE); 
    TlsSetValue(g_dwTlsIndex, IpszStr); 
    } 
    WaitForSingleObject(g_hMutex, INFINITE); 
    LoadString(g_hinstDll, IDS_STRINGFIRST + nStringId, IpszStr, RESSTR_SIZE); 
    nStringId = (nStr-ingId + 1) % IDS_STRINGNUM: 
    ReleaseMutex(g_hMutex); 
    return(lpszStr); 
    i 
    i 
    ////////////////////////////////// конец файла I / / I I I I / / / I I///I 
    SOMELIB.H 
    /***.*..*...*******.*.*...**,*****...*.*,,*******^*»**,*...**.***********.... 
    Модуль: SomeLib.H 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    **,.***,******...*********,..,...,,***,*****..»».*.***,*******...,,..***..**.*/ 
    #if !defined(SOMELIBAPI) 
    ftdefine SOMELIBAPI __declspec(dllexport) 
    ffendif 
    // идентификаторы, используемые в таблице строк 
    ftdefine IDS_STRINGLAST (IDS_STRINGFIRST + 9) 
    ftdefine IDS_STRINGNUM (IDS_STRINGLAST - \ 
    IDS_STRINGFIRST + 1) 
    // функция, возвращающая адрес строки в памяти 
    SOMELIBAPI LPCTSTR LoadResString (void); 
    ////////////////////////


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

    Статическая локальная память потока основана на той же концепции, что и 
динамическая, — она предназначена для того, чтобы с потоком можно было 
сопоставить те или иные данные. Однако статическую TLS использовать гораздо 
проще, так как при этом не нужно обращаться к каким-либо функциям. 
    Возьмем такой пример: Вы хотите сопоставить стартовое время с каждым 
потоком, создаваемым программой. В этом случае нужно лишь объявить переменную 
для хранения стартового времени: 

    __declspec(thread) DWORD gt_dwStartTime = 0; 

    __________________________Глава 13. Локальная память потока 407 

    Префикс _declspec (thread) — новый модификатор, введенный в компилятор 
Microsoft Visual C++. Он сообщает компилятору, что соответствующую переменную 
следует поместить в отдельный раздел ЕХЕ- или DLL-файла. Переменная, 
указываемая за _declspec (thread), должна быть либо глобальной, либо 
статической внутри (или вне) функции. Локальную переменную с модификатором 
_declspec (thread) объявить нельзя. Но это не должно Вас беспокоить — ведь 
локальные переменные и так связаны с конкретным потоком. Кстати, глобальные 
TLS-переменные я помечаю префиксом gt_ , а статические — st_. 
    Обрабатывая программу, компилятор выносит все TLS-переменные в отдельный 
раздел, и Вы вряд ли удивитесь, что этому разделу присваивается имя .tis. 
Компоновщик объединяет .tls-разделы из разных объектных модулей и создает в 
итоге один большой раздел .tis, размещаемый в конечном ЕХЕ- или DLL-файле. 
    Работа статической TLS строится на тесном взаимодействии с операционной 
системой. Загружая приложение в память, система отыскивает в ЕХЕ-файле раздел 
.tis и динамически выделяет блок памяти для хранения всех статических 
TLS-переменных. Всякий раз, как Ваша программа ссылается на одну из таких 
переменных, ссылка переадресуется к участку, расположенному в выделенном блоке 
памяти. В итоге компилятору приходится генерировать дополнительный код для 
ссылок на статические TLS-переменные, что увеличивает размер приложения и 
замедляет скорость его работы. В частности, на процессорах х86 каждая ссылка 
на статическую TLS-переменную заставляет генерировать 3 дополнительных 
машинных команды. 
    Если в процессе создается другой поток, система выделяет еще один блок 
памяти для хранения статических переменных нового потока. Только что созданный 
поток имеет доступ лишь к своим статическим TLS-переменным и не может 
обратиться к TLS-переменным любого другого потока. 
    Вот так в общих чертах и работает TLS-память. Теперь посмотрим, что 
происходит при участии DLL. Ведь скорее всего Ваша программа, использующая 
статические TLS-переменные, компонуется с какой-нибудь DLL, в которой тоже 
применяются переменные этого типа. Загрузив такую программу, система сначала 
определит объем ее раздела .tis, а затем добавит эту величину к сумме размеров 
всех .tls-разделов, содержащихся в DLL, подключенных к Вашей программе. При 
создании потоков система автоматически выделит блок памяти, достаточно 
большой, чтобы в нем уместились все TLS-переменные, необходимые как 
приложению, так и неявно подключаемым к ней DLL. Все так хорошо, что даже не верится! 
    И не верьте! Подумайте, что будет, если приложение вызовет LoadLibrary и 
подключит DLL, тоже содержащую статические TLS-переменные. Системе придется 
проверить потоки, уже существующие в процессе, и увеличить их блоки 
TLS-памяти, чтобы подогнать эти блоки под дополнительные требования, 
предъявляемые новой DLL. Ну а если Вы вызовете FreeLibrary для отключения DLL 
со статическими TLS-переменными, системе придется ужать блоки памяти, 
сопоставленные с каждым потоком в данном процессе. 
    Это слишком большая нагрузка на операционную систему. Кроме того, допуская 
явную загрузку DLL, содержащих статические TLS-переменные, система не в 
состоянии должным образом инициализировать TLS-данные, что при попытке 
обращения к ним может вызвать нарушение доступа. Это, пожалуй, единственный 
недостаток статической TLS; при использовании динамической TLS такой проблемы 
не возникает. DLL, работающие с динамической TLS, могут загружаться и 
выгружаться из выполняемой программы в любой момент и без всяких проблем. 

    Программа-пример TLSStat 

    Программа TLSStat (TLSSTAT.ЕХЕ) (см. листинг на рис. 13-4) демонстрирует 
применение статической локальной памяти потока. Файлы исходного кода и 
ресурсов для этой програм

    408 _____________________________Windows для профессионалов 

    мы находятся в каталоге TLSSTAT на компакт-диске, прилагаемом к книге. При 
первом ее запуске на экране появляется диалоговое окно, которое позволяет 
динамически создавать новые потоки и наблюдать за их выполнением: 

    Thread number:     1              Clear     i 
    Ыит of cycles;     ГГо 
    Sycle time (sees):   p;I' Create thread 1 
    Thread execution log 
    11                                                                         

    В самое начало программы включена строка: 

    __declspec(thread) DWORD gt_dwStartTime = 0; 

    В ней объявляется статическая переменная gt_dwStartTime. Всякий раз, когда 
в процессе создается новый поток, система создает новый экземпляр этой 
переменной. И если какойто поток ссылается на нее, он всегда ссылается на свой 
экземпляр переменной. 
    Щелчок кнопки Create Thread (создать поток) приводит к созданию нового 
потока, при этом из кучи выделяется структура данных, элементы которой 
заполняются информацией (номер потока, число циклов, исполняемых потоком, 
продолжительность каждого цикла), передаваемой только что созданному потоку. 
Указатель на эту структуру передается _beginthreadex, которая в свою очередь 
передает его в функцию потока. Последняя отвечает за освобождение данного 
блока памяти (когда необходимость в нем отпадает).2 
    Каждый новый поток начинает с того, что записывает системное время в 
переменную gt_dwStartTime и приступает к выполнению цикла. В начале каждой 
итерации поток выводит в окно списка Thread Execution Log (журнал работы 
потоков) общее время своего существования. 
    В этом примере я мог бы и не использовать переменную gt_dwStartTime, 
создав вместо нее локальную (базирующуюся на стеке) переменную dwStartTime и 
поместив ее внутрь ThreadFunc. По сути, так и следовало бы поступить: мы бы 
сэкономили память и уменьшили накладные расходы на работу со статической 
TLS-памятью. Но тогда я бы не смог продемонстрировать Вам использование 
статической TLS-памяти. 
    Еще раз обращаю Ваше внимание на то, что TLS-память 
создана_с11ециально_ддя DLL, хотя ее можно использовать и в приложениях. 
    Перед созданием потока можно установить число циклов (поле ввода Num Of 
Cycles) и продолжительность каждого цикла [поле ввода Cycle Time (Sees)]. 
Выполнение каждого потока занимает несколько секунд, поэтому можно создать еще 
несколько потоков, пока 
    2. Составляя этот раздел кода, я подумал, что неплохо было бы ввести в 
Win32 функцию, которая позволяет одному потоку каким-то образом изменять 
TLS-переменные, принадлежащие другому потоку. Будь такая функция, мне вообще 
не пришлось бы работать с кучей — я бы просто закрепил нужные значения за 
новым потоком. 

    _________________________Глава 13. Локальная память потока 409 

    работает первый; для этого запишите в поля ввода Num Of Cycles и Cycle 
Time (Sees) нужные значения и снова щелкните кнопку Create Thread. Показанный 
ниже экранный снимок сделан при одновременном выполнении 3 потоков: 

    Thread number.     Л              Clear     j 
    Hum ot cycles;     Я 
    Sycle time (sees):   p([""Create !hread"'i| 
    Thread execution loci: 
    Thread 1, Cycles lett=9, time running=0 
    Thread 1, Cycles left=8, time running=3005            : „': 
    Threadl.Cycle lefl-7.time running-6009           '.:, 
    Thread 1. Cycle left-6. time running-9013              ; 
    Thread started: 
    Thread 2, Cycle lett^.tme running=0              ! i: . 
    Thread 2. Cycle lert-3,tme runmng-2003            ,'„ 
    Threadl.Cycle left-B.tme running-12016           , ^ 
    Thread 2. Cycle left-2. tme running"1006           .,;; 
    Thread started:                                  • : , 
    Thread 3. Cycle left=2. tme running=0              —я' 
    Thread 1. Cycle left-< tme running-15022 
    Thread 2, Cycle left-1. tme running-6009              . 
    Thread 3, Cycle lett=1,tme running=1001              . 
    Thread 3. Cycle left=0. tme running-2003 
    ,1 Thread 2. Cycle left-0 tme running=S012           _d 
    h___________ 
    Эти З потока были созданы со следующими параметрами: 
    Thread Number          Num Of Cycles          Cycle Time (Sees) 
    (номер потока)           (число циклов)         [время цикла (сек)] 
    1                        10                        3 
    252 
    3                        3                        1 
    Счетчик Thread Number увеличивается при создании каждого нового потока. 
Состояние программы можно восстановить до исходного, щелкнув кнопку Clear 
(очистить); при этом счетчику потоков присваивается 1, а окно списка очищается. 
    И последнее. При завершении программы TLSStat на экране появляется окно: 

    Total time running applicetion-327941.   i 
    era 

    В нем сообщается общее время, затраченное программой (или первичным 
потоком) на ссылки на свою TLS-переменную gt_dwStartTime. 
потоком) на ссылки на свою TLS-переменную gt_dwStartTime. 


    410 _____________________________Windows для профессионалов 


    TLSSTAT.C 
    /**,****.***,,**,*.**..*..**..**.***,,**.,.*.,,**..********..*****,,*..,,**,.,. 
    Модуль: TLSStat.C 
    Автор: Copyright © 1995-1997, Джеффри Рихтер (Jeffrey Richter) 
    .*******,**.*****,***.***,,**.***.*.*,**,..***.**...***,****,*.***,.********../ 
    ftinclude "..\CmnHdr.H"           /* см. приложение В */ 
    ^include <windows.h> 
    ftinclude <windowsx.h> 
    ftinclude <tchar.h> 
    ftinclude <stdlib.h>              // для rand 
    ftinclude <stdio.h>               // для sprintf 
    ffinclude <process.h>             // для _beginthreadex 
    #include "Resource.H" 
    ///////////////////////
    II эта структура используется для передачи данных от одного потока другому 
    typedef struct { 
    int nThreadNum;     // номер потока 
    int nNumCycles;     // число циклов 
    DWORD dwCycleTime; // продолжительность цикла 
    } THREADDATA, *LPTHREADDATA; 
    
    // глобальный описатель окна списка, в котором регистрируется 
    // время выполнения каждого потока 
    HWND g_hwndLogLB = NULL: 
    
    // Глобальная статическая TLS-переменная для хранения стартового 
    // времени каждого потока. Система сама создает экземпляр этой 
    // переменной для каждого нового потока. 
    __declspec(thread) DWORD gt_dwStartTime = 0: 
    ////////////////////////////////
    DWORD WINAPI ThreadFunc (LPVOID IpvThreadParm) { 
    // параметр, переданный нам, - указатель на структуру THREADDATA; 
    // сохраним его в локальной переменной 
    LPTHREADDATA IpThreadData = (LPTHREADDATA) IpvThreadParm; 
    TCHAR szBuf[100]; 
    // запоминаем стартовое время потока в его статической TLS-переменной 
    gt_dw8tartTime = GetTickCount(); 
    // отмечаем в списке, что поток начал выполнение 
    _stprintf(szBuf, __TEXT("Thread started: %d"), lpThreadData->nThreadNum); 
    ListBox_AddString(g_hwndLogLB, szBuf); 
    ListBox_SetCurSel(g_hwndLogLB, 0); 

    см. след. стр. 

    Рис. 13-4. Листинг программы TLSStat 

    __________________________Глава 13. Локальная память потока 411 


    РИС. 13-4  (продолжение) 
    
    II начинаем что-то делать... 
    while (lpThreadData->nNumCycles--) { 
    
    // записываем в список, сколько циклов осталось пройти потоку 
    // перед его завершением и сколько времени он уже работает 
    _stprintf(szBuf, 
    
    __ТЕХТ("Thread %d, Cycles left=%d, time running=%d"), 
    lpThreadData->nThreadNurn, lpThreadData->nNumCycles, 
    GetTickCountO - gt_dwStartTime); 
    ListBox_AddString(g_hwndLogLB, szBuf): 
    // "засыпаем" ненадолго и даем поработать другим потокам 
    Sleep(lpThreadData->dwCycleTime): 
    } 
    
    // поток закончил свою работу; пишем об этом в списке 
    // и сообщаем общее время выполнения этого потока 
    _stprintf(szBuf, __TEXT("Thread ended: %d, total tlme=%d"), 
    lpTnreadData->nThreadNum, GetTickCountO - gt_dwStartTime); 
    ListBox_AddString(g_hwndLogLB, szBuf): 
    // поток отвечает за удаление структуры THREADDATA, 
    // выделенной первичным потоком 
    HeapFree(GetProcessHeap(), 0, IpvThreadParm); 
    return(O); 
    i 
    /////////////////////////////////
    BOOL Dlg_OnInitDialog (HWND hwnd, HWND hwndFocus, LPARAM IParam) { 
    
    // связываем значок с диалоговым окном 
    chSETDLGICONS(hwnd, IDI_TLSSTAT, IDI_TLSSTAT); 
    // по умолчаний номер потока - 1 
    SetDlgItemInt(nwnd, IDC_THREADNUM, 1, FALSE); 
    
    // по умолчанию число циклов - 10 
    SetDlgItemInt(hwnd, IDC_NUMCYCLES. 10, FALSE); 
    
    // по умолчаний время цикла - 3 секунды 
    SetDlgItemInt(hwnd, IDC_CYCLETIME, 3, FALSE); 
    // записываем описатель диалогового окна в глобальную переменную, 
    // чтобы к нему можно было легко обратиться из функции потока 
    g_hwndLogLB = GetDlgItem(hwnd. IDC_LOG): 

    см. след. стр. 

    412 ______________________________ Windows для профессионалов 


    РИС. 13-4  (продолжение) 
    
    II начнем с того, что поместим кнопку Create Thread 
    // в фокус клавиатуры 
    SetFocus(GetDlgItem(hwnd, IDOK)); 
    
    // фокус установлен мною. поэтому Dialog Manager'у этого делать не надо 
    return(FALSE); 
    ////////////////////////////////////////////    
    void Dlg_OnCommand (HWNO hwnd, int id, HWND hwndCtI, UINT codeNotify) { 
    DWORD        dwIDThread: 
    HANDLE       hThread; 
    LPTHREADDATA IpThreadData; 
    switch (id) { 
    case IDC_CLEAR; 
    
    // восстанавливаем начальное состояние программы 
    Set01gltemlnt(hwnd, IDC_THREADNUM, 1, FALSE): 
    ListBox_ResetContent(g_hwndLogLB): 
    break; 
    case IDOK: 
    
    // выделяем блок памяти, с помощью которого можно будет 
    // передавать данные из этого потока в новый - тот, 
    // который мы собираемся создать 
    
    IpThreadData = (LPTHREADDATA) HeapAlloc(GetProcessHeap(), О, 
    sizeof(THREADDATA)); 
    if (IpThreadData == NULL) { 
    // память выделить не удалось; выводим сообщение 
    chMB(__TEXT("Error creating ThreadOata")); 
    break; 
    // заполняем блок памяти данными из диалогового окна 
    lpThreadData->nThreadNum = 
    GetDlgItemInt(hwnd, IDC_THREADNUM, NULL. FALSE): 
    lpThreadData->nNumCycles = 
    GetDlgItemInt(hwnd, IDC_NUMCYCLES, NULL, FALSE); 
    // умножаем время цикла на 1000. 
    // чтобы преобразовать в миллисекунды 
    lpThreadData->dwCycleTime = (DWORD) 
    (1000 * GetDlgItemInt(hwnd, 
    IDC_CYCLETIME. NULL, FALSE)): 

    см. след. стр. 

    ________________________Глава 13. Локальная память потока 413 


    РИС. 13-4  (продолжение) 
    
    II увеличиваем счетчик потоков 
    SetDlgItemInt(hwnd, IDC_THREADNUM, lpTnreadData->nThreadNum + 1, FALSE); 
    
    // Создаем новый поток и передаем ему адрес выделенного 
    // нами блока памяти, содержащего атрибуты, которые должен 
    // использовать этот поток. Он отвечает за освобождение блока 
    // памяти, когда тот станет ненужным. 
    hThread = chBEGINTnREADEX(NULL. О, ThreadFunc, 
    (LPVOID) IpThreadData, 0, &dwIDThread); 
    if (hThread != NULL) { 
    // если новый поток создан, закрываем описатель; 
    // этому потоку больше не надо обращаться к новому 
    CloseHandle(hThread); 
    !^» { 
    // поток создать не удалось; выводим сообщение 
    chMB(__TEXT("Error creating the new thread")); 
    HeapFree(GetProcessHeap(), 0, (LPVOID) IpThreadData); 
    } 
    break; 
    case 1DCANCLL: 
    EndDialog(hwnd, id): 
    break: 
    BOOL CALLBACK Dlg_Proc (HWND hwnd. UINT uMsg, WPAHAM wparam, LpAhAM 
ipaiam,) | 
    switch (uMsg) { 
    chHANDLE_DLGMSG(hwnd. WM_INITDIALOG, Dlg_OnImtDialog); 
    chHANDLE_DLGMSG(hwnd, WM_COMMAND, Dig_OnCommand); 
    } return(FALSE); 
    \ 
    int WINAPI _tWinMain (HINSTANCE hinstExe, 
    HINSTANCE hinstPrev, LPTSTR pszCmdLine, int nCmdShow) { 
    TCHAR szBuf[100]; 
    chWARNIFUNICODEUNDERWIN95(); 

    см. след. стр. 

    414 ______________________________Windows для профессионалов 


    РИС. 13~4   (продолжение) 

    II Первичный поток тоже получает свой экземпляр переменной gt_dw8tartTime. 
    // Инициализируем ее временем начала выполнения программы. 
    gt_dw8tartTime = GetTickCount(): 
    DialogBox(hinstExe, MAKEINTRESOURCE(IOD_TLSSTAT). NULL, Dlg_Proc)' 
    // пользователь закрыл диалоговое окно: покажем, 
    // сколько времени работала программа 
    _stprintf(szBuf, __TEXT("Total time running application=%d."), 
    GetTickCountO - gt_dwStartTime): 
    chMB(3zBuf); 
    '^е+игп^):