![]() Главная страница Случайная страница КАТЕГОРИИ: АвтомобилиАстрономияБиологияГеографияДом и садДругие языкиДругоеИнформатикаИсторияКультураЛитератураЛогикаМатематикаМедицинаМеталлургияМеханикаОбразованиеОхрана трудаПедагогикаПолитикаПравоПсихологияРелигияРиторикаСоциологияСпортСтроительствоТехнологияТуризмФизикаФилософияФинансыХимияЧерчениеЭкологияЭкономикаЭлектроника |
Критические секции
Рассмотрим следующий класс: 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; } }
|