четверг, 26 августа 2010 г.

[Перевод] Джозеф Албахари. Работа с потоками в C#. Часть 4

Это перевод четвертой части “Part 4. Advanced Threading” из замечательной серии статей Джозефа Албахари (Joseph Albahari)  о работе с потоками в языке C# и, как и при переводе третьей части, сейчас я публикую только, что появилось с выходом нового издания книги  “C# 4.0 in Nutshell: The Definitive Reference” и еще не опубликовано на русском языке на rsdn.ru.

Класс Barrier

Класс Barrier – это сигнальная конструкция, которая появилась в .Net Framework 4.0. Он реализует барьер потока исполнения, который позволяет множеству потоков встречаться в определенном месте во времени. Этот класс очень быстрый и эффективный, поскольку построен на основе Wait, Pulse и спин-блокировок.

Для использования этого класса необходимо:

  1. Создать экземпляр, указав количество потоков, которые будут встречаться одновременно(вы можете изменить это значение позднее путем вызова методов AddParticipants/RemoveParticipants).

Создание экземпляра класса Barrier со значением 3 приведет к тому, что вызов метода SignalAndWait блокируется до тех пор, пока этот метод не будет вызван трижды. Но, в отличие от CountdownEvent, он автоматически запускает этот процесс заново: вызов метода SignalAndWait снова блокируется до тех пор, пока он не вызовется трижды. Это позволяет идти нескольким потокам «в ногу» во время обработки набора задач.

clip_image001

В следующем примере, каждый из трех потоков выводит числа от 0 до 4, шагая в ногу с другими потоками:

static Barrier _barrier = new Barrier (3);
 
static void Main()
{
  new Thread (Speak).Start();
  new Thread (Speak).Start();
  new Thread (Speak).Start();
}
 
static void Speak()
{
  for (int i = 0; i < 5; i++)
  {
    Console.Write (i + " ");
    _barrier.SignalAndWait();
  }
}
0 0 0 1 1 1 2 2 2 3 3 3 4 4 4

Очень полезной возможностью класса Barrier является возможность указать при его конструировании постдействие (post-phase action). Этот делегат выполняется после того, как метод SignalAndWait вызван n раз, но перед тем, как потоки будут разблокированы. Если в нашем примере мы создадим экземпляр класса барьера следующим образом:

static Barrier _barrier = new Barrier (3, barrier => Console.WriteLine());

то вывод на экран будет таким:

0 0 0 
1 1 1
2 2 2
3 3 3
4 4 4

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

Блокировки чтения/записи

Довольно часто экземпляры некоторых типов являются потокобезопасными при параллельных операциях чтениях, но не являются таковыми при параллельных обновлениях (при параллельных операциях чтения и обновления). Это также относится к таким ресурсам, как файлы. И хотя защита экземпляров таких типов простой эксклюзивной блокировкой для всех режимов доступа, решает проблему параллельного доступа, это может необоснованно ограничить параллелизм, когда к экземпляру обращаются много читателей при небольшом количестве обновлений. Типичным примером, когда может возникнуть такая ситуация – это сервер бизнес-приложений, в котором часто используемые данные кэшируются в статических полях для последующего быстрого доступа. Класс ReaderWriteLockSlim разработан для обеспечения максимальной доступности ресурса именно для этого случая.

Класс ReaderWriterLockSlim появился в .Net Framework 3.5 и предназначен для замены старого «тяжеловесного» класса ReaderWriterLock. Этот класс обладает аналогичной функциональности, но он в несколько раз медленнее и содержит ошибки проектирования в механизме обработки обновления блокировки.

Оба эти класса предусматривают два типа блокировки – блокировка чтения и блокировка записи:

  • Блокировка записи является полностью эксклюзивной.
  • Блокировка чтения совместима с другими блокировками чтения.

So, a thread holding a write lock blocks all other threads trying to obtain a read or write lock (and vice versa). But if no thread holds a write lock, any number of threads may concurrently obtain a read lock.

ReaderWriterLockSlim defines the following methods for obtaining and releasing read/write locks:

Т.е. поток, захвативший блокировку записи, блокирует все другие потоки, пытающиеся получить блокировку чтения или записи (и наоборот). Но если ни один поток не держит блокировку записи, любое количество потоков могут одновременно захватить блокировку чтения.

В классе ReaderWriterLockSLim определены следующие методы для захвата и освобождения блокировок чтения/записи:

public void EnterReadLock();
public void ExitReadLock();
public void EnterWriteLock();
public void ExitWriteLock();

В дополнение к этим методом, существуют также “Try” версии всех EnterXXX методов, которые принимают тайм-аут в качестве параметра, аналогично методу Monitor.TryEnter (таймауты могут часто возникать при работе с высокой степенью конкуренции). Класс ReaderWriterLock содержит аналогичные методы с именами AcquireXXX и ReleaseXXX, которые в случае возникновения тайм-аута генерируют ApplicationException, а не возвращают false.

Следующий код демонстрирует использование класса ReaderWriterLockSlim. Три потока постоянно читают список, в то время как два других потока добавляют случайное значение в него каждую секунду. Блокировка чтения защищает читателей списка, а блокировка записи защищает писателей.

class SlimDemo
{
    static ReaderWriterLockSlim _rw = new ReaderWriterLockSlim();
    static List<int> _items = new List<int>();
    static Random _rand = new Random();

    static void Main()
    {
        new Thread(Read).Start();
        new Thread(Read).Start();
        new Thread(Read).Start();

        new Thread(Write).Start("A");
        new Thread(Write).Start("B");
    }

    static void Read()
    {
        while (true)
        {
            _rw.EnterReadLock();
            foreach (int i in _items) Thread.Sleep(10);
            _rw.ExitReadLock();
        }
    }

    static void Write(object threadID)
    {
        while (true)
        {
            int newNumber = GetRandNum(100);
            _rw.EnterWriteLock();
            _items.Add(newNumber);
            _rw.ExitWriteLock();
            Console.WriteLine("Thread " + threadID + " added " + newNumber);
            Thread.Sleep(100);
        }
    }

    static int GetRandNum(int max) { lock (_rand) return _rand.Next(max); }
}

В промышленном коде вам следует добавить блоки try/finally для гарантии освобождения блокировок при генерации исключения.

Вот результат выполнения:

Thread B added 61
Thread A added 83
Thread B added 55
Thread A added 33
...

Класс ReaderWriterLockSlim позволяет выполнять больше параллельных операций чтения, нежели обычная блокировка. Мы можем продемонстрировать это путем добавления следующей строки в метод Write в начале цикла while:

Console.WriteLine (_rw.CurrentReadCount + " concurrent readers");

Этот код практически всегда выводит “3 concurrent readers” (методы Read проводят основное время выполнения внутри циклов foreach). Помимо свойства ConcurrentReadCount класс ReaderWriterLockSlim предоставляет следующие свойства для мониторинга блокировок:

public bool IsReadLockHeld            { get; }
public bool IsUpgradeableReadLockHeld { get; }
public bool IsWriteLockHeld           { get; }
 
public int  WaitingReadCount          { get; }
public int  WaitingUpgradeCount       { get; }
public int  WaitingWriteCount         { get; }
 
public int  RecursiveReadCount        { get; }
public int  RecursiveUpgradeCount     { get; }
public int  RecursiveWriteCount       { get; }
Обновляемые блокировки и рекурсия

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

  1. Получить блокировку чтения
  2. Проверить, если элемент уже находится в списке, освободить блокировку и завершить выполнение функции
  3. Освободить блокировку чтения
  4. Захватить блокировку записи
  5. Добавить элемент

Проблема в том, что другой поток может вклиниться и модифицировать список (например, добавить такой же элемент) между шагами 3 и 4. ReaderWriterLockSlim решает эту задачу с помощью третьего типа блокировки, называемой обновляемой блокировкой (upgradeable lock). Обновляемая блокировка аналогична блокировке чтения за исключением того, что позднее с помощью атомарной операции она может быть расширена до блокировки записи. Вот как ею пользоваться:

  1. Вызвать метод EnterUpgradeableReadLock
  2. Выполнить операции, требующие доступ на чтение (например, проверить находится ли некоторый элемент в списке).
  3. Вызвать EnterWriteLock (это преобразует обновляемую блокировку в блокировку записи).
  4. Выполнить операции, требующие доступ на запись (например, добавить элемент в список).
  5. Вызвать ExitWriteLock (это преобразует блокировку записи обратно к обновляемой блокировке).
  6. Выполнить любые другие операции, требующие доступ на чтение
  7. Вызвать ExitUpgradeableReadLock.

С точки зрения вызывающего кода это очень похоже на вложенную или рекурсивную блокировку. Но функционально, на третьем шаге, ReaderWriteLockSlim освобождает блокировку чтения и автоматически получает новую блокировку записи.

Существует еще одно важное отличие между обновляемыми блокировками и блокировками чтения. В то время как обновляемая блокировка может совместно существовать с любым количеством блокировок чтения, в один момент времени может быть получена только одна обновляемая блокировка. Это предотвращает взаимоблокировку при преобразовании блокировок во время сериализации нескольких одновременных обновляемых блокировок, аналогично тому, как обновляются блокировки в SQL Server:

SQL Server

ReaderWriterLockSlim

Разделяемая блокировка

Блокировка на чтение

Эксклюзивная блокировка

Блокировка на запись

Обновляемая блокировка

Обновляемая блокировка

Мы можем показать использование обновляемой блокировки, модифицировав метод Write предыдущего примера таким образом, чтобы добавлять значение в список только тогда, когда его там еще нет:

while (true)
{
  int newNumber = GetRandNum (100);
  _rw.EnterUpgradeableReadLock();
  if (!_items.Contains (newNumber))
  {
    _rw.EnterWriteLock();
    _items.Add (newNumber);
    _rw.ExitWriteLock();
    Console.WriteLine ("Thread " + threadID + " added " + newNumber);
  }
  _rw.ExitUpgradeableReadLock();
  Thread.Sleep (100);
}

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

Рекурсия блокировок

Обычно, вложенные или рекурсивные блокировки с классом ReaderWriteLockSlim запрещены. Вот почему следующий код генерирует исключение:

var rw = new ReaderWriterLockSlim();
rw.EnterReadLock();
rw.EnterReadLock();
rw.ExitReadLock();
rw.ExitReadLock();

Однако этот код выполняется без ошибок, если создать объект ReaderWriteLockSlim следующим образом:

var rw = new ReaderWriterLockSlim (LockRecursionPolicy.SupportsRecursion);

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

rw.EnterWriteLock();
rw.EnterReadLock();
Console.WriteLine (rw.IsReadLockHeld);     // True
Console.WriteLine (rw.IsWriteLockHeld);    // True
rw.ExitReadLock();
rw.ExitWriteLock();

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

      Блокировка чтения  Обновляемая блокировка  Блокировка записи

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

понедельник, 23 августа 2010 г.

[Перевод] Джозеф Албахари. Работа с потоками в C#. Часть 3

Не так давно вышло новое издание замечательной книги Джозефа Албахари (Josepth Albahari) “C# 4.0 in Nutshell: The Definitive Reference”, которая, как подсказывает название, обновилась с учетом нововведений, появившихся в последней версии языка программирования C# и платформы .Net. Еще до перевода этой книги на русский язык, одна из самых интересных и полезных частей книги – о многопоточном программировании на языке C# – была переведена Алексеем Кирюшкиным (a.k.a. Odi$$ey) и опубликована на сайте rsdn.ru (Часть 1 и Часть 2).

В связи с тем, что в новой версии .Net Framework появилось много новых полезных конструкций, призванных упростить работу с многопоточностью, эта часть книги была существенно расширена и переработана. Так, во-первых, добавилась новая часть “Part 5. Parallel Programming”, а во-вторых, части 3 и 4 подверглись существенным переработкам.

С любезного разрешения автора я решил продолжить работу, начатую Алексеем и перевести на русский язык 5-ю часть, а также те изменения, которые появились в частях предыдущих. Я не хочу здесь заново публиковать то, что уже опубликовано на русский язык (хотя, я думаю, что в скором времени полную версию предыдущих частей можно будет почитать на rsdn.ru), и оставлю здесь только те новшества, которые появились в новой версии .Net Framework. Так что, если вы уже знакомы с конструкциями многопоточности в предыдущих версиях платформы, это позволит вам не тратить лишнее время на то, что вы и так уже знаете, ну, а если нет – то лучше начать с чтения предыдущего издания этих частей, опубликованных на rsdn.ru.

Часть 3. Работа с потоками

Безопасная отмена операций

Как мы видели в предыдущем разделе, вызов метода Abort в большинстве случаев является опасным. В таком случае, альтернативой является реализация кооперативного паттерна(cooperative pattern), когда рабочий поток периодически проверяет флаг, указывающий на необходимость прекращения его работы (как в классе BackgroundWorker). Для отмены операции, ее инициатор просто устанавливает этот флаг, и затем ожидает завершения выполнения рабочего потока. Вспомогательный класс BackgroundWorker реализует этот паттерн отмены на основе флага (flag-based cancelation pattern), и вы легко можете реализовать его самостоятельно.

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

class RulyCanceler
{
    object _cancelLocker = new object();
    bool _cancelRequest;
    public bool IsCancellationRequested
    {
        get { lock (_cancelLocker) return _cancelRequest; }
    }

    public void Cancel() { lock (_cancelLocker) _cancelRequest = true; }

    public void ThrowIfCancellationRequested()
    {
        if (IsCancellationRequested) throw new OperationCanceledException();
    }
}

Класс OperationCanceledException – это класс в составе .NET Framework предназначенный именно для этих целей. Хотя для этих целей подойдет и любой другой класс исключения.

Мы можем использовать его следующим образом:

class Test
{
    static void Main()
    {
        var canceler = new RulyCanceler();
        new Thread(() =>
        {
            try { Work(canceler); }
            catch (OperationCanceledException)
            {
                Console.WriteLine("Canceled!");
            }
        }).Start();
        Thread.Sleep(1000);
        canceler.Cancel();               // безопасная отмена.
    }

    static void Work(RulyCanceler c)
    {
        while (true)
        {
            c.ThrowIfCancellationRequested();
            // ...
            try { OtherMethod(c); }
            finally { /* необходимая очистка ресурсов */ }
        }
    }

    static void OtherMethod(RulyCanceler c)
    {
        // Выполняем некоторые действия ...
        c.ThrowIfCancellationRequested();
    }
}

Мы можем упростить наш пример, убрав класс RulyCanceler и добавив вместо него в классе Test статическое булево поле _cancelRequest. Однако, в случае исполнения метода Work несколькими потоками одновременно, установка флага _cancelRequest в true прервет выполнение всех рабочих потоков. Так что класс RulyCanceler является полезной абстракцией. Недостатком этого подхода является то, что глядя на сигнатуру метода Work не сразу понятно его предназначение:

static void Work (RulyCanceler c)
Может ли метод Cancel объекта RulyCanceler быть вызван из самого метода Work? В данном случае, ответ – нет, так что было бы хорошо, если бы система типов поддерживала бы это. .Net Framework 4.0 для этой цели предоставляет маркеры отмены (cancelation tokens).
Маркеры отмены

В .Net Framework 4.0 существует два типа, формализующие этот только что продемонстрированный кооперативный паттерн отмены: CancellationTokenSource и CancellationToken. Эти два типа работают в тандеме:

  • В классе CancellationTokenSource определен метод Cancel
  • В классе CancellationToken определено свойство IsCancellationRequested и метод ThrowIfCancellationRequested

Вместе, эти классы являются более сложной версией класса RulyCanceler, представленного в предыдущем примере. Но поскольку типы разделены, вы можете разделить возможность отмены операции от возможности проверки флага отмены.

Для использования этих типов, создайте вначале экземпляр класса CancellationTokenSource:

var cancelSource = new CancellationTokenSource();

Затем, передайте свойство Token этого объекта в метод, поддерживающий отмену:

new Thread (() => Work (cancelSource.Token)).Start();

Вот как будет определен метод Work:

void Work (CancellationToken cancelToken)
{
  cancelToken.ThrowIfCancellationRequested();
  ...
}

Когда вы хотите отменить выполнение метода, просто вызовите метод Cancel объекта cancelSource.

CancellationToken на самом деле является структурой, хотя ведет себя как класс. При копировании, копия ведет себя точно также и указывает на исходный объект CancellationTokenSource.

В структуре CancellationToken определены два дополнительных полезных метода. Первый – это WaitHandle, возвращающий дескриптор ожидания (wait handle), который переходит в сигнальное состояние при отмене. Второй – Register, который позволяет зарегистрировать метод обратного вызова и который будет вызван при отмене.

Маркеры отмены используются в самом .NET Framework, в частности, в следующих классах:

Большинство этих классов используют маркеры отмены в методах Wait. Например, если вы вызовете метод Wait объекта класса ManualResetEventSlim и укажете маркер отмены, другой класс сможет вызвать метод Cancel и отменить ожидание. Этот метод чище и безопаснее, нежели вызывать метод Interrupt заблокированного потока.

Отложенная инициализация

Типичной проблемой в многопоточной среде является отложенная инициализация разделяемого (shared) поля потокобезопасным способом. Такая потребность возникает, когда создание экземпляра некоторого типа является дорогостоящим:

class Foo
{
  public readonly Expensive Expensive = new Expensive();
  ...
}
class Expensive/* Предположим, что создание этого объекта является дорогим */ }

Проблема с этим кодом в том, что создание экземпляра класса Foo приводит к затратам на  создания экземпляра класса Expensive не зависимо от того, используется это поле или нет. Очевидным решением этой задачи является создание экземпляра по требованию:

class Foo
{
  Expensive _expensive;
  public Expensive Expensive       // Создать экземпляр класса Expensive отложенно
  {
    get
    {
      if (_expensive == null) _expensive = new Expensive();
      return _expensive;
    }
  }
  ...
}

В таком случае возникает вопрос: является ли этот код потокобезопасным? Даже не смотря на тот факт, что доступ к полю _expensive вне блока lock без использования барьеров памяти, давайте подумаем о том, что будет, если два потока обратятся к этому свойству одновременно. При выполнении каждого потока условие if будет выполнено и каждый поток получит разные экземпляры класса Expensive. Это может привести к тонким ошибкам, так что, в общем случае мы можем сказать, что этот код не является потокобезопасный.

Решение проблемы состоит в использовании блокировки вокруг проверки и инициализации объекта:

Expensive _expensive;
readonly object _expenseLock = new object();
 
public Expensive Expensive
{
  get
  {
    lock (_expenseLock)
    {
      if (_expensive == null) _expensive = new Expensive();
      return _expensive;
    }
  }
}
Lazy<T>

В .Net Framework 4.0 существует новый класс, под названием Lazy<T> для упрощения отложенной инициализации. Если объект этого класса будет создан с аргументом, равным true, он будет реализовывать только что описанный паттерн инициализации.

На самом деле класс Lazy<T> реализует более эффективную версию этого паттерна, под названием блокировка с двойной проверкой (double-checked locking). При блокировке с двойной проверкой выполняется дополнительная операция volatile чтение для избегания захвата блокировки в случае, если объект уже проинициализирован.

Для использования класса Lazy<T>, создайте класс с делегатом[S7] -фабрикой, который будет определять способ создания нового значения, и в качестве второго параметра передайте true. Затем, вы можете использовать это значение с помощью свойства Value:

Lazy<Expensive> _expensive = new Lazy<Expensive>
  (() => new Expensive(), true);
 
public Expensive Expensive { get { return _expensive.Value; } }

Если вы передадите false в конструктор класса Lasy<T>, тогда объект этого класса будет реализовывать потоконебезопасный паттерн отложенной инициализации, рассмотренный в начале этого раздела. Это имеет смысл, если вы будете применять Lazy<T> в однопоточноном контексте.

LazyInitializer

Класс LazyInitializer – это статический класс, который работает в точности как Lazy<T> за исключением следующего:

  • Его функциональность реализована в виде статического метода, который работает непосредственно с полем внутри вашего типа. Это устраняет дополнительный уровень косвенности и улучшает производительность в случае необходимости чрезвычайной оптимизации.
  • Он предоставляет еще один способ инициализации, при котором во время инициализации возможны гонки нескольких потоков.
Expensive _expensive;
public Expensive Expensive
{
  get   // Реализует блокировку с двойной проверкой
  {
    LazyInitializer.EnsureInitialized (ref _expensive,
                                      () => new Expensive());
    return _expensive;
  }
}

Вы также можете передать другой аргумент для того, чтобы указать на возможность гонок нескольких потоков, борющихся между собой за инициализацию ресурса. Этот способ кажется аналогичным нашему исходному потоконебезопасному примеру, за исключением того, что в данном случае первый поток «всегда выиграет», так что в конечном итоге всегда будет получен только один экземпляр. Преимущество этого подхода состоит в том, что он даже быстрее (на многоядерных компьютерах) чем блокировка с двойной проверкой, поскольку он может быть полностью реализован без блокировок. Такая чрезвычайная оптимизация требуется достаточно редко и она не обходится бесплатно:

  • Этот способ медленнее, когда количество потоков, борющихся за инициализацию, превосходит количество ядер.
  • Он потенциально расходует ресурсы процессора понапрасну, выполняя избыточную инициализацию.
  • Логика инициализации должна быть потокобезопасной (например, будет потоконебезопасно, если конструктор класса Expensive будет записывать что-либо в статические поля).
  • Если инициализатор создает объект, требующий очистки ресурсов (disposable object), ресурсы «лишнего» объекта не будут освобождены без дополнительной логики.

Для справки, вот как реализовывается блокировка с двойной проверкой:

volatile Expensive _expensive;
public Expensive Expensive
{
  get
  {
    if (_expensive == null)
    {
      var expensive = new Expensive();
      lock (_expenseLock) if (_expensive == null) _expensive = expensive;
    }
    return _expensive;
  }
}

А вот как реализован способ с возможностью гонок при инициализации:

volatile Expensive _expensive;
public Expensive Expensive
{
  get
  {
    if (_expensive == null)
    {
      var instance = new Expensive();
      Interlocked.CompareExchange (ref _expensive, instance, null);
    }
    return _expensive;
  }
}
Локальное хранилище потока

В этой статье делается серьезный акцент на конструкции синхронизации и на проблемы, который возникают при одновременном доступе различных потоков к одним и тем же данным. Однако иногда вы хотите иметь изолированные данные, гарантируя, что у каждого потока будет их отдельная копия. Локальные переменные предназначены именно для этого, но они применимы только для временных данных (transient data).

Решением является использование локального хранилища потока (thread-local storage). Вы можете увидеть противоречие в этом требовании: данные, которые вы хотите изолировать для каждого отдельного потока, являются временными по своей природе. Главная цель этих хранилищ – хранение «нетиповых» (“out-of-band”) данных, которые являются частью инфраструктуры приложения, такие как сообщения, транзакции и маркеры безопасности (security tokens). Передача таких данных через параметры методов является чрезвычайно грубым решением и усложняет все, кроме этих методов; хранение же такой информации в обыкновенных статических полях означает разделение этих данных всеми потоками.

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

Существует три способа реализации локального хранилища потока.

[ThreadStatic]

Самым простым способом использования локального хранилища потока является пометка  статического поля с помощью атрибута ThreadStatic:

[ThreadStatic] static int _x;

В этом случае каждый поток будет видеть независимую копию переменной _x.

К сожалению, атрибут [ThreadStatic] не работает с экземплярными полями (он просто ничего не делает в этом случае); а также нормально не работает с инициализаторами статических полей (field initializer), поскольку они выполняются только один раз при выполнении статического конструктора. Если вам нужно работать с экземплярными полями или значениями статических полей, отличных от значений по умолчанию, более подходящим выбором будет ThreadLocal<T>.

ThreadLocal<T>

Класс ThreadLocal<T> появился в .Net Framework 4.0 и он позволяет использовать локальное хранилище потока, как для статических, так и для экземплярных полей, а также позволяет задавать значение по умолчанию.

Вот как можно создать экземпляр ThreadLocal<int> со значением по умолчанию для каждого потока, равным 3:

static ThreadLocal<int> _x = new ThreadLocal<int> (() => 3);

Затем вы можете использовать свойство Value переменной _x для получения или установки значения, локального для каждого потока. Бонусом использования ThreadLocal является отложенная инициализация значений: фабричная функция выполняется при первом вызове (для каждого потока).

ThreadLocal<T> и экземплярные поля

Класс TrheadLocal<T> также полезен при работе с экземплярными полями и захваченными локальными переменными. Например, давайте рассмотрим задачу генерации случайных чисел в многопоточном окружении. Класс Random не потокобезопасен, поэтому нам либо нужно, либо воспользоваться блокировкой вокруг использования объекта класса Random (ограничивая одновременный доступ) либо генерировать объект класса Random для каждого потока. Класс ThreadLocal<T> легко решает вторую задачу:

var localRandom = new ThreadLocal<Random>(() => new Random());
Console.WriteLine (localRandom.Value.Next());

Наша фабричная функция для создания объекта Random немного упрощенная, поскольку конструктор без параметров класса Random основывается на системном времени для начальных значений. И это значение может быть одинаковым для двух разных объектов Random созданных с интервалом порядка 10 мс. Вот как это можно исправить:

var localRandom = new ThreadLocal<Random>
( () => new Random (Guid.NewGuid().GetHashCode()) );

Мы воспользуемся этим способом в 5-й части (см. пример параллельной проверки правописания в разделе “PLINQ”).

GetData and SetData

Третий подход основывается на использовании двух методов класса Thread: GetData и SetData. Эти методы сохраняют данные в специальных «слотах» потока. Метод Thread.GetData читает из  этого хранилища потока, а метод Thread.SetData – записывает туда. Для идентификации слота оба метода требуют объект LocalDataStoreSlot. Одна и та же ячейка может быть использована всеми потоками, но при этом каждый из них будет работать со своими собственными значениями. Вот пример:

class Test
{
  // Один и тот же объект LocalDataStoreSlot может использоваться всеми потоками.
  LocalDataStoreSlot _secSlot = Thread.GetNamedDataSlot ("securityLevel");
 
  // Это свойство содержит разное значение для каждого потока
  int SecurityLevel
  {
    get
    {
      object data = Thread.GetData (_secSlot);
      return data == null ? 0 : (int) data;    // null == поле неинициализировано
    }
    set { Thread.SetData (_secSlot, value); }
  }
  ...

В этом примере, мы вызываем Thread.GetNamedDataSlot, который создает новый именованный слот , что позволяет совместно использовать этот слот всему приложению. Создавая объект LocalDataStoreSlot явно и не задавая его имени, вы можете управлять областью видимостью слота самостоятельно:

class Test
{
  LocalDataStoreSlot _secSlot = new LocalDataStoreSlot();
  ...

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