Новости из Блогов Пишем простое многопоточное приложение на C#

Discussion in 'Мировые новости. Обсуждения.' started by d3l3t3, 3 Aug 2012.

  1. d3l3t3

    d3l3t3 Banned

    Joined:
    3 Dec 2010
    Messages:
    1,771
    Likes Received:
    98
    Reputations:
    10
    Пишем простое многопоточное приложение


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


    Итак, так как мы пишем абстрактное приложение, то никакого полезного функционала оно делать не будет.
    Давайте просто разметим, что хотелось бы видеть в любом многопоточном приложении.
    • Выставление количества запускаемых потоков.
    • Возможность не только запуска, но и остановки потоков.
    • Лог файл, в который потоки будут писать выполняемые действия.
    • Контролы, которые будут говорить о кол-ве выполненных / невыполненных заданий.
    • Контрол, который будет показывать кол-во работающих потоков.
    • Прогрессбар выполнения общей работы.

    Давайте накидаем это всё необходимое на форму. У меня получилось вот так:

    [​IMG]

    Теперь перейдем к написанию кода. В первую очередь надо сказать о том, что потоки не имеют доступа к GUI, т.е. вы не сможете в потоке написать что-то вроде richTextBox1.Text = «str»;

    Мой вариант решения этой проблемы такой: создаем отдельный static (!) класс, который будет получать текущую форму и делать с ней манипуляции. Каждое необходимое изменение GUI я описал в отдельных функциях, причем изменение происходит через Invoke. Подробнее об Invoke можно легко почитать в MSDN.
    Мой класс, который я назвал GUIController, выглядит так:

    PHP:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;
     
    namespace 
    ThreadingTest
    {
        public static class 
    GUIController
        
    {
            private static 
    Form1 _instance;
     
            public static 
    void setForm(Form1 f)
            {
                
    _instance f;
            }
     
            public static 
    void updateProgressBar(int incValue)
            {
                
    ProgressBar PB = (ProgressBar)_instance.Controls["progressBar1"];
                
    PB.BeginInvoke(new MethodInvoker(() => PB.Increment(incValue)));
            }
     
            public static 
    void updateSuccessLabel(int successCount)
            {
                
    Label L = (Label)_instance.Controls["successLabel"];            
                
    L.BeginInvoke(new MethodInvoker(() => L.Text successCount.ToString()));
            }
     
            public static 
    void updateAliveThreadsLabel(int aliveThreadsCount)
            {
                
    Label L = (Label)_instance.Controls["aliveThreadsLabel"];
                
    L.BeginInvoke(new MethodInvoker(() => L.Text aliveThreadsCount.ToString()));
            }
     
            public static 
    void updateFailsLabel(int failCount)
            {
                
    Label L = (Label)_instance.Controls["failLabel"];
                
    L.BeginInvoke(new MethodInvoker(() => L.Text failCount.ToString()));
            }
     
            public static 
    void appendLog(string threadIdstring text)
            {
                
    RichTextBox RTB = (RichTextBox)_instance.Controls["richTextBox1"];
                
    RTB.BeginInvoke(new MethodInvoker(() => RTB.AppendText(string.Format("Thread #{0}: {1}" Environment.NewLinethreadIdtext))));
            }
     
            public static 
    void startButtonEnabled(bool enabled)
            {
                
    Button B = (Button)_instance.Controls["button1"];
                
    B.BeginInvoke(new MethodInvoker(() => B.Enabled enabled));
            }
     
            public static 
    void stopButtonEnabled(bool enabled)
            {
                
    Button B = (Button)_instance.Controls["button2"];
                
    B.BeginInvoke(new MethodInvoker(() => B.Enabled enabled));
            }
     
        }
    }
    Перед использованием, необходимо загрузить форму, которая будет использоваться для изменения. Для этого у меня объявлена переменная _instance, а также функция setForm, которая запишет нашу форму туда. Так что помимо добавления класса, необходимо добавить код, который будет записывать текущую форму в GUIController. Это сделать очень легко:

    PHP:
            public Form1()
            {
                
    InitializeComponent();
                
    GUIController.setForm(this);
            }
    Теперь давайте создадим класс, где будет код, который будет выполняться в потоках. Я назвал его ThreadJob. Наша программа будет генерировать число от 3 до 20, а затем ничего не делать сгенерированное кол-во секунд. Так как у нас есть индикаторы успешности выполнения абстрактной работы, то надо придумать то, что будет этим показателем. Например у меня работа считается успешной, когда сгенерированное число от 0 до 2 — это 0 .

    Еще кое-что: нам необходимо будет сделать Event выхода из потока, чтобы основной после выхода потока обновлял все счетчики.
    Для этого нам надо будет создать Event, который я назвал ThreadCompleted.

    Для того, чтобы сообщить значение успешности работы главному потоку, я сделал класс ThreadArgs (который наследуется от EventArgs).
    Т.е. примерный ход работы такой:

    1. В основном коде есть функция, которая создает потоки + функция, которая обрабатывает Event выхода из потока.
    2. При создании потока говорим ему, что именно эта функция будет обрабатывать наш эвент.
    3. Поток выполняет работу, затем кидает Event ThreadCompleted, а в ThreadArgs отдает данные об успешности выполнения задания.
    4. Функция в основном коде на основе ThreadArgs обновляет счетчики успешности (а заодно и все остальные).

    В потоке я также пишу в лог все необходимые данные.
    Полный код класса ThreadJob и ThreadArgs:
    PHP:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading;
     
    namespace 
    ThreadingTest
    {
        public class 
    ThreadArgs EventArgs
        
    {
            public 
    bool isSuccess// это пойдет в основной поток, который будет обрабатывать
            
    public ThreadArgs(bool success)
            {
                
    isSuccess success// для удобства я вынес этот единственный параметр в конструктор
            
    }
        }
     
        class 
    ThreadJob
        
    {
            private static 
    Random random = new Random((int)DateTime.Now.Ticks); // инициализируем генератор 
            
    public event EventHandler ThreadCompleted// создаем новый эвент, который будет выполняться по завершению работы
            
    public void Job() // функция работы
            
    {
                
    GUIController.appendLog(Thread.CurrentThread.Name"Started!"); // пишем в лог с помощью GUIController
     
                
    int s random.Next(320); // генерируем рандом число
                
    GUIController.appendLog(Thread.CurrentThread.Name"Sleep " " seconds.");
                
    Thread.Sleep(1000); // спим, умножение на 1000 -- потому что в секундах, а нам надо в миллисекундах
     
     
                
    GUIController.appendLog(Thread.CurrentThread.Name"Done, exiting");
     
     
                if (
    ThreadCompleted != null)
                {
                    
    ThreadCompleted(Thread.CurrentThread, new ThreadArgs( (random.Next(02) == 0) ); // кидаем Event о том, что мы закончили + ставим переменную isSuccess
                
    }
            }
        }
    }
    Также у меня есть переменная типа List в которую при добавлении — потоки записываются, а при выходе — удаляются. Это позволяет контроллировать любой из потоков (нам же это нужно для кол-ва активных + остановки).

    Комментированный код всего основного кода:
    PHP:
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Threading;
    using System.Windows.Forms;
     
    namespace 
    ThreadingTest
    {
        public 
    partial class Form1 Form
        
    {
            List<
    Threadthreads = new List<Thread>(); // список всех потоков
            
    public int successJobs 0// счетчик успешных работ
            
    public int failJobs 0// соответственно неуспешных
            
    public object lockobj = new object(); // будем использовать этот объект в lock -- на случай, что несколько потоков закончат одновременно.
     
            
    public Form1()
            {
                
    InitializeComponent();
                
    GUIController.setForm(this); // устанавливаем эту форму как контроллируемую для GUI
            
    }
     
     
            public 
    void ThreadCompleted(object senderEventArgs e)  // функция обрабатывающая Event выхода из потока
            
    {
                
    lock (lockobj)
                {
                    
    ThreadArgs TA = (ThreadArgs)e// приведем EventArgs к ThreadArgs, по-другому никак
                    
    threads.Remove((Thread)sender); // удаляем поток из списка работающих
     
                    
    if (TA.isSuccess// работа выполнена успешно 
                    
    {
                        
    successJobs++; // увеличиваем счетчик
                        
    GUIController.updateSuccessLabel(successJobs); // модифицируем GUI
     
                    
    }
                    else 
    // неуспешно
                    
    {
                        
    failJobs++;
                        
    GUIController.updateFailsLabel(failJobs);
                    }
     
                    
    GUIController.updateProgressBar(1); // добавляем в прогрессбар
                    
    GUIController.updateAliveThreadsLabel(threads.Count); // меняем кол-во запущенных потоков
     
                    
    if (threads.Count == 0// если это был последний поток
                    
    {
                        
    GUIController.appendLog("MAIN""ALL JOB COMPLETED"); // пишем в лог, что работа завершена
                        
    GUIController.startButtonEnabled(true); // включаем кнопку старт
                        
    GUIController.stopButtonEnabled(false); // отключаем кнопку стоп
                    
    }
                }
            }
     
            private 
    void button1_Click(object senderEventArgs e// запуск потоков
            
    {
                
    int thrCount Convert.ToInt32(numericUpDown1.Value); // получаем кол-во запускамых потоков
     
                
    progressBar1.Value 0// скинем значения
                
    progressBar1.Maximum thrCount// максимум прогрессбара равен кол-ву потоков
     
                
    GUIController.appendLog("MAIN""RUNNING " thrCount " THREADS"); // пишем в лог, что запускаем n потоков
                
    for (int i 0thrCounti++)
                {
                    
    ThreadJob TJ = new ThreadJob(); // инициализируем класс с работой
                    
    TJ.ThreadCompleted += ThreadCompleted// важно! ставим обработчик на Event о выходе
     
                    
    Thread thr = new Thread(TJ.Job); // создаем новый поток, который будет выполнять функцию из класса с работой
                    
    thr.Name = (1).ToString(); // за имя потока я обычно ставлю его номер 
                    
    thr.Start(); // стартуем поток
     
                    
    threads.Add(thr); // добавляем его в список запущенных
     
                
    }
     
                
    GUIController.startButtonEnabled(false); // все запущены, вырубаем кнопку старта
                
    GUIController.stopButtonEnabled(true); // врубаем кнопку стоп
                
    GUIController.updateAliveThreadsLabel(thrCount);
     
            }
     
            private 
    void button2_Click(object senderEventArgs e// остановка потоков
            
    {
                for (
    int i 0threads.Counti++) // каждый из запущенных потоков
                
    {
                    
    Thread thrd threads[i];
                    
    thrd.Abort(); // остановить 
                    
    threads.Remove(thrd); // убрать из списка запущенных
                
    }
     
                
    GUIController.updateAliveThreadsLabel(threads.Count); // обновляем кол-во запущенных
                
    GUIController.startButtonEnabled(true); // кнопки
                
    GUIController.stopButtonEnabled(false);
     
            }
     
     
        }
    }
    Скачать проект: ThreadingTest.rar (60 кб)


    Антон Антонов
    http://bafoed.net/post/7624/
     
    Flip Nicesik likes this.
  2. ChymeNik

    ChymeNik Member

    Joined:
    31 Aug 2010
    Messages:
    29
    Likes Received:
    7
    Reputations:
    9
    Спасибо за код.
    static Класс создавать не обязательно, пример для добавления строки в лог:

    private void log(string s,string account)
    {
    BeginInvoke(new Action(() => { textBox1.AppendText(DateTime.Now.Hour+":"+DateTime.Now.Minute+" - "+ account+": "+s + "\r\n"); }));
    }
     
  3. j0sur

    j0sur Member

    Joined:
    8 Apr 2012
    Messages:
    140
    Likes Received:
    7
    Reputations:
    0
    Добавлю немного от себя, invok'и вовсе не обязательны, нужно лишь св-во формы CheckForIllegalCrossThreadCalls поставить в false и работать с контролами как с потока gui.
     
  4. seosimf

    seosimf Member

    Joined:
    3 Mar 2011
    Messages:
    271
    Likes Received:
    44
    Reputations:
    6
    И потом радоваться куче возможных проблем, стандартные UI котролы НЕ Thread-Safe, не говоря уже о:
    Статья ниочем, каждый нуб овладевший простым навыками сразу же пытается ими поделиться, подобными писульками интернеты уже забиты.
     
    #4 seosimf, 6 Aug 2012
    Last edited: 6 Aug 2012
  5. VY_CMa

    VY_CMa Green member

    Joined:
    6 Jan 2012
    Messages:
    917
    Likes Received:
    492
    Reputations:
    724
    _________________________
    #5 VY_CMa, 6 Aug 2012
    Last edited: 6 Aug 2012
  6. j0sur

    j0sur Member

    Joined:
    8 Apr 2012
    Messages:
    140
    Likes Received:
    7
    Reputations:
    0
    Врут, у меня все окей(правда потоки обращаются только к ProgressBar'у, к остальным у меня нет необходимости обращаться).