Домашняя страница Что такое TLS и как ее использовать?
Публикация
Отменить

Что такое TLS и как ее использовать?

Уже давно программы перестали быть однопоточными. Сейчас даже маленькие приложения используют множество потоков, для различных фоновых процессов. Для разработки эффективного и thread-save кода нужно понимание того, что такое TLS (Thread Local Storage - локальная память потока) и того когда и как этим пользоваться.

Зачем нужна TLS

Допустим создается приложение которое параллельно обрабатывает большой набора данных. Существует некий массив, каждый элемент которого вместе с его содержимым соответствует отдельному потоку, который ответственен за вычисления связанные с этим элементом. Откуда конкретный поток узнает, какой индекс в глобальном массиве его?

Можно передать функции потока ThreadProc этот индекс в качестве параметра. Тогда индекс будет храниться в локальной переменной потока. Но представьте, что ThreadProc вызывает какую-то функцию. Потом еще одну, и так он может вызывать сотни функций с разными уровнями вложенности. Куда денется индекс, которым владеет поток? Можно попытаться передавать этот индекс каждой функции, но это очевидно будет сказываться на эффективности. Именно для решения подобных проблем нужна память, специфичной для потока - TLS.

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

Итак, TLS - это локальная память потока. Она бывает статической и динамической. С каждым потоком в процессе можно ассоциировать некоторую область в памяти, в которой будут храниться определенные данные. У каждого треда эта область памяти своя, и получить к ней доступ из другого потока нельзя. Таким образом, это призвано решить проблему совместного доступа к данным.

Динамическая TLS

Чтобы связать данные с динамической TLS поток может использовать четыре WinAPI функции – TlsAlloc, TlsGetValue, TlsSetValue, TlsFree. Поток имеет определенное количество ячеек каждая из которых размером 4 байта. Количество ячеек разниться в зависимости от версии Windows, но самое маленькое – это 64 ячейки для Windows 95. В более новых ОС количество доступных ячеек увеличивается.

1
2
3
64 - Windows 95
80 - Windows 98/Me
1088 - Windows 2000/XP

Работать с этими функциями достаточно просто. Функция TlsAlloc используется для резервирования ячейки в локальной памяти потока и возвращает ее индекс.

Функция TlsSetValue устанавливает значение в ячейке с данным индексом. Она принимает индекс возвращенный функцией TlsAlloc, и значение для сохранения в ячейке с данным индексом. Функция возвращает 1 в случае успеха и 0 в противном случае.

Функция TlsGetValue соответственно возвращает значение указанное данным индексом. В случае ошибки возвращается 0. Чтобы различить нулевое значение в ячейке, с сигнализацией об ошибке вызывайте GetLastError. Если ошибки не было, то GetLastError вернет NO_ERROR.

Функция TlsFree делает ячейку памяти свободной.

Для получения дополнительной информации в случае ошибки используется функция GetLastError.

Вот пример работы с динамической TLS:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// глобальные TLS индексы
DWORD g_intId; 
DWORD g_charId;

// главная функция потока
DWORD WINAPI ThreadFunc( LPVOID lpParam ) 
{
// сохранить параметр в локальную переменную потока
   TlsSetValue(g_intId, lpParam);

// сохранить указателя на выделенную память
   TlsSetValue(g_charId, new char[25]);
...

// получение значения параметра
   int i = TlsGetValue(g_intId);
   const char* buf = TlsGetValue(g_charId);
...
   delete[] TlsGetValue(g_charId);
   return 0; 
} 

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, intnCmdShow)
{ 
   DWORD threadId;

   g_intId = TlsAlloc();
   g_charId = TlsAlloc();

   CreateThread(NULL, 0, ThreadFunc, (LPVOID)1, 0, &threadId);
   CreateThread(NULL, 0, ThreadFunc, (LPVOID)2, 0, &threadId);

   TlsFree(g_intId);
   TlsFree(g_charId);
   return 0;
}

Статическая TLS

Статическая локальная память для потока не использует API функций, а используется механизмы загрузчика. Для работы со статической TLS используются синтаксические средства языка программирования. Основную работу с thread-local storage берет на себя операционная система. Линкер генерирует специальные структуры в PE-файле, а также секцию с именем .tls (как правило), в которых хранятся все нужные данные для того, чтобы загрузчик модуля правильно инициализировал локальную память потоков.

Компилятор Microsoft VC++ позволяет использовать следующий синтаксис для создания переменной специфичной для потока:

1
2
3
4
__declspec(thread) int g_int = 1; // это не индекс ячейки, а сама переменная
__declspec( thread ) char g_char[25];

// дальше можно использовать эти переменные, как обычные глобальные переменные

Как видно, подобный метод значительно проще в программирование и делает код более чистым и понятным, однако накладывает некоторые ограничения.

Ограничения TLS

Специфика реализации доступа к TLS налагает некоторые ограничения на использование статической TLS в высокоуровневых компиляторах. Вот некоторые из них:

  1. __declspec( thread ) может быть использован только с данными.
  2. TLS можно применять только к глобальным переменным.
  3. Нельзя получить адрес переменной TLS, т.к. он не является константой.
  4. Этот механизм нельзя использовать в DLL, которую динамически загружают с помощью LoadLibrary. Для них нужно вызывать функции TlsXxx, которые были описаны выше. Эта проблема актуальна в WindowsXP и исправлена в ОС Windows Vista и старше. Проблем с использованием DLL, которые линкуются статически - нет.

Подробнее про ограничения TLS можно прочитать тут.

Публикация защищена лицензией CC BY 4.0 .