Мастера DELPHI, Delphi programming community Рейтинг@Mail.ru Титульная страница Поиск, карта сайта Написать письмо 
| Новости |
Новости сайта
Поиск |
Поиск по лучшим сайтам о Delphi
FAQ |
Огромная база часто задаваемых вопросов и, конечно же, ответы к ним ;)
Статьи |
Подборка статей на самые разные темы. Все о DELPHI
Книги |
Новинки книжного рынка
Новости VCL
Обзор свежих компонент со всего мира, по-русски!
|
| Форумы
Здесь вы можете задать свой вопрос и наверняка получите ответ
| ЧАТ |
Место для общения :)
Орешник |
Коллекция курьезных вопросов из форумов
KOL и MCK |
KOL и MCK - Компактные программы на Delphi
 

Работа с локальной памятью потока (TLS)

В данной статье мы опишем так называемую локальную память потока (TLS, Thread Local Storage).

Многие алгоритмы, которые сейчас работают в составе Windows программ, были перенесены с операционной системы MS DOS. Но операционная сисмема MS DOS по своей сути является однопоточной. поэтому использование этих алгоритмов в многопоточной среде может вызвать проблемы. Одним из таких узких мест являтеся использование процедурами и функциями глобальных переменных.

Обратимся к простому примеру. Пусть существует набор из трех процедур, которые предназначены для нахождения суммы нескольких чисел и реализованы так, как показано ниже.

 1:  var Sum: Integer;
 2:  
 3:  procedure ClearSum;
 4:  begin
 5:    Sum := 0;
 6:  end;
 7:  
 8:  procedure AddNumber(Number: Integer);
 9:  begin
10:    Sum := Sum + Number;
11:  end;
12:
13:  function GetSum: Integer;
14:  begin
15:    Result := Sum;
16:  end;

Очевидно, что использование этих процедур одновременно в нескольких потоках может привести к тому, что числа, переданные одним потоком, смешаются с числами, передаваемыми другими потоками, и мы не получим нужный результат. Пример того, как работает указанные выше набор процедур при многопоточности можно найти в файле TLSDemo01.dpr.

Конечно, данный пример весьма прост. К более сложным примерам можно отнести алгоритм решения системы линейных уравнений методом Гаусса, который использует глобальный массив. Попытка решить одновременно две системы в разных потоках приведет к тому, что данные в матрице исказяться и системы будут решены неправильно.

Какие же выходы из создавшегося положения, кроме как переделывания всех алгоритмов с учетом многопоточности, предоставляет нам Delphi, Win API, и как реализована в Delphi поддержка TLS - вот предмет нашего дальнейшего рассмотрения.

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

Как можно решить приведенную выше проблему? Самое простое - реализовать возможность описания таких переменных, чтобы в каждом новом потоке создавалась из отдельная копия. Нетрудно догадаться, что такая возможность имеется в Delphi. Для описания таких переменных используется ключевое слово threadvar. Запустите программу TLSDemo02.dpr, чтобы убедиться в том, что она работает корректно, а ведь она отдичается от программы TLSDemo01.dpr только шестью символами в строке 6!

Из этой особенности переменных, описанных в секции threadvar, вытекают также следующие ограничения:

  • эти переменные не могут иметь начальное значение;
  • эти переменные не могут быть локальными;
  • к этим переменных не применима директива absolute;
  • при описания обычных переменных в секции absolute нельзя ссылаться на переменные threadvar.
  • Все сказанное проиллюстрировано ниже:

     1: threadvar
     2:
     3:   I: Integer = 5; 
     4:     {  Ошибка - переменная не может иметь начального
     5:       значения }
     6:
     7:   CrtMode: Byte absolute $0040; 
     8:     {  Ошибка - к переменной неприменима директива
     9:       absolute }
    10:
    11:   Count: Integer; // Правильно
    12:
    13: var
    14:   Reference: Integer absolute Count;
    15:     {  Ошибка - ссылка на переменную threadvar }
    16: 
    17: procedure MyProc;
    18: threadvar
    19:     {  Неправильно - описание threadvar переменных в 
    20:       процедуре }
    

    Тем не менее, описание переменных в секции threadvar не делает их потоко-безопасными. Это, в первую очередь, применимо для всех переменных, для которых реализован механизм подсчета ссылок (строки, динамические массивы и т. д.). Рассомтрим следующий пример:

     1: threadvar
     2:  S1: string;
     3:
     4: var 
     5:   S2: string;
     6:
     7: { Строки, выполняемые в первом потоке }
     8:   S1 := 'Test';
     9:   S2 := S1;
    10:   ....................
    11:   S1[3] := 'x';
    12: 
    13: { Строки, выполняемые во втором потоке }
    14:   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 заранее не может сказать, какой же эелементов массива указателей будет свободен. Поэтому необходима функция, которая бы возвращала номер какого-либо свободного слота. Соответственно мы приходим к тому, чтобы возложить на операционную систему контроль за занятыми и свободными слотами.

    А вот и обещанные четыре функции 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_DIRECTORY>\Source\Rtl.

    Одной из основных процедур Delphi, предназначенной для поддежки потоко-независимых переменых является _GetTLS. Эта процедура возвращает в регистре EAX указатель на блок threadvar переменных данного потока и вызывается непосредственно перед каждым обращением к потоко-независимой переменной. Вот ее реализация:

     1: procedure _GetTls;
     2: asm
     3:         MOV     CL,ModuleIsLib 
     4:         MOV     EAX,TlsIndex 
     5:         TEST    CL,CL 
     6:         JNE     @@isDll 
     7:         MOV     EDX,FS:tlsArray 
     8:         MOV     EAX,[EDX+EAX*4] 
     9:         RET 
    10:
    11: @@initTls: 
    12:         CALL    InitThreadTLS 
    13:         MOV     EAX,TlsIndex 
    14:         PUSH    EAX 
    15:         CALL    TlsGetValue 
    16:         TEST    EAX,EAX 
    17:         JE      @@RTM32 
    18:         RET 
    19: 
    20: @@RTM32: 
    21:         MOV     EAX, tlsBuffer 
    22:         RET 
    23: 
    24: @@isDll: 
    25:         PUSH    EAX 
    26:         CALL    TlsGetValue 
    27:         TEST    EAX,EAX 
    28:         JE      @@initTls 
    29: end;
    

    Конечно, для новичка выглядит страшно, но... Обо всем по порядку.

    1. Смотрим, эта часть кода выполняется в части dll или в части exe (переменная IsModuleLib). Если это exe, то мы возвращаем значение TlsGetValue(TlsIndex), только полученное весьма хитрым способом:

     
     4:         MOV     EAX,TlsIndex 
     7:         MOV     EDX,FS:tlsArray 
     8:         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)

    Сначала исходник, а болтовня потом:

     1: procedure       InitThreadTLS;
     2: var
     3:  p: Pointer;
     4: begin
     5:  if @TlsLast = nil then
     6:    Exit;
     7:  if TlsIndex < 0 then
     8:    RunError(226);
     9:  p := LocalAlloc(LMEM_ZEROINIT, Longint(@TlsLast));
    10:  if p = nil then
    11:    RunError(226)
    12:  else
    13:    TlsSetValue(TlsIndex, p);
    14:  tlsBuffer := p;
    15: end;
    16:
    17: procedure       InitProcessTLS;
    18: var
    19:   i: Integer;
    20: begin
    21:   if @TlsLast = nil then
    22:     Exit;
    23:   i := TlsAlloc;
    24:   TlsIndex := i;
    25:   if i < 0 then
    26:     RunError(226);
    27:   InitThreadTLS;
    28: 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)

    Опять же, вначале рассмотрим исходник:

     1: procedure       ExitThreadTLS;
     2: var
     3:  p: Pointer;
     4: begin
     5:  if @TlsLast = nil then
     6:    Exit;
     7:  if TlsIndex >= 0 then begin
     8:     p := TlsGetValue(TlsIndex);
     9:     if p <> nil then
    10:       LocalFree(p);
    11:   end;
    12: end;
    13:
    14: procedure       ExitProcessTLS;
    15: begin
    16:   if @TlsLast = nil then
    17:     Exit;
    18:   ExitThreadTLS;
    19:   if TlsIndex >= 0 then
    20:     TlsFree(TlsIndex);
    21: end;
    

    Думаю, что сокрыто за этим исходником ясно и без комментариев. ExitThreadTLS освобождает динамическую память, связанную с конкретным потоком, а ExitProcessTLS освобождает элемент массива TLS с индексом TlsIndex.

       Внимание! Запрещается перепечатка данной статьи или ее части без согласования с автором. Если вы хотите разместить эту статью на своем сайте или издать в печатном виде, свяжитесь с автором.
    Автор статьи:  Мистик
      

    Другие статьи Наверх


      Рейтинг@Mail.ru     Титульная страница Поиск, карта сайта Написать письмо