Работа с локальной памятью потока (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, который можно найти в директории \Source\Rtl.

Одной из основных процедур 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.

Вот и всё, Удачи!


    No results found.
Отменить.