Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Базовые технологии платформы .NET.pdf
Скачиваний:
66
Добавлен:
11.05.2015
Размер:
1.73 Mб
Скачать

32.2. Синхронизация на основе подачи сигналов

При использовании синхронизации на основе подачи сигналов один поток получает уведомления от другого потока. Обычно уведомления используются для возобновления работы заблокированного потока. Для реализации данного подхода платформа .NET предлагает классы AutoResetEvent, ManualResetEvent,

ManualResetEventSlim, CountdownEvent и Barrier (все они размещены в пространстве имён System.Threading).

Классы AutoResetEvent, ManualResetEvent, ManualResetEventSlim унасле-

дованы от класса EventWaitHandle. Имея доступ к объекту EventWaitHandle, поток может вызвать его метод WaitOne(), чтобы остановиться и ждать сигнала. Для отправки сигнала применяется вызов метода Set(). Если используются

ManualResetEvent и ManualResetEventSlim, все ожидающие потоки освобожда-

ются и продолжают выполнение. При использовании AutoResetEvent ожидающие потоки освобождаются и запускаются последовательно, на манер очереди. Аналогией для AutoResetEvent является турникет, пропускающий людей по одному, а ManualResetEvent подобен воротам, которые или закрыты, или открыты.

 

a = new AutoResetEvent(false);

 

время

Thread #1

Thread #2

Thread #3

a.WaitOne();

a.WaitOne();

a.Set();

a.Set();

Рис. 18. Пример работы с объектом AutoResetEvent.

Класс CountdownEvent позволяет организовать счётчик уведомлений. Конструктор класса принимает в качестве аргумента начальное значение счётчика. Вызов экземплярного метода Signal() уменьшает значение счётчика на единицу. Метод Wait() блокирует поток, пока счётчик не станет равным нулю.

private static CountdownEvent counter = new CountdownEvent(3);

public static void Main()

{

new Thread(SaySomething).Start("I am thread 1"); new Thread(SaySomething).Start("I am thread 2"); new Thread(SaySomething).Start("I am thread 3");

132

// ждём, пока метод Signal() не вызовется три раза counter.Wait();

Console.WriteLine("All threads have finished speaking!");

}

private static void SaySomething(object thing)

{

Thread.Sleep(1000); Console.WriteLine(thing); counter.Signal();

}

Объект класса Barrier организует для нескольких потоков точку встречи во времени. Чтобы использовать Barrier, нужно вызвать его конструктор, передав количество встречающихся потоков. Затем отдельные потоки вызывают экземплярный метод Barrier.SignalAndWait(), чтобы приостановиться и продолжить выполнение после совместной встречи.

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

using System;

using System.Threading;

public class BarrierExample

{

private static readonly Barrier _barrier = new Barrier(3);

public static void Main()

{

new Thread(Speak).Start(); new Thread(Speak).Start(); new Thread(Speak).Start();

// вывод: 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4

Console.ReadLine();

}

private static void Speak()

{

for (var i = 0; i < 5; i++)

{

Console.Write(i + " "); _barrier.SignalAndWait();

}

}

}

133

32.3. Неблокирующие средства синхронизации

Неблокирующие средства синхронизации позволяют осуществлять сов-

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

Для оптимизации выполнения программы центральный процессор иногда применяет перестановку инструкций программы или их кэширование. Чтобы отменить подобную оптимизацию следует разместить в исходном коде барьеры памяти. Процессор не способен изменить порядок команд, чтобы инструкции до барьера, выполнялись после инструкций за барьером. Для установки барьера памяти нужно вызвать статический метод Thread.MemoryBarrier(). Язык C# содержит специальный модификатор volatile, применяемый при объявлении поля. Инструкции записи данных в такое не переставляются процессором в целях оптимизации.

Статический класс System.Threading.Interlocked имеет методы для инкремента, декремента и сложения аргументов типа int или long, а также методы присваивания значений числовым и ссылочным переменным. Каждый метод гарантировано выполняется как атомарная операция, без блокировки текущего потока выполнения.

int x = 10, y = 20;

 

 

 

 

Interlocked.Add(ref x, y);

// x

= x

+ y

Interlocked.Increment(ref x);

// x++

 

Interlocked.Exchange(ref x, y);

//

x

= y

 

Interlocked.CompareExchange(ref x, 50, y);

//

if (x

== y) x = 50

32.4. Разделение данных между потоками

Если некий метод запускается в нескольких потоках, только локальные переменные метода будут уникальными для потока. Поля объектов по умолчанию разделяются между всеми потоками. В пространстве имён System определён атрибут [ThreadStatic], применяемый к статическим полям. Если поле помечено таким атрибутом, то каждый поток будет содержать свой экземпляр поля. Для [ThreadStatic]-полей не рекомендуется делать инициализацию при объявлении, так как код инициализации выполнится только в одном потоке.

public class SomeClass

{

public static int SharedField = 25;

[ThreadStatic]

public static int NonSharedField;

}

Для создания неразделяемых статических полей можно использовать тип ThreadLocal<T>. Перегруженный конструктор ThreadLocal<T> принимает функцию инициализации поля. Значение поля хранится в свойстве Value.

134

public class Slot

{

private static readonly Random rnd = new Random();

private static int Shared = 25;

private static ThreadLocal<int> NonShared =

new ThreadLocal<int>(() => rnd.Next(1, 20));

public static void PrintData()

{

Console.WriteLine("Thread: {0} Shared: {1} NonShared: {2}", Thread.CurrentThread.Name,

Shared, NonShared.Value);

}

}

public class MainClass

{

public static void Main()

{

// для тестирования запускаем три потока

new Thread(Slot.PrintData) {Name = "First"}.Start(); new Thread(Slot.PrintData) {Name = "Second"}.Start(); new Thread(Slot.PrintData) {Name = "Third"}.Start(); Console.ReadLine();

}

}

Отметим, что класс Thread имеет статические методы AllocateDataSlot(),

AllocateNamedDataSlot(), GetNamedDataSlot(), FreeNamedDataSlot(), GetData(),

SetData(), которые предназначены для работы с локальными хранилищами данных потока. Эти локальные хранилища могут рассматриваться как альтернатива неразделяемым статическим полям.

Распространённый шаблон при разработке многопоточных приложений – неизменяемый объект (immutable object). После создания такой объект допускает только чтение своих полей, но не запись. Приведём пример класса, объекты которого являются неизменяемыми:

public class ProgressStatus

{

public readonly int PercentComplete; public readonly string StatusMessage;

public ProgressStatus(int percentComplete, string statusMessage)

{

PercentComplete = percentComplete; StatusMessage = statusMessage;

}

}

135