Методы класса Monitor: Wait, Pulse и PulseAll




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

Оператор lock

Оператор lock предназначен для того, чтобы одному потоку не дать войти в важный раздел кода в тот момент, когда в нем находится другой поток. При попытке входа другого потока в заблокированный код потребуется дождаться снятия блокировки объекта. Этот оператор оформляется следующим образом:

Object thisLock = new Object();

lock (thisLock)

{

// Критический фрагмент код

}

где thisLock - обозначает ссылку на синхронизируемый объект, который гарантирует, что фрагмент кода, защищенный блокировкой для данного объекта, будет использоваться только в потоке, получающем эту блокировку, а все остальные потоки блокируются до тех пор, пока блокировка не будет снята. Блокировка снимается по завершении защищаемого ею фрагмента кода.

Так же для блокировки объектов можно применять конструкция lock (this). Но данную конструкцию следует применять в том случае, если this является ссылкой на закрытый объект.

Пример работы распараллеленного приложения без синхронизации потоков:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading;

namespace OperatorLock

{

class Work

{

int i, j;

public void ThreadStart()

{

 

if (i!= j)

Console.WriteLine("Ошибка");

i++;

 

Thread.Sleep(200);

j++;

Console.WriteLine("{0},{1}", i, j);

}

}

class Program

{

static void Main(string[] args)

{

Work store = new Work();

 

for (int i = 0; i < 10; ++i)

{

new Thread(new ThreadStart(store.ThreadStart)).Start();

Thread.Sleep(100);

}

Console.ReadLine();

}

}

}

В методе Main() - метод ThreadStar() 10 раз, в отдельных потоках. Запустим программу. В случайном порядке выведется на экран цифры и надпись "ошибка" (Рис. 5.1).


увеличить изображение
Рис. 5.1. Результат выполнения программы без оператора lock

Такой результат получается потому, что приращение значения переменной i и переменой j происходит не сразу, а с небольшой задержкой (в данном случае искусственной, полученной с помощью метода Sleep(), который усыпляет поток на 200 милисекунд). Именно на этом моменте происходит рассинхронизация потоков. Для синхронизации потоков используется оператор блокировки lock. Модифицируем пример, добавив в него синхронизацию:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading;

namespace OperatorLock

{

class Work

{

int i, j;

object LockObj = new object();

public void ThreadStart()

{

lock (LockObj)

{

if (i!= j)

Console.WriteLine("Ошибка");

i++;

Thread.Sleep(200);

j++;

Console.WriteLine("{0},{1}", i, j);

}

}

}

class Program

{

static void Main(string[] args)

{

Work store = new Work();

 

for (int i = 0; i < 10; ++i)

{

new Thread(new ThreadStart(store.ThreadStart)).Start();

Thread.Sleep(100);

}

 

Console.ReadLine();

}

}

}

Как только поток войдет в контекст lock, маркер блокировки (в данном случае - текущий объект) станет недоступным другим потокам до тех пор, пока блокировка не будет снята по выходе из контекста lock. Если теперь запустить приложение, можно увидеть, что каждый поток получил возможность выполнить свою работу до конца (Рис. 5.2).


увеличить изображение
Рис. 5.2. Результат выполнения программы с оператором lock

Классы синхронизации в.NET Framework

Interlocked

Класс Interlocked предоставляет атомарные операции для переменных, общедоступных нескольким потокам. К примеру, операции инкремента (i++) и декремента (i--) которые не являются безопасными при многопоточной обработке. Такие операции могут прерываться планировщиком потоков. Класс Interlocked позволяет выполнять операции инкремента, декремента, обмена и считывания значений, в безопасной к потокам манере.

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

Таблица 5.1. Методы класса Interlocked
Имя Описание
CompareExchange() Безопасно проверяет два значения на эквивалентность. Если они эквивалентны, изменяет одно из значений на третье
Decrement() Безопасно уменьшает значение на 1
Exchange() Безопасно меняет два значения местами
Increment() Безопасно увеличивает значение на 1

Можно так же использовать оператор lock для блокирования доступа к переменной при установке для нее нового значения, взамен метода Increment():

lock(x)

{

x++;

}

Но можно воспользоваться классом Interlocked. Метод Increment() не только изменяет значение входного параметра, но также возвращает полученное новое значение:

public void Proc()

{

int x = Interlocked.Increment(ref intVal);

}

В дополнение, методы Increment() и Decrement() позволяют автоматически присваивать значения переменным. В примере переменной x присваивается значение 2:

public int x;

public void Proc ()

{

Interlocked.Exchange(ref x, 2);

}

Класс Monitor

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

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

public static void Enter(object syncOb)

public static void Exit(object syncOb)

где syncOb - синхронизируемый объект. Если при вызове метода Enter() заданный объект недоступен, вызывающий поток будет ожидать до тех пор, пока объект не станет доступным.

Использование оператора lock эквивалентно вызову метода Enter() с последующим вызовом метода Exit() класса Monitor. Класс Monitor обладает одним важным преимуществом по сравнению с оператором lock: он позволяет добавлять значение тайм-аута для ожидания получения блокировки. Таким образом, вместо того, чтобы ожидать блокировку до бесконечности, можно вызвать метод TryEnter() и передать в нем значение тайм-аута, указывающее, сколько максимум времени должно ожидаться получение блокировки. Один из форматов его использования метода TryEnter():

public static bool TryEnter(object syncOb)

Метод возвращает значение true, если вызывающий поток получает блокировку для объекта syncOb, и значение false в противном случае. Если заданный объект недоступен, вызывающий поток будет ожидать до тех пор, пока он не станет доступным.

Методы класса Monitor: Wait, Pulse и PulseAll

Методы Wait(), Pulse() и PulseAll() определены в классе Monitor и могут вызываться только из заблокированного фрагмента блока (lock - оператор). Когда выполнение потока временно блокируется, вызывается метод Wait(), т.е. он переходит в режим ожидания и снимает блокировку с объекта, позволяя другому потоку использовать этот объект. Позже, когда другой поток входит в аналогичное состояние блокирования и вызывает метод Pulse() или PulseAll(), "спящий" поток "просыпается". Обращение к методу Pulse() возобновляет выполнение потока, стоящего первым в очереди потоков, пребывающих в режиме ожидания. Обращение к методуPulseAll() сообщает о снятии блокировки всем ожидающим потокам.

Два наиболее используемых формата использования метода Wait():

public static bool Wait(object waitOb)

public static bool Wait(object waitOb, int миллисекунд_простоя)

Первый формат означает ожидание до уведомления. Второй - ожидает до уведомления или до истечения периода времени, заданного в миллисекундах. В обоих случаях параметр waitOb задает объект, к которому ожидается доступ. Форматы использования методов Pulse() и PulseAll() приведены ниже:

public static void Pulse(object waitOb)

public static void PulseAll(object waitOb)

где параметр waitOb - означает объект, освобождаемый от блокировки.

Если метод Wait(), Pulse() или PulseAll() вызывается из кода, который находится вне lock блока, генерируется исключение типа SynchronizationLockException.

В качестве примера работы методов Wait() и Pulse(), создадим программу, которая имитирует работу часов посредством отображения на экране слов "тик" и "так". Для этого создадим класс TickTock, который содержит два метода: tick() - отображает слово "тик" и tock() - отображает слово "так":

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading;

namespace TickTack

{

class TickTock

{

 

public void tick(bool running)

{

lock (this)

{

if (!running) { // Остановка часов.

Monitor.Pulse(this); // Уведомление любых

// ожидающих потоков.

return;

}

Console.Write("тик ");

Monitor.Pulse(this); // Разрешает выполнение

// метода tock().

Monitor.Wait(this); // Ожидаем завершения

// метода tock().

}

}

public void tock(bool running)

{

lock (this)

{

if (!running) { // Остановка часов.

Monitor.Pulse(this); // Уведомление любых

// ожидающих потоков.

return;

}

Console.WriteLine("так");

Monitor.Pulse(this); // Разрешает выполнение

// метода tick().

Monitor.Wait(this); // Ожидаем завершения

// метода tick().

}

}

}

class MyThread

{

public Thread thrd;

TickTock ttOb;

// Создаем новый поток.

public MyThread(string name, TickTock tt)

{

thrd = new Thread(new ThreadStart(this.run));

ttOb = tt;

thrd.Name = name;

thrd.Start();

}

// Начинаем выполнение нового потока.

void run()

{

if (thrd.Name == "тик")

{

for (int i = 0; i < 5; i++) ttOb.tick(true);

ttOb.tick(false);

}

else

{

for (int i = 0; i < 5; i++) ttOb.tock(true);

ttOb.tock(false);

}

}

}

 

class Program

{

static void Main(string[] args)

{

TickTock tt = new TickTock();

MyThread mt1 = new MyThread("тик", tt);

MyThread mt2 = new MyThread("так", tt);

mt1.thrd.Join();

mt2.thrd.Join();

Console.WriteLine("Часы остановлены");

Console.ReadLine();

}

}

}

При выполнении программа сгенерирует результаты, представленные на Рис. 5.3.


увеличить изображение
Рис. 5.3. Результат выполнения программы с использованием методов Wait() и Pulse()

В методе Main() создается объект класса TickTock - tt, который используется для запуска двух потоков на выполнение. Если в методе Run() из класса MyThread обнаруживается имя потока mt1, соответствующее ходу часов "тик", то вызывается метод tick(). А если это имя потока mt2, соответствующее ходу часов "так", то вызывается метод tock().

Каждый из их методов вызывается пять раз подряд с передачей логического значения true в качестве аргумента. Часы идут до тех пор, пока этим методам передается логическое значение true, и останавливаются, как только передается логическое значение false:

public void tick(bool running)

{

lock (this)

{

if (!running) { // Остановка часов.

Monitor.Pulse(this); // Уведомление любых

// ожидающих потоков.

return;

}

Console.Write("тик ");

Monitor.Pulse(this); // Разрешает выполнение

// метода tock().

Monitor.Wait(this); // Ожидаем завершения

// метода tock().

}

}

Методы Wait() и Pulse() могут использоваться только в синхронизированных блоках кода. Вначале метода tick() проверяется значение текущего параметра, которое служит явным признаком остановки часов. Если это логическое значение false, то часы остановлены. В этом случае вызывается метод Pulse(), разрешающий выполнение любого потока, ожидающего своей очереди.

Если же часы идут при выполнении метода tick(), то на экран выводится слово "тик" с пробелом, затем вызывается метод Pulse(), а после него - метод Wait(). При вызове метода Pulse() разрешается выполнение потока для того же самого объекта, а при вызове метода Wait() выполнение метода tock() приостанавливается до тех пор, пока метод Pulse() не будет вызван из другого потока. Теперь, уберем методы Wait() иPulse() из созданной ранее программы:

class TickTock

{

public void tick(bool running)

{

lock (this)

{

if (!running)

{ // Остановка часов.

return;

}

Console.Write("тик ");

}

}

public void tock(bool running)

{

lock (this)

{

if (!running)

{ // Остановка часов.

return;

}

Console.WriteLine("так");

}

}

}

При выполнении программа сгенерирует результаты, представленные на Рис. 5.4.


увеличить изображение
Рис. 5.4. Результат выполнения программы без использования методов Wait() и Pulse()

Как видно из результатов выполнения программы, методы tick() и tock() больше не синхронизированы.

Класс Mutex

Когда двум или более потокам одновременно требуется доступ к общему ресурсу, системе необходим механизм синхронизации, чтобы обеспечить использование ресурса только одним потоком одновременно. Класс Mutex, определенный в пространстве имен System. Threading - это примитив, который предоставляет эксклюзивный доступ к общему ресурсу только одному потоку синхронизации. Если поток получает семафор, второй поток, желающий получить этот семафор, приостанавливается до тех пор, пока первый поток не освободит семафор. Термин Mutex происходит от фразы mutually exclusive (взаимно исключающий), и поскольку только один поток может получить блокировку монитора для данного объекта в любой момент времени, только один поток в любой момент времени может получить данный Mutex. Класс Mutex очень похож на класс Monitor тем, что тоже допускает наличие только одного владельца. Только один поток может получить блокировку и иметь доступ к защищаемым Mutex синхронизированным областям кода. У Mutex имеется несколько конструкторов. Ниже приведены три наиболее употребительных конструктора:

public Mutex()

public Mutex(bool initiallyOwned);

public Mutex(bool initiallyOwned, string name_mutex)

В первой форме конструктора создается Mutex, которым первоначально никто не владеет. А во второй и третей форме исходным состоянием Mutex завладевает вызывающий поток, если параметр initiallyOwned имеет логическое значение true, если false, то объектом Mutex никто не владеет.

Для того чтобы получить Mutex, используется метод WaitOne(). Метод WaitOne() ожидает до тех пор, пока не будет получен Mutex, для которого он был вызван. Следовательно, этот метод блокирует выполнение вызывающего потока до тех пор, пока не станет доступным указанный Mutex. Данный метод всегда возвращает логическое значение true. Форма объявления метода WaitOne():

Mutex mutex = new Mutex(false);

mutex.WaitOne();

Когда в коде не требуется использовать Mutex, он освобождается с помощью метода ReleaseMutex():

mutex.ReleaseMutex();

Ниже продемонстрирован код реализующий работу Mutex:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading;

namespace Mutext

{

class Program

{

private static Mutex mut = new Mutex();

private const int numIterations = 1;

private const int numThreads = 3;

private static void Main(string[] args)

{

// Создаем потоки, которые будут использовать защищенный ресурс

for (int i = 0; i < numThreads; i++)

{

Thread myThread = new Thread(new ThreadStart(MyThreadProc));

myThread.Name = String.Format("Поток{0}", i + 1);

myThread.Start();

}

// Главный поток завершил работу

}

private static void MyThreadProc()

{

for (int i = 0; i < numIterations; i++)

{

UseResource();

}

}

//Синхронизируем данный метод

private static void UseResource()

{

mut.WaitOne();

Console.WriteLine("{0} зашел в защищенную зону",

Thread.CurrentThread.Name);

// Имитируем работу

Thread.Sleep(500);

Console.WriteLine("{0} покинул защищенную зону",

Thread.CurrentThread.Name);

// Release the Mutex.

mut.ReleaseMutex();

Console.ReadLine();

}

}}

Результат работы программы, с использованием Mutex, представлен на Рис. 5.5.


увеличить изображение
Рис. 5.5. Результат выполнения программы с использованием синхронизации Mutex

Если же закомментировать методы WaitOne() и ReleaseMutex() и запустить программу, то программа сгенерирует результат, представленный на Рис. 5.6.


увеличить изображение
Рис. 5.6. Результат выполнения программы без использования синхронизации Mutex

Класс Semaphore

Класс Semaphore предназначен для управления доступом к пулу ресурсов. Потоки производят вход в семафор, вызывая метод WaitOne(), и освобождают семафор при вызове метода Release(). Semaphore похож на Mutex, за исключением того, что он предоставляет одновременный доступ к общему ресурсу не одному, а нескольким потокам. Счетчик на семафоре уменьшается на единицу каждый раз, когда в семафор входит поток, и увеличивается на единицу, когда поток освобождает семафор. Когда счетчик равен нулю, последующие запросы блокируются, пока другие потоки не освободят семафор. Когда семафор освобожден всеми потоками, счетчик имеет максимальное значение, заданное при создании семафора.

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

Ниже приведена форма конструктора данного класса:

public Semaphore(int initialCount, int maximumCount)

где initialCount - это первоначальное значение для счетчика разрешений семафора, т.е. количество первоначально доступных разрешений;

maximumCount - максимальное значение данного счетчика, т.е. максимальное количество разрешений, которые может дать семафор.

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

Если коду больше не требуется владеть семафором, он освобождает его, вызывая метод Release(). Ниже приведены две формы этого метода:

public int Release()

public int Release(int releaseCount)

В первой форме метод Release() высвобождает только одно разрешение, а во второй форме - количество разрешений, определяемых параметром releaseCount. В обеих формах данный метод возвращает подсчитанное количество разрешений, существовавших до высвобождения.

Класс Barrier

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

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

1. Создать экземпляр, указав количество потоков, которые будут встречаться одновременно;

2. Каждый поток, должен вызывать метод SignalAndWait().

Метод SignalAndWait() cсообщает, что участник достиг барьера (Barrier) и ожидает достижения барьера другими участниками. Ниже приведена форма этого метода:

public void SignalAndWait()

Пример использования класса Barrier приведен ниже:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading;

namespace BarrierProgram

{

class Program

{

static Barrier barrier = new Barrier(3);

static void Main(string[] args)

{

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

}

Console.ReadLine();

}

}

}

Как видно из результата выполнения программы (Рис. 5.7), каждый из трех потоков выводит числа от 0 до 4, одновременно с другими потоками.


увеличить изображение
Рис. 5.7. Результат выполнения программы с использованием класса Barrier

Если же не использовать класс Barrier,а именно убрать из кода данную строчку: barrier.SignalAndWait() - программа выведет числа в случайном порядке (Рис. 5.8).


увеличить изображение
Рис. 5.8. Результат выполнения программы без использования класса Barrier



Поделиться:




Поиск по сайту

©2015-2024 poisk-ru.ru
Все права принадлежать их авторам. Данный сайт не претендует на авторства, а предоставляет бесплатное использование.
Дата создания страницы: 2017-06-11 Нарушение авторских прав и Нарушение персональных данных


Поиск по сайту: