Пишем простое многопоточное приложение Привет. Перед прочтением, сразу хочу предупредить, что не являюсь профессиональным C#-программистом. Я не знаю насколько код, который я пишу, правильный, возможно эти вещи надо делать по-другому. В этой статье я просто покажу способ, которым пользуюсь сам. Если бы такая статья была тогда, когда я только начинал играть с потоками, то, скорее всего, я бы освоил их несколько быстрее. Итак, так как мы пишем абстрактное приложение, то никакого полезного функционала оно делать не будет. Давайте просто разметим, что хотелось бы видеть в любом многопоточном приложении. Выставление количества запускаемых потоков. Возможность не только запуска, но и остановки потоков. Лог файл, в который потоки будут писать выполняемые действия. Контролы, которые будут говорить о кол-ве выполненных / невыполненных заданий. Контрол, который будет показывать кол-во работающих потоков. Прогрессбар выполнения общей работы. Давайте накидаем это всё необходимое на форму. У меня получилось вот так: Теперь перейдем к написанию кода. В первую очередь надо сказать о том, что потоки не имеют доступа к 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 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. Это сделать очень легко: 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(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 в которую при добавлении — потоки записываются, а при выходе — удаляются. Это позволяет контроллировать любой из потоков (нам же это нужно для кол-ва активных + остановки). Комментированный код всего основного кода: 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<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 кб) Антон Антонов http://bafoed.net/post/7624/
Спасибо за код. static Класс создавать не обязательно, пример для добавления строки в лог: private void log(string s,string account) { BeginInvoke(new Action(() => { textBox1.AppendText(DateTime.Now.Hour+":"+DateTime.Now.Minute+" - "+ account+": "+s + "\r\n"); })); }
Добавлю немного от себя, invok'и вовсе не обязательны, нужно лишь св-во формы CheckForIllegalCrossThreadCalls поставить в false и работать с контролами как с потока gui.
И потом радоваться куче возможных проблем, стандартные UI котролы НЕ Thread-Safe, не говоря уже о: Статья ниочем, каждый нуб овладевший простым навыками сразу же пытается ими поделиться, подобными писульками интернеты уже забиты.
Врут, у меня все окей(правда потоки обращаются только к ProgressBar'у, к остальным у меня нет необходимости обращаться).