пятница, 13 июля 2012 г.

Пауза и возобновление работы потока

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

За работу с потоками в C# отвечают несколько классов: во-первых это класс Thread (System.Threading), представляющий отдельный поток. А во-вторых - это ThreadPool, дающий возможность работы с пулом потоков. Пул потоков предпочтительнее для использования, да и удобнее он в реализации. Пул потоков - это очередь потоков приложения, в эту очередь вы можете вставить свой метод, который будет выполнен в отдельном потоке, когда один из потоков освободится.
Многие современные приложения используют возможность паузы выполнения какой то задачи. В тех же антивирусных сканерах обязательно имеются кнопки паузы для вирусного сканирования и его возобновления. Такие задачи как поиск вирусов в антивирусных комплексах реализованы почти наверняка через многопоточность, так как же приостановить выполнение потока, а потом его опять запустить с того же места? Попробуем в этом разобраться...

Создадим простейшее приложение на C#, которое будет использовать отдельный поток для вывода информации. На форме у нас должны располагаться три кнопки, в свойстве Text которых можно написать: Старт, Пауза, Стоп. Так же нам понадобится метка label.
Разместив необходимые компоненты на форме, можно приступать к кодингу. Объявим несколько глобальных переменных и напишем кое-что в конструктор формы:

EventWaitHandle ew;
        bool Stop, Pause;
        public Form1()
        {

            InitializeComponent();
            ew = new EventWaitHandle(false, EventResetMode.AutoReset);
            Stop = true;
        }
Здесь мы объявили объект класса EventWaitHandle, именно с помощью этого класса и происходит сигнализация потоку о необходимости остановке или возобновления своей работы. Так же мы объявили две булевых переменных.
На кнопку Старт повесим код:

private void button1_Click(object sender, EventArgs e)
        {
            if (Stop)
            {
                button2.Enabled = button3.Enabled = true;
                button1.Enabled = false;
                Stop = false;
                ThreadPool.QueueUserWorkItem(new WaitCallback((s) =>
                {
                    ThreadMethod(100);
                }));
            }
            else
            {
                ew.Set();
                button3.Enabled = true;
            }
        }
Так же создадим метод, который будет выполняться в отдельном потоке:

void ThreadMethod(int value)
        {
            Action<int> act = new Action<int>((v) =>
                {
                    label1.Text = v.ToString();
                });

            int i = 0;
            while (!Stop && i <= value)
            {
                if (Pause)
                {
                    ew.WaitOne();
                    Pause = false;
                }
                Thread.Sleep(500);
                i += 1;
                if (label1.InvokeRequired)
                    label1.Invoke(new MethodInvoker(() =>
                        {
                            act(i);
                        }));
                else
                    act(i);
            }
        }
Настала пора немного рассказать про этот загадочный EventWaitHandle. Экземпляр данного класса имеет много методов, наиболее полезные из которых это - WaitOne() и Set(). WaitOne() служит для приостановки действия потока, а Set - для возобновления его работы с места остановки. По нажатию кнопки Старт у нас в пул потоков помещается метод, который будет выполнен в отдельном потоке. Если вы не знакомы с лямбда-выражениями, то советую вам побыстрее исправлять это, т.к лямбды - чрезвычайно упрощают написание кода. В метод ThreadMethod, который мы запускаем в новом потоке мы передаём в параметре число 100.

Особенность работы с WinForm в C# заключается в том что доступ к компоненту можно получить только из того потока. в котором данный компонент создан, соответственно, если попытаться достучаться из другого потока до свойств компонента не созданного в этом потоке,  то велика вероятность возникновения ошибок. Для решения этой проблемы у всех компонентов имеется метод Invoke в который необходимо передать делегат (ссылку на метод). Через свойство InvokeRequired проверяется необходимость доступа через Invoke, если InvokeRequired равен false значит метод можно запустить и без Invoke.
На кнопки Пауза и Стоп повесим код, соответственно:

private void button2_Click(object sender, EventArgs e)//PAUSE
        {
            Pause = true;
            button3.Enabled = false;
            button1.Enabled = true;
        }

        private void button3_Click(object sender, EventArgs e)//STOP
        {
            Stop = true;
            button2.Enabled = false;
            button1.Enabled = true;
        }

Что же делает наше приложение? По нажатию кнопки Старт у нас запускается поток в котором меняется свойство Text label'a с задержкой в 500mc. При нажатии на Паузу, булева переменная Pause становится равна истине и в ThreadMethhod'e поток приостанавливается при помощи WaitOne(). При нажатии Старт поток возобновляет работу с места остановки. При нажатии Стоп поток останавливается, уничтожается. Вот такое незамысловатое приложение

Исходники: http://dl.dropbox.com/u/16627362/threadPause.rar

Автор: Богданов М. 
13.07.2012



5 комментариев:

  1. Эта тема как раз "вовремя", мне пригодилось. Написано просто и понятно, только лямбды злят.

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

      Удалить
  2. а для чего в строке

    while (!Stop && i <= value)

    проверяют i <= value

    ОтветитьУдалить