Авг 012012
 

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


Итак, так как мы пишем абстрактное приложение, то никакого полезного функционала оно делать не будет.
Давайте просто разметим, что хотелось бы видеть в любом многопоточном приложении.

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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 threadId, string text)
        {
            RichTextBox RTB = (RichTextBox)_instance.Controls["richTextBox1"];
            RTB.BeginInvoke(new MethodInvoker(() => RTB.AppendText(string.Format("Thread #{0}: {1}" + Environment.NewLine, threadId, text))));
        }
 
        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. Это сделать очень легко:

1
2
3
4
5
        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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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(3, 20); // генерируем рандом число
            GUIController.appendLog(Thread.CurrentThread.Name, "Sleep " + s + " seconds.");
            Thread.Sleep(s * 1000); // спим, умножение на 1000 -- потому что в секундах, а нам надо в миллисекундах
 
 
            GUIController.appendLog(Thread.CurrentThread.Name, "Done, exiting");
 
 
            if (ThreadCompleted != null)
            {
                ThreadCompleted(Thread.CurrentThread, new ThreadArgs( (random.Next(0, 2) == 0) ); // кидаем Event о том, что мы закончили + ставим переменную isSuccess
            }
        }
    }
}

Также у меня есть переменная типа List в которую при добавлении — потоки записываются, а при выходе — удаляются. Это позволяет контроллировать любой из потоков (нам же это нужно для кол-ва активных + остановки).

Комментированный код всего основного кода:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
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<Thread> threads = 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 sender, EventArgs 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 sender, EventArgs e) // запуск потоков
        {
            int thrCount = Convert.ToInt32(numericUpDown1.Value); // получаем кол-во запускамых потоков
 
            progressBar1.Value = 0; // скинем значения
            progressBar1.Maximum = thrCount; // максимум прогрессбара равен кол-ву потоков
 
            GUIController.appendLog("MAIN", "RUNNING " + thrCount + " THREADS"); // пишем в лог, что запускаем n потоков
            for (int i = 0; i < thrCount; i++)
            {
                ThreadJob TJ = new ThreadJob(); // инициализируем класс с работой
                TJ.ThreadCompleted += ThreadCompleted; // важно! ставим обработчик на Event о выходе
 
                Thread thr = new Thread(TJ.Job); // создаем новый поток, который будет выполнять функцию из класса с работой
                thr.Name = (i + 1).ToString(); // за имя потока я обычно ставлю его номер 
                thr.Start(); // стартуем поток
 
                threads.Add(thr); // добавляем его в список запущенных
 
            }
 
            GUIController.startButtonEnabled(false); // все запущены, вырубаем кнопку старта
            GUIController.stopButtonEnabled(true); // врубаем кнопку стоп
            GUIController.updateAliveThreadsLabel(thrCount);
 
        }
 
        private void button2_Click(object sender, EventArgs e) // остановка потоков
        {
            for (int i = 0; i < threads.Count; i++) // каждый из запущенных потоков
            {
                Thread thrd = threads[i];
                thrd.Abort(); // остановить 
                threads.Remove(thrd); // убрать из списка запущенных
            }
 
            GUIController.updateAliveThreadsLabel(threads.Count); // обновляем кол-во запущенных
            GUIController.startButtonEnabled(true); // кнопки
            GUIController.stopButtonEnabled(false);
 
        }
 
 
    }
}

Скачать проект: ThreadingTest.rar (60 кб)
Если у вас остались какие-то вопросы, то отвечу на них в комментариях.