Работа с локальной памятью потока (TLS)
В данной статье мы опишем так называемую локальную память потока (TLS, Thread Local Storage).
Многие алгоритмы, которые сейчас работают в составе Windows программ, были перенесены с операционной системы MS DOS. Но операционная сисмема MS DOS по своей сути является однопоточной. поэтому использование этих алгоритмов в многопоточной среде может вызвать проблемы. Одним из таких узких мест являтеся использование процедурами и функциями глобальных переменных.
Обратимся к простому примеру. Пусть существует набор из трех процедур, которые предназначены для нахождения суммы нескольких чисел и реализованы так, как показано ниже.
var Sum: Integer; procedure ClearSum; begin Sum := 0; end; procedure AddNumber(Number: Integer); begin Sum := Sum + Number; end; function GetSum: Integer; begin Result := Sum; end;Очевидно, что использование этих процедур одновременно в нескольких потоках может привести к тому, что числа, переданные одним потоком, смешаются с числами, передаваемыми другими потоками, и мы не получим нужный результат. Пример того, как работает указанные выше набор процедур при многопоточности можно найти в файле TLSDemo01.dpr.
Конечно, данный пример весьма прост. К более сложным примерам можно отнести алгоритм решения системы линейных уравнений методом Гаусса, который использует глобальный массив. Попытка решить одновременно две системы в разных потоках приведет к тому, что данные в матрице исказяться и системы будут решены неправильно.
Какие же выходы из создавшегося положения, кроме как переделывания всех алгоритмов с учетом многопоточности, предоставляет нам Delphi, Win API, и как реализована в Delphi поддержка TLS - вот предмет нашего дальнейшего рассмотрения.
Использование локальной памяи потока в Delphi
Как можно решить приведенную выше проблему? Самое простое - реализовать возможность описания таких переменных, чтобы в каждом новом потоке создавалась из отдельная копия. Нетрудно догадаться, что такая возможность имеется в Delphi. Для описания таких переменных используется ключевое слово threadvar. Запустите программу TLSDemo02.dpr, чтобы убедиться в том, что она работает корректно, а ведь она отдичается от программы TLSDemo01.dpr только шестью символами в строке 6!
Из этой особенности переменных, описанных в секции threadvar, вытекают также следующие ограничения:
эти переменные не могут иметь начальное значение;
эти переменные не могут быть локальными;
к этим переменных не применима директива absolute;
при описания обычных переменных в секции absolute нельзя ссылаться на переменные threadvar.
Все сказанное проиллюстрировано ниже:
threadvar I: Integer = 5; { Ошибка - переменная не может иметь начального значения } CrtMode: Byte absolute $0040; { Ошибка - к переменной неприменима директива absolute } Count: Integer; // Правильно var Reference: Integer absolute Count; { Ошибка - ссылка на переменную threadvar } procedure MyProc; threadvar { Неправильно - описание threadvar переменных в процедуре }Тем не менее, описание переменных в секции threadvar не делает их потоко-безопасными. Это, в первую очередь, применимо для всех переменных, для которых реализован механизм подсчета ссылок (строки, динамические массивы и т. д.). Рассомтрим следующий пример:
threadvar S1: string; var S2: string; { Строки, выполняемые в первом потоке } S1 := 'Test'; S2 := S1; .................... S1[3] := 'x'; { Строки, выполняемые во втором потоке } S2 := '';Будем полагать, что вначале выполняться строки 8-9 первого потока, а затем одновременно выполняться строки 11 и 14. Тогда возможен следубщий сценарий:
1. Поток #1 выполняет строку 11. Он видит, что счетчик ссылок равен 2, поэтому уменьшает его на единицу (счетчик ссылок=1), но в это время прерывается.
2. Поток #2 видит, что счетчик ссылок равен единице, поэтому без заззрения совести освобождает строку, а указателю не нее присваивает nil.
3. Поток #1 пробуждается и доделывает свои манипуляции: выделяет новый фрагмент памяти, копирует в него старое содержимое строки... Но, строка уже указывает на освобожденный фрагмент памяти, что может привести к любимому всеми исключению Access violation.
Так вот, ключевое слово threadvar НЕ ДЕЛАЕТ такие пременные потоко-безопасными, и если вы хотите обращаться к переменным таких типов из разных потоков, то их работу необходимо синхронизировать.
Разработчики Delphi не рекомендуют использовать в качестве threadvar переменных указатели и процедурные типы.
Функции для работы с локальной памятью потока, предоставляемые Win32 API В Windows существует всего четыре простеньких функции для работы с локальной памятью потока. Но прежде чем их описывать, расскажем немного об основах TLS. При создании каждого потока Windows автоматически выделяет ему некоторый участок памяти, правда фиксированного размера, который содержит данные, доступные только этому потоку. Тут же храниться массив из TLS_MINIMUM_AVAIBLE указателей, который и использутся в качестве локальной памяти потока. Так что если поток хочет получить какое-то количество данных, то необходимо просто взять один из таких указателей (например нулевой), выделить ему столько памяти, сколько требуется и использовать его в качетве блока локальных переменных.
Все бы было хорошо, да только в потоко-независимых переменных иногда нуждаются и библиотеки dll. Если в программе мы можем заранее распределить все элементы массива указателей по своему усмотрению, то библиотека dll заранее не может сказать, какой же эелементов массива указателей будет свободен. Поэтому необходима функция, которая бы возвращала номер какого-либо свободного слота. Соответственно мы приходим к тому, чтобы возложить на операционную систему контроль за занятыми и свободными слотами.
Кстати, вы можете потренировать свой навык набора на виртуальной клавиатуре с армянской раскладкой по ссылке
all-armenia.com/onlajn-klaviatura-(raskladka).html
А вот и обещанные четыре функции Windows:
function TlsAlloc: DWORD; stdcall;Функция возвращает номер свобожного элемента массива указателей, обнуляет указатель по этому индексу и меняет состояние этого элемента на занятое. Если свободных указателей больше нет, то функция вернет $FFFFFFFF.
function TlsFree(dwTlsIndex: DWORD): BOOL; stdcall;А эта функция просто делает элемент массива указателей с номером dwTlsIndex свободным. При этом не производится никакой попытки освободить указатель с указанным индексом - эта работа, которая лежит на Вас.
function TlsGetValue(dwTlsIndex: DWORD): Pointer; stdcall;Получаем элемент массива с индексом dwTlsIndex. В случае неудачного звершения функция вернет нулевой указатель. Но что если мы поместили по указанному индексу нулевой указатель? Тогда надо смотреть, что вернет GetLastError. Если он вернет NO_ERROR, значит действительно там храниться сейчас нулевой указатель. А иначе была ошибка. С другой стороны, если верить Джеффри Рихтеру, то в целях обеспечения скорости, в Windows не реализовано никакой проверки, свободен ли указанный элемент массива. Так что, во-первых, необходимо соблюдать осторожность при работе с TLS, а, во-вторых, необходимо поламать голову над тем, будет ли когда-нибудь такая проверка реализована в последующих версиях Windows, и стоит ли все эти проверки включать в свой код.
function TlsSetValue(dwTlsIndex: DWORD; lpTlsValue: Pointer): BOOL; stdcall;Устанавливаем в элемент массива с индексом dwTlsIndex в значение lpTlsValue. Возвращает True при удачном завершении и False в случае ошибки. А дальше перечитайте описание функции TlsGetValue на предмет того, как реализована в Windows проверка ошибок
В заключение дадим ссылку на программу, которая выполняет все тоже, что и программа TLSDemo02.dpr, но с использованием функций Win32 API: TLSDemo03.dpr.
3. Как это реализовано в Delphi 5 Отметим, что все сказанное ниже относится к пятой версии Delphi и только к ней. Возможно, в прошлых версия это было не так, и в будущих это будет по-другому. Тогда зачем нужно это рассматривать? Ну, во-первых, изучение исходников полезно само по себе (ответ на вопрос, как это делают профессионалы). А во вторых, операторы меняются, а принципы остаются.
Все указанные ниже процедура располагаются в модуле SysInit, который можно найти в директории
Одной из основных процедур Delphi, предназначенной для поддежки потоко-независимых переменых является _GetTLS. Эта процедура возвращает в регистре EAX указатель на блок threadvar переменных данного потока и вызывается непосредственно перед каждым обращением к потоко-независимой переменной. Вот ее реализация:
procedure _GetTls; asm MOV CL,ModuleIsLib MOV EAX,TlsIndex TEST CL,CL JNE @@isDll MOV EDX,FS:tlsArray MOV EAX,[EDX+EAX*4] RET @@initTls: CALL InitThreadTLS MOV EAX,TlsIndex PUSH EAX CALL TlsGetValue TEST EAX,EAX JE @@RTM32 RET @@RTM32: MOV EAX, tlsBuffer RET @@isDll: PUSH EAX CALL TlsGetValue TEST EAX,EAX JE @@initTls end;Конечно, для новичка выглядит страшно, но... Обо всем по порядку.
1. Смотрим, эта часть кода выполняется в части dll или в части exe (переменная IsModuleLib). Если это exe, то мы возвращаем значение TlsGetValue(TlsIndex), только полученное весьма хитрым способом:
MOV EAX,TlsIndex MOV EDX,FS:tlsArray MOV EAX,[EDX+EAX*4]TlsIndex - это индекс блока потоко-независимых переменных для данной dll или exe-модуля. Дело в том, что фактически по смещению tlsArray=$2C расположен указатель на массив указатлей, специфичных для данного потока упомянутый нами ранее массив из TLS_MINIMUM_AVAIBLE указателей), и мы вместо вызова TlsGetValue просто обращаемся к нужному нам индексу (строка 8). Ничего предосудительного в этом нет, компиляторы Microsoft используют ту же конструкию, хотя эта возможность и недокументирована.
2. Это dll. Смотрим, что нам вернула TlsGetValue(TlsIndex) и если это не нуль, то оное значение возвращается. Иначе...
3. Попытка проинициализировать блок TLS посредством вызова InitThreadTLS. И опять проверяем, что нам вернула TlsGetValue(TlsIndex). Если не нуль, то возвращаемся, а иначе...
4. Возвращаем значение хитрой переменной TlsBuffer (Случай dll, будет рассмотрен ниже).
Для случая части exe-модуля это все. Инициализацию сегмента TLS и установку переменной TlsIndex берет на себя непосредственно загрузчик программы в Delphi, которого мы и не видим.
В случае же dll инициализацию проводит вызов происходит при подсоединении к dll нового процесса. Рассмотрим этапы инициализации и деинициализации блока TLS для dll:
Инициализация TLS (случай DLL)
Сначала исходник, а болтовня потом:
procedure InitThreadTLS; var p: Pointer; begin if @TlsLast = nil then Exit; if TlsIndex < 0 then RunError(226); p := LocalAlloc(LMEM_ZEROINIT, Longint(@TlsLast)); if p = nil then RunError(226) else TlsSetValue(TlsIndex, p); tlsBuffer := p; end; procedure InitProcessTLS; var i: Integer; begin if @TlsLast = nil then Exit; i := TlsAlloc; TlsIndex := i; if i < 0 then RunError(226); InitThreadTLS; end;В обоих функциях присутствует загадочная переменная TlsLast. Компилятор всегда помещает эту переменную последней в сегменте переменных threadvar. Начало сегмента есть $00000000. Следовательно, адрес @TlsLast, используемый в обеих процедурах, есть не что иное, как суммарный размер всех потоко-независимых переменных. И @TlsLast=nil если в dll нет потоко-независимых переменных.
Что делает .InitProcessTLS? Смотрим по строкам.
1. Проверяет адрес переменной TlsLast на nil. Если это так, то никакой инициализации не требуется (строки 21-22).
2. Выполняет TlsAlloc и выполняет контроль на ошибку (строки 23-26).
3. Далее вызывается InitThreadTLS. (строка 27).
То есть, если выкинуль проверку на ошибки, процедура InitProcessTLS инициализирует переменную TlsIndex и выполняет вызов InitThreadTLS.
Что делает InitThreadTLS? Смотрим по строкам.
1. Проверяет адрес TlsLast на nil. Если это так, то выходим.
2. Проверяет на допустимость TlsIndex. (строки 7-8).
3. Производит выделение динамической памяти для блока переменных threadvar и присваевает указатель на него как в tlsBuffer, так и испотльзуя вызов TlsSetValue(TlsIndex, <значение>). (строки 9-14). Заметим, что поскольку менеджер памяти может быть еще не проинициализирован системой, то динамическая память выделяется вызовом Win API.
Обобщаем. В случае dll при подключении к ней нового процесса происходит вызов InitProcessTLS, который выполняет инициализацию переменных TlsIndex и TlsBuffer. При обращении в dll к threadvar переменным, мы пытаемся получить указатель на переменные по индексу TlsIndex, и если попытка тщетна (например, обращение к переменной было произведено в потоке в первый раз), то выполняем вызов TlsInitThread, А потом перечитываем заново указатель TlsIndex. Самое интересное, что если в этом случае мы получаем nil, то берем значение из переменной TlsBuffer. Когда это может случиться и в чем смысл мне пока неизвестно, по крайней мере в отладчике я ни разу на эти строки не попадал.
Осталась
Деинициализация TLS (случай DLL)
Опять же, вначале рассмотрим исходник:
procedure ExitThreadTLS; var p: Pointer; begin if @TlsLast = nil then Exit; if TlsIndex >= 0 then begin p := TlsGetValue(TlsIndex); if p <> nil then LocalFree(p); end; end; procedure ExitProcessTLS; begin if @TlsLast = nil then Exit; ExitThreadTLS; if TlsIndex >= 0 then TlsFree(TlsIndex); end;Думаю, что сокрыто за этим исходником ясно и без комментариев. ExitThreadTLS освобождает динамическую память, связанную с конкретным потоком, а ExitProcessTLS освобождает элемент массива TLS с индексом TlsIndex. Вот и всё, Удачи!