Локальное хранилище потока в С#

В управляемом окружении можно создавать локальное хранилище потока (thread-local storage — TLS). В зависимости от приложения, может понадобиться иметь статическое поле класса, уникальное для каждого потока, в котором используется класс. В большинстве случаев на С# сделать это очень просто.

Статическое поле, которое должно быть привязано к потоку, нужно просто снабдить атрибутом ThreadStaticAttribute.

После этого поле будет инициализировано отдельно для каждого потока, обращающегося к нему. “За кулисами” каждый поток получает собственное связанное с ним место хранения значения или ссылки. Однако при использовании ссылок на объекты будьте осторожны, выдвигая предположения о создании объектов.

В приведенном ниже коде демонстрируется возможная ловушка, которой следует избегать:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using System.Threading;
public class TLSClass
{
public TLSClass ()  {
Console.WriteLine ( "Создание TLSClass" );
}
public class TLSFieldClass {
[ThreadStatic]
public static TLSClass tlsdata = new TLSClass ();
}
public class EntryPoint {
private static void ThreadFunc ()  {
Console.WriteLine ( "Поток {0} запускается...",
Thread.CurrentThread.ManagedThreadld ) ; 
Console.WriteLine( "tlsdata для этого потока - \"{0}\"", TLSFieldClass.tlsdata ); 
Console.WriteLine( "Поток {0} завершается", Thread.CurrentThread.ManagedThreadld ) ;
static void Main()  { Thread threadl = new Thread( new ThreadStart(EntryPoint.ThreadFunc) ); 
Thread thread2 = new Thread( new ThreadStart(EntryPoint.ThreadFunc) ); 
threadl.Start(); 
thread2.Start();
}

В коде создаются два потока, которые обращаются к связанному с потоком статическому члену TLSFieldClass. Чтобы проиллюстрировать ловушку, присутствующую в коде, предусмотрен специфичный для потока элемент типа TLSClass, который код пытается инициализировать с помощью инициализатора в определении класса, просто вызывающего new с конструктором по умолчанию.

Теперь посмотрите, насколько неожиданный вывод получается:

Поток 3 запускается…
Поток 4 запускается…
Создание TLSClass
tlsdata для этого потока – “TLSClass”
Поток 3 завершается
tlsdata для этого потока – “”
Поток 4 завершается

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

Важный момент, который следует здесь отметить: конструктор TLSClass вызывался лишь однажды. Конструктор был вызван для первого потока, но не для второго.

Во втором потоке поле инициализируется значением null. Поскольку поле tlsdata является статическим, его инициализация в действительности производится в момент вызова статического конструктора TLSFieldClass. Однако статические конструкторы могут быть вызваны только один раз для одного класса в одном домене приложений.

По этой причине следует избегать присваивания значений элементам, связанным с потоком, в точке объявления. Таким образом, они всегда будут получать значения по умолчанию. Для ссылочных типов это означает null, а для типов значений — эквивалент установки всех бит хранилища в 0.

При первом доступе к специфичному для потока элементу его можно проверять на равенство null и соответствующим образом создавать экземпляр. Самый чистый способ достижения этого — всегда обращаться к локальному по отношению к потоку элементу через статическое свойство.

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

Если бы среда CLR попыталась “исправить” эту проблему, она стала бы без сомнения менее эффективной, поскольку пришлось бы проверять, осуществляется ли доступ к полю первый раз, и если это так — вызывать код инициализации. Если вы задумаетесь об этом, то поймете, что все намного сложнее, чем кажется на первый взгляд.

Существует и другой способ организации локального хранилища потока, не предполагающий декорирование статической переменной атрибутом. Локальное хранилище потока можно распределять динамически, используя либо метод Thread. AllocateDataSlot, либо Thread.AllocateNamedDataSlot.

Эти методы будут применяться, если заранее не известно, сколько специфичных для потока элементов понадобится до момента выполнения.

В противном случае обычно намного проще использовать метод статического поля. При вызове AllocateDataSlot во всех потоках распределяется новый элемент для хранения ссылки на экземпляр типа System.Object.

Метод AllocateDataSlot возвращает дескриптор в форме экземпляра объекта LocalDataStoreSlot. Обращаться к этому месту в памяти можно с помощью методов GetData и SetData потока. Рассмотрим следующую модификацию предыдущего примера:

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
using System;
using System.Threading;
public class TLSClass
{
static TLSClass ()  {
tlsSlot = Thread.AllocateDataSlot();
}
private TLSClass ()  {
Console.WriteLine ( "Создание TLSClass" );
}
public static TLSClass TlsSlot { get {
Object obj = Thread.GetData( tlsSlot ); 
if ( obj == null )  {
obj = new TLSClass ();
Thread.SetData ( tlsSlot, obj );
}
return (TLSClass) obj;
}
}
private static LocalDataStoreSlot tlsSlot = null;
}
public class EntryPoint {
private static void ThreadFunc ()  {
Console.WriteLine ( "Поток {0} запускается...".
Thread.CurrentThread.ManagedThreadld ) ; 
Console.WriteLine( "tlsdata for this thread is \"{0}\"",
TLSClass.TlsSlot ) ; 
Console.WriteLine( "Поток {0} завершается".
Thread.CurrentThread.ManagedThreadld ) ;
}
static void Main()  { 
Thread threadl = new Thread( new ThreadStart(EntryPoint.ThreadFunc)  ); 
Thread thread2 = new Thread( new ThreadStart(EntryPoint.ThreadFunc)  ); 
threadl.Start(); 
thread2.Start() ;
}

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

Таким образом, элемент выделяется для всех потоков в точке, где исполняющая среда инициализирует тип для использования. Обратите внимание, что в средстве доступа свойства класса TLSClass элемент проверяется на равенство null. При распределении элемента с применением AllocateDataSlot он инициализируется null для каждого потока.

Обращаться к специфичному для потока хранилищу может быть удобно по строковому имени, а не через ссылку на экземпляр LocalDataStoreSlot. Для этого элементTLS должен быть создан с использованием Thread.AllocateNamedDataSlot.

Однако при этом следует разумно выбирать уникальное имя, чтобы применение того же самого имени еще где-либо в коде не привело к нежелательным эффектам. В качестве имени для элемента можно применить, например, строковое представление глобально уникального идентификатора GUID.

Для доступа к элементу вызывается метод GetNameDataSlot, который просто транслирует строку в экземпляр LocalDataStoreSlot. Необходимые сведения по именованию элементов локальных хранилищ потоков даны в документации MSDN.

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

Вы можете следить за любыми ответами на эту запись через RSS 2.0 ленту. Вы можете оставить ответ, или trackback с вашего собственного сайта.

Оставьте отзыв

XHTML: Вы можете использовать следующие теги: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

 
Rambler's Top100