Предыдущие
примеры показывали, как обеспечить «критические секции», но иногда возникает
необходимость не заблокировать доступ к некоторым данным из нескольких потоков,
а просто уведомить, что часть работы сделана, продолжайте. Вот сегодня давайте
и посмотрим, как это можно реализовать.
Для передачи синхронизированных событий в C# имеются два класса AutoResetEvent и ManualResetEvent (есть еще статический EventWaitHandle и упрощенный класс CountdountEvent, но о них чуть позже). Принципиальное отличие между ними заключается в том, кто принимает решение о том, что событие перестало быть активным.
Итак, давайте сразу посмотрим пример. Помните, у нас два потока прибавляли по 1 и вычитали по 1 из общей переменной? Если значение этой переменной вывести на экран, то там может быть что угодно:
Этот вывод я получил, добавив в критическую секцию методов Inc и Dec вывод на консоль переменной value.Но, допустим, нам необходимо, чтобы значение в переменной было всегда меньше или равно пяти и, в тоже время, не меньше ноля. Для решения поставленной задачи, заведем два события синхронизации и будем ими обмениваться между нашими потоками. Первый поток будет увеличивать значение на 1, но как только оно достигнет значения 5, он сообщит об этом второму потоку и будет ждать от него ответного уведомления. Второй поток, будет уменьшать на 1, а при достижении ноля, уведомлять первый поток и соответственно ждать ответного уведомления.
Любознательный читатель, набрав приведенный пример возмутиться: «Программа повисла!». А пусть объяснение того что происходит, останется вопросом на засыпку. Какие есть варианты? И как можно решить данную проблему?Основная проблема, которая возникнет с применением WaitOne в таком варианте (кстати, она аналогична вызову Join у Thread), заключается в том, что на время ожидания события поток приостанавливает свою работу. Частично, данную проблему можно решить, поставив ожидание до события или до истечения заданного интервала времени (есть две перегруженные версии с интервалом в миллисекундах и с TimeSpan). В этом случае, если событие не произошло, мы можем сделать некую полезную работу, а потом подождать еще немного. Определить, что именно произошло, можно по булевому значению, возвращаемому методом (если событие – true, если таймаут, то false).
Давайте посмотрим небольшой пример на такую функциональность. Допустим у нас главное приложение должно скопировать файл из одного места в другое. Операция будет достаточно длительная, и если пользователь решит прервать ее, мы не должны препятствовать этому решению. Или, иными словами, у нас в приложении должно быть два потока: для взаимодействия с пользователем и для копирования файла. В процессе копирования, время от времени поток будет проверять, а не сигналит ли ему поток взаимодействующий с пользователем, о том, что операцию желательно прервать.
Если в процессе работы программы (копирования файла) нажать на любую кнопку, то копирование прервется:
Ну и давайте на сегодня последний пример, посмотрим, в чем все таки отличие AutoResetEvent от ManualResetEvent.Создадим три потока, которые будет ждать наступления события. Как только поток получает событие, он сообщает об этом на консоль.
Причем, если мы будем несколько раз запускать приложение, то можем увидеть и то, что событие получил поток A, и то, что поток B. Но главное, в том, что один поток получив событие, «сбрасывает» его. Все остальные потоки о том, что событие было не узнают.Если в приведенном примере заменить AutoResetEvent на ManualResetEvent:
и
То после запуска, все три потока получат событие, т.к. ManualResetHandler не сбрасывается отдельным методом (Reset), который в данном примере никто не вызывает:
Ладно, с событиями заканчиваем. Давайте в следующий раз посмотрим реальный пример намногопоточность.
Для передачи синхронизированных событий в C# имеются два класса AutoResetEvent и ManualResetEvent (есть еще статический EventWaitHandle и упрощенный класс CountdountEvent, но о них чуть позже). Принципиальное отличие между ними заключается в том, кто принимает решение о том, что событие перестало быть активным.
Итак, давайте сразу посмотрим пример. Помните, у нас два потока прибавляли по 1 и вычитали по 1 из общей переменной? Если значение этой переменной вывести на экран, то там может быть что угодно:
Этот вывод я получил, добавив в критическую секцию методов Inc и Dec вывод на консоль переменной value.Но, допустим, нам необходимо, чтобы значение в переменной было всегда меньше или равно пяти и, в тоже время, не меньше ноля. Для решения поставленной задачи, заведем два события синхронизации и будем ими обмениваться между нашими потоками. Первый поток будет увеличивать значение на 1, но как только оно достигнет значения 5, он сообщит об этом второму потоку и будет ждать от него ответного уведомления. Второй поток, будет уменьшать на 1, а при достижении ноля, уведомлять первый поток и соответственно ждать ответного уведомления.
class Program
{
static int value = 0;
static AutoResetEvent
five = null;
static AutoResetEvent
zero = null;
static void
Inc()
{
// Перед запуском
ждем, чтобы нам сообщили,
// что в переменной
0
zero.WaitOne();
for
(int i = 0; i < 100; i++)
{
if (value == 5)
{
// Сообщаем,
что достигли 5
five.Set();
//
Ждем
ноля
zero.WaitOne();
}
value = value + 1;
Console.WriteLine(value);
}
}
static void Dec()
{
// Перед запуском
ждем, чтобы нам сообщили,
// что в переменной
5
five.WaitOne();
for
(int i = 0; i < 100; i++)
{
if (value == 0)
{
// Сообщаем,
что достигли 0
zero.Set();
// Ждем
5
five.WaitOne();
}
value = value - 1;
Console.WriteLine(value);
}
}
static void Main(string[] args)
{
Thread inc = new
Thread(new ThreadStart(Inc));
Thread dec = new
Thread(new ThreadStart(Dec));
five = new AutoResetEvent(false); // Событие
не
активно
zero = new AutoResetEvent(true); // т.к. в value ноль, поднимаем событие
inc.Start();
dec.Start();
inc.Join();
dec.Join();
Console.WriteLine("Значение по окончании:", value);
Console.ReadKey();
}
}Вот
так выглядит процесс работы:Любознательный читатель, набрав приведенный пример возмутиться: «Программа повисла!». А пусть объяснение того что происходит, останется вопросом на засыпку. Какие есть варианты? И как можно решить данную проблему?Основная проблема, которая возникнет с применением WaitOne в таком варианте (кстати, она аналогична вызову Join у Thread), заключается в том, что на время ожидания события поток приостанавливает свою работу. Частично, данную проблему можно решить, поставив ожидание до события или до истечения заданного интервала времени (есть две перегруженные версии с интервалом в миллисекундах и с TimeSpan). В этом случае, если событие не произошло, мы можем сделать некую полезную работу, а потом подождать еще немного. Определить, что именно произошло, можно по булевому значению, возвращаемому методом (если событие – true, если таймаут, то false).
Давайте посмотрим небольшой пример на такую функциональность. Допустим у нас главное приложение должно скопировать файл из одного места в другое. Операция будет достаточно длительная, и если пользователь решит прервать ее, мы не должны препятствовать этому решению. Или, иными словами, у нас в приложении должно быть два потока: для взаимодействия с пользователем и для копирования файла. В процессе копирования, время от времени поток будет проверять, а не сигналит ли ему поток взаимодействующий с пользователем, о том, что операцию желательно прервать.
class Program
{
static AutoResetEvent
exit = null;
static void Copy()
{
FileStream reader = new FileStream(@"Путь к файлу который копируем", FileMode.Open);
FileStream writer = new FileStream(@"Путь куда копируем", FileMode.Create);
byte[] buffer = new byte[1024*1024];
// Цикл, пока
не кончится файл или пока пользователь не нажмет кнопку на клавиатуре
while
(!exit.WaitOne(10) && reader.Position < reader.Length)
{
int readedBytesCount =
reader.Read(buffer, 0, buffer.Length);
writer.Write(buffer, 0, readedBytesCount);
Console.Write(".");
}
writer.Close();
if (reader.Position < reader.Length)
{
Console.WriteLine();
Console.WriteLine("Отмена копирования");
File.Delete(@"Путь куда копируем");
}
else
{
Console.WriteLine("Копирование завершено");
}
reader.Close();
}
static void Main(string[] args)
{
exit = new AutoResetEvent(false);
Thread longOperatioin = new Thread(new ThreadStart(Copy));
longOperatioin.Start();
Console.WriteLine("Идет копирование файла. Нажатие любой клавиши прервет операцию.");
Console.ReadKey();
exit.Set();
Console.ReadKey();
}
}Если в процессе работы программы (копирования файла) нажать на любую кнопку, то копирование прервется:
Ну и давайте на сегодня последний пример, посмотрим, в чем все таки отличие AutoResetEvent от ManualResetEvent.Создадим три потока, которые будет ждать наступления события. Как только поток получает событие, он сообщает об этом на консоль.
class Program
{
static AutoResetEvent
re = null;
static void Worker()
{
re.WaitOne();
Console.WriteLine("Поток {0} получил событие", Thread.CurrentThread.Name);
}
static void Main(string[] args)
{
re = new AutoResetEvent(false);
Thread threadA = new Thread(new ThreadStart(Worker))
{ Name = "A" };
Thread threadB = new Thread(new ThreadStart(Worker))
{ Name = "B" };
Thread threadC = new Thread(new ThreadStart(Worker))
{ Name = "C" };
threadA.Start();
threadB.Start();
threadC.Start();
Console.WriteLine("Потоки запущены");
Thread.Sleep(1000);
re.Set();
Console.ReadKey();
}
}Вот
так выглядит окно нашего приложения после запуска:Причем, если мы будем несколько раз запускать приложение, то можем увидеть и то, что событие получил поток A, и то, что поток B. Но главное, в том, что один поток получив событие, «сбрасывает» его. Все остальные потоки о том, что событие было не узнают.Если в приведенном примере заменить AutoResetEvent на ManualResetEvent:
//static AutoResetEvent re =
null;
static ManualResetEvent re = null;
//re = new AutoResetEvent(false);
re = new ManualResetEvent(false);То после запуска, все три потока получат событие, т.к. ManualResetHandler не сбрасывается отдельным методом (Reset), который в данном примере никто не вызывает:
Ладно, с событиями заканчиваем. Давайте в следующий раз посмотрим реальный пример намногопоточность.
Комментариев нет:
Отправить комментарий