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

очередь пула служит метод QueueUserWorkItem(). Он принимает делегат типа WaitCallback1 и, возможно, аргумент инкапсулируемого метода.

public static void Main()

{

ThreadPool.QueueUserWorkItem(Go); ThreadPool.QueueUserWorkItem(Go, 123); Console.ReadLine();

}

private static void Go(object data)

{

Console.WriteLine("Hello from the thread pool! " + data);

}

32. Синхронизация потоков

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

32.1. Критические секции

Рассмотрим следующий класс:

public class ThreadUnsafe

{

private static int x, y;

public void Go()

{

if (y != 0)

{

Console.WriteLine(x/y);

}

y = 0;

}

}

Этот класс небезопасен с точки зрения многопоточного доступа к данным. Если вызвать метод Go() в разных потоках одновременно, может возникнуть ошибка деления на ноль, так как поле y будет обнулено в одном потоке как раз между проверкой условия y != 0 и вызовом Console.WriteLine() в другом потоке. Чтобы сделать код потокобезопасным, необходимо гарантировать выполнение операторов, составляющих тело метода Go(), только одним потоком в любой момент времени. Такие блоки кода называются критическими секциями.

Для организации критических секций платформа .NET предлагает статиче-

ский класс System.Threading.Monitor. Метод Monitor.Enter() определяет вход

1 public delegate void WaitCallback(object state);

126

в критическую секцию, а метод Monitor.Exit() – выход из секции. Вход и выход должны выполняться в одном и том же потоке. Аргументами методов является объект-идентификатор критической секции.

Изменим метод Go(), чтобы сделать его потокобезопасным:

public class ThreadSafe

{

// объект locker будет идентификатором критической секции private static readonly object locker = new object(); private static int x, y;

public

void Go()

 

{

 

 

Monitor.Enter(locker);

// вход в критическую секцию

if

(y != 0)

 

{

 

 

Console.WriteLine(x/y);

}

y = 0;

Monitor.Exit(locker); // выход из критической секции

}

}

Рис. 16 демонстрирует поведение двух потоков, работающих с одной критической секцией.

время

Thread #1

Thread #2

 

Monitor.Enter(locker);

 

Monitor.Enter(locker);

Monitor.Exit(locker);

Monitor.Enter(locker);

Monitor.Exit(locker);

Monitor.Exit(locker);

Рис. 16. Два потока работают с одной критической секцией.

Метод Monitor.Enter() имеет перегруженную версию, с ref-параметром типа bool. Если вход в критическую секцию был выполнен успешно, этот параметр возвращается как true. Если организовать критическую секцию не уда-

127

лось (например, по причине недостатка памяти), параметр возвращается как false, а метод Enter() выбрасывает исключение.

Язык C# содержит оператор lock, маскирующий вызовы методов

Monitor.Enter() и Monitor.Exit(). Синтаксис оператора lock следующий:

lock (выражение) вложенный-оператор

Здесь выражение должно иметь ссылочный тип и задаёт идентификатор критической секции. Часто в качестве выражения записывают поле или переменную, на которую накладывается блокировка. Оператор lock вида lock (x) эквивалентен следующему коду:

bool lockWasTaken = false; try

{

Monitor.Enter(x, ref lockWasTaken);

// здесь размещается вложенный оператор lock

}

finally

{

if (lockWasTaken)

{

Monitor.Exit(x);

}

}

Перепишем метод Go() класса ThreadSafe, используя оператор lock:

public void Go()

{

lock (locker)

{

if (y != 0)

{

Console.WriteLine(x/y);

}

y = 0;

}

}

Класс System.Threading.Mutex (мьютекс) подобен классу Monitor, но поз-

воляет организовать критическую секцию для нескольких процессов. Применяя Mutex, нужно вызвать метод WaitOne() для входа в критическую секцию, а метод ReleaseMutex() – для выхода из неё (выход может быть произведён только в том же потоке выполнения, что и вход).

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

128

using System;

using System.Threading;

public class OneAtATimePlease

{

public static void Main()

{

// имя мьютекса должно быть уникально для компьютера using (var mutex = new Mutex(false, "RandomString"))

{

//пытаемся войти в критическую секцию в течение 3 сек.

//ожидаем 3 секунды на случай, если другой экземпляр

//приложения в процессе завершения работы

if (!mutex.WaitOne(TimeSpan.FromSeconds(3), false))

{

Console.WriteLine("Another instance is running"); return;

}

RunProgram();

}

}

private static void RunProgram()

{

Console.WriteLine("Running (press Enter to exit)"); Console.ReadLine();

}

}

Семафор – это объект синхронизации, позволяющий войти в заданный участок кода не более чем N потокам (N – ёмкость семафора). Аналогией семафора является охранник у входа в клуб с фиксированным количеством мест. Новые посетители попадают в заполненный клуб, только если из него кто-то ушёл. Семафор с ёмкостью, равной единице, аналогичен монитору или мьютексу, однако получение и снятие блокировки в случае семафора может выполняться из разных потоков.

Для организации семафоров платформа .NET предлагает классы Semaphore и SemaphoreSlim из пространства имён System.Threading. Первый класс применяется для синхронизации между процессами, второй работает только в рамках одного процесса. Метод Wait() этих классов выполняет получение блокировки, а метод Release() – снятие блокировки.

using System;

using System.Threading;

public class TheClub

{

// ёмкость семафора равна 2

129

private static SemaphoreSlim s = new SemaphoreSlim(2);

public static void Main()

{

for (var i = 1; i <= 4; i++)

{

new Thread(Enter).Start(i);

}

}

private static void Enter(object id)

{

Console.WriteLine(id + " wants to enter"); s.Wait();

Console.WriteLine(id + " is in!");

//

только два потока

Thread.Sleep(1000 * (int) id);

//

могут одновременно

Console.WriteLine(id + " is leaving"); //

выполнять этот код

s.Release();

}

}

время

Thread #1

Thread #2

Thread #3

Thread #4

 

s.Wait();

s.Wait();

s.Wait();

s.Wait();

s.Release();

 

 

 

 

 

 

 

 

s.Release();

 

 

 

 

s.Release();

s.Release();

Рис. 17. Демонстрация работы семафора в классе TheClub.

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

ReaderWriterLockSlim. Его экземплярные методы EnterReadLock() и

ExitReadLock() задают секцию чтения ресурса, а методы EnterWriteLock() и ExitWriteLock() – секцию записи ресурса. В следующем примере при помощи ReaderWriterLockSlim построен класс, реализующий простой кэш с поддержкой многопоточности:

130

public class SynchronizedCache

{

private Dictionary<int, string> cache =

 

new Dictionary<int, string>();

private ReaderWriterLockSlim locker =

 

new ReaderWriterLockSlim();

public string Read(int key)

 

{

 

locker.EnterReadLock();

// секция чтения началась

try

 

{

 

return cache[key];

// читать могут несколько потоков

}

 

finally

{

locker.ExitReadLock(); // секция чтения закончилась

}

}

public void Add(int key, string value)

{

 

locker.EnterWriteLock();

// секция записи началась

try

 

{

 

cache.Add(key, value);

 

}

 

finally

 

{

 

locker.ExitWriteLock();

// секция запись закончилась

}

 

}

 

public bool AddWithTimeout(int key, string value, int timeout)

{

if (locker.TryEnterWriteLock(timeout))

// таймаут входа

{

 

try

 

{

 

cache.Add(key, value);

 

}

 

finally

 

{

 

locker.ExitWriteLock();

 

}

 

return true;

 

}

 

return false;

 

}

}

131