пятница, 25 мая 2012 г.

Часть 5. События синхронизации

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


Для передачи синхронизированных событий в 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), который в данном примере никто не вызывает:
Ладно, с событиями заканчиваем. Давайте в следующий раз посмотрим реальный пример намногопоточность.

Комментариев нет:

Отправить комментарий