Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Базовые технологии платформы .NET.docx
Скачиваний:
11
Добавлен:
18.08.2019
Размер:
423.24 Кб
Скачать

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() определяет вход в критическую секцию, а метод 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 демонстрирует поведение двух потоков, работающих с одной критической секцией.

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

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

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

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

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();

}

}

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

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

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;

}

}