Пошаговое руководство. Разработка простого многопоточного компонента с помощью Visual C#
Обновлен: Ноябрь 2007
Компонент BackgroundWorker заменяет аналогичный код из пространства имен System.Threading и расширяет его функциональные возможности, но при необходимости исходное пространство имен System.Threading можно сохранить для обеспечения обратной совместимости и использования в будущем. Дополнительные сведения см. в разделе Общие сведения о компоненте BackgroundWorker.
Можно написать приложение, которое будет одновременно выполнять несколько задач. Эта возможность (называемая многопоточностью или созданием свободных потоков) является мощным средством разработки компонентов, для которых требуются значительные процессорные ресурсы и ввод данных пользователем. Примером компонента, в котором можно использовать многопоточность, служит компонент, производящий расчет заработной платы. Этот компонент может в одном потоке обрабатывать данные, введенные пользователем в базу данных, в то время как в другом потоке будут выполняться вычисления, потребляющие значительные ресурсы процессора. При запуске этих действий в отдельных потоках пользователю не нужно ждать, пока компьютер закончит вычисления, чтобы ввести следующие данные. В данном пошаговом руководстве создается простой многопоточный компонент, который выполняет одновременно несколько сложных вычислений.
Создание проекта
Приложение будет состоять из одной формы и компонента. Пользователь будет вводить значения и сообщать компоненту о необходимости начать вычисления. Форма будет получать из компонента значения и отображать их в элементах управления "Label". Компонент будет выполнять вычисления, занимающие процессор, и сообщать форме о завершении. Для хранения значений, полученных из интерфейса пользователя, в компоненте следует создать общие переменные. В компоненте следует также реализовать методы для выполнения вычислений на основе значений этих переменных.
Примечание. |
---|
Несмотря на то что в качестве метода, вычисляющего значение, обычно используется функция, аргументы между потоками передаваться не могут и значения не возвращаются. Существует множество простых способов передачи значений потокам и получения значений из них. В этом примере значения будут возвращаться в интерфейс пользователя путем обновления общих переменных, а для уведомления основной программы о завершении выполнения потока будут использоваться события. Отображаемые диалоговые окна и команды меню могут отличаться от описанных в справке в зависимости от текущих параметров или выпуска среды. Для изменения параметров выберите Импорт и экспорт параметров в меню Сервис. Дополнительные сведения см. в разделе Параметры Visual Studio. |
Чтобы создать форму, выполните следующие действия.
Создайте новый проект Приложение Windows.
Задайте для приложения имя Calculations и переименуйте форму Form1.cs в frmCalculations.cs. Получив приглашение от Visual Studio переименовать элемент кода Form1, нажмите кнопку Да.
Эта форма будет служить в приложении основным интерфейсом пользователя.
Добавьте в форму пять элементов управления Label, четыре элемента управления Button и один элемент управления TextBox.
Задайте свойства для этих элементов следующим образом.
Элемент управления
Имя
Текст
label1
lblFactorial1
(пусто)
label2
lblFactorial2
(пусто)
label3
lblAddTwo
(пусто)
label4
lblRunLoops
(пусто)
label5
lblTotalCalculations
(пусто)
button1
btnFactorial1
Факториал
button2
btnFactorial2
Факториал - 1
button3
btnAddTwo
Прибавить два
button4
btnRunLoops
Выполнить цикл
textBox1
txtValue
(пусто)
Чтобы создать компонент "Калькулятор", выполните следующие действия.
В меню Проект выберите Добавить компонент.
Задайте компоненту имя Calculator.
Чтобы добавить в компонент "Калькулятор" общие переменные, выполните следующие действия.
Откройте Редактор кода для компонента Calculator.
Добавьте операторы для создания общих переменных, которые будут использоваться для передачи значений из формы frmCalculations в каждый поток.
Переменная varTotalCalculations будет хранить текущее общее количество вычислений, выполненных компонентом, а остальные переменные будут получать значения из формы.
public int varAddTwo; public int varFact1; public int varFact2; public int varLoopValue; public double varTotalCalculations = 0;
Чтобы добавить в компонент "Калькулятор" методы и события, выполните следующие действия.
Объявите делегаты для событий, которые будут использоваться компонентом для передачи значений в форму.
Примечание. Несмотря на то что будет объявлено четыре события, должны быть созданы только три делегата, поскольку два события будут иметь одинаковые подписи.
Сразу под объявлением переменных, сделанным на предыдущем шаге, введите следующий код.
// This delegate will be invoked with two of your events. public delegate void FactorialCompleteHandler(double Factorial, double TotalCalculations); public delegate void AddTwoCompleteHandler(int Result, double TotalCalculations); public delegate void LoopCompleteHandler(double TotalCalculations, int Counter);
Объявите события, используемые компонентом для связи с приложением. Для этого добавьте следующий код непосредственно под кодом, введенным на предыдущем шаге.
public event FactorialCompleteHandler FactorialComplete; public event FactorialCompleteHandler FactorialMinusOneComplete; public event AddTwoCompleteHandler AddTwoComplete; public event LoopCompleteHandler LoopComplete;
Сразу после предыдущего кода введите следующий код.
// This method will calculate the value of a number minus 1 factorial // (varFact2-1!). public void FactorialMinusOne() { double varTotalAsOfNow = 0; double varResult = 1; // Performs a factorial calculation on varFact2 - 1. for (int varX = 1; varX <= varFact2 - 1; varX++) { varResult *= varX; // Increments varTotalCalculations and keeps track of the current // total as of this instant. varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } // Signals that the method has completed, and communicates the // result and a value of total calculations performed up to this // point. FactorialMinusOneComplete(varResult, varTotalAsOfNow); } // This method will calculate the value of a number factorial. // (varFact1!) public void Factorial() { double varResult = 1; double varTotalAsOfNow = 0; for (int varX = 1; varX <= varFact1; varX++) { varResult *= varX; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } FactorialComplete(varResult, varTotalAsOfNow); } // This method will add two to a number (varAddTwo+2). public void AddTwo() { double varTotalAsOfNow = 0; int varResult = varAddTwo + 2; varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; AddTwoComplete(varResult, varTotalAsOfNow); } // This method will run a loop with a nested loop varLoopValue times. public void RunALoop() { int varX; double varTotalAsOfNow = 0; for (varX = 1; varX <= varLoopValue; varX++) { // This nested loop is added solely for the purpose of slowing down // the program and creating a processor-intensive application. for (int varY = 1; varY <= 500; varY++) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; } } LoopComplete(varTotalAsOfNow, varLoopValue); }
Передача введенных пользователем данных в компонент
Следующим шагом является добавление в форму frmCalculations кода для получения введенных пользователем данных, а также для обмена значениями с компонентом Calculator.
Чтобы реализовать в форме "frmCalculations" интерфейс пользователя, выполните следующие действия.
Откройте форму frmCalculations в Редакторе кода.
Найдите оператор public partial class frmCalculations. Непосредственно после знака { введите:
Calculator Calculator1;
Найдите конструктор. Непосредственно перед знаком } добавьте следующую строку.
// Creates a new instance of Calculator. Calculator1 = new Calculator();
В окне конструктора нажмите поочередно каждую кнопку, чтобы создать структуру кода для обработчика события Click для этой кнопки и добавить код обработчика.
В результате обработчики событий Click должны выглядеть следующим образом.
// Passes the value typed in the txtValue to Calculator.varFact1. private void btnFactorial1_Click(object sender, System.EventArgs e) { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = false; Calculator1.Factorial(); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; Calculator1.FactorialMinusOne(); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; Calculator1.AddTwo(); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; Calculator1.RunALoop(); }
После кода, добавленного на предыдущем шаге, введите указанный ниже код для обработки событий, которые форма будет получать из компонента Calculator1.
private void FactorialHandler(double Value, double Calculations) // Displays the returned value in the appropriate label. { lblFactorial1.Text = Value.ToString(); // Re-enables the button so it can be used again. btnFactorial1.Enabled = true; // Updates the label that displays the total calculations performed lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void FactorialMinusHandler(double Value, double Calculations) { lblFactorial2.Text = Value.ToString(); btnFactorial2.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void AddTwoHandler(int Value, double Calculations) { lblAddTwo.Text = Value.ToString(); btnAddTwo.Enabled = true; lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); } private void LoopDoneHandler(double Calculations, int Count) { btnRunLoops.Enabled = true; lblRunLoops.Text = Count.ToString(); lblTotalCalculations.Text = "TotalCalculations are " + Calculations.ToString(); }
В окне конструктора формы frmCalculations непосредственно перед знаком } добавьте следующий код для обработки пользовательских событий, которые форма будет получать из компонента Calculator1.
Calculator1.FactorialComplete += new Calculator.FactorialCompleteHandler(this.FactorialHandler); Calculator1.FactorialMinusOneComplete += new Calculator.FactorialCompleteHandler(this.FactorialMinusHandler); Calculator1.AddTwoComplete += new Calculator.AddTwoCompleteHandler(this.AddTwoHandler); Calculator1.LoopComplete += new Calculator.LoopCompleteHandler(this.LoopDoneHandler);
Проверка работы приложения
На этом этапе создан проект, который содержит форму и компонент, предназначенный для выполнения некоторых сложных вычислений. Несмотря на то что многопоточность еще не реализована, сейчас следует проверить работоспособность проекта.
Чтобы проверить проект, выполните следующие действия.
В меню Отладка выберите команду Начать отладку.
Приложение начнет работу и появится форма frmCalculations.
В текстовом поле введите 4, а затем нажмите кнопку с надписью Прибавить два.
Под кнопкой должна появиться цифра "6", а в метке lblTotalCalculations должен быть отображен текст "Всего вычислений — 1".
Теперь нажмите кнопку с надписью Факториал - 1.
Под кнопкой должна появиться цифра 6, а метка lblTotalCalculations теперь содержит текст "Всего вычислений — 4".
Измените значение текстового поля на 20, а затем нажмите кнопку с надписью Факториал.
Под кнопкой должно появиться число "2.43290200817664E+18", а метка lblTotalCalculations теперь содержит текст "Всего вычислений — 24".
Измените значение текстового поля на 50000, а затем нажмите кнопку с надписью Выполнить цикл.
Заметьте, что перед тем, как кнопка вновь станет доступна, пройдет небольшой, но заметный интервал времени. В метке под кнопкой должно отображаться "50000", а общее количество вычислений теперь равно 25000024.
Измените значение текстового поля на 50000, нажмите кнопку с надписью Выполнить цикл и непосредственно после этого нажмите кнопку Добавить два. Нажмите кнопку еще раз.
Кнопка не будет реагировать (так же как и все остальные элементы управления в форме), пока не завершится цикл.
Если в программе запускается только один поток выполнения, занимающие процессор вычисления (аналогичные приведенным выше) могут приостанавливать программу, пока они не будут закончены. В следующем разделе приложение будет дополнено поддержкой многопоточности, в результате чего несколько потоков смогут выполняться одновременно.
Добавление поддержки многопоточности
В предыдущем примере были показаны ограничения, характерные для приложений, в которых запускается только один поток выполнения. В следующем разделе в компонент будет добавлено несколько потоков выполнения с помощью класса Thread.
Чтобы добавить подпрограмму "Threads", выполните следующие действия.
Откройте файл Calculator.cs в Редакторе кода.
В начале кода найдите объявление класса и сразу после знака { введите следующий код.
// Declares the variables you will use to hold your thread objects. public System.Threading.Thread FactorialThread; public System.Threading.Thread FactorialMinusOneThread; public System.Threading.Thread AddTwoThread; public System.Threading.Thread LoopThread;
Непосредственно перед концом объявления класса в нижней части кода добавьте следующий метод.
public void ChooseThreads(int threadNumber) { // Determines which thread to start based on the value it receives. switch(threadNumber) { case 1: // Sets the thread using the AddressOf the subroutine where // the thread will start. FactorialThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.Factorial)); // Starts the thread. FactorialThread.Start(); break; case 2: FactorialMinusOneThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.FactorialMinusOne)); FactorialMinusOneThread.Start(); break; case 3: AddTwoThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.AddTwo)); AddTwoThread.Start(); break; case 4: LoopThread = new System.Threading.Thread(new System.Threading.ThreadStart(this.RunALoop)); LoopThread.Start(); break; } }
Когда будет создан экземпляр объекта Thread, ему потребуется аргумент в виде объекта ThreadStart. Объект ThreadStart — это делегат, указывающий на адрес метода, в котором начинается поток. Объект ThreadStart не может принимать параметры или передавать значения, и поэтому он может определять только метод void. Реализованный здесь метод ChooseThreads будет получать значение из вызывающей его программы и использовать это значение для определения запускаемого потока.
Чтобы добавить соответствующий код в форму "frmCalculations", выполните следующие действия.
Откройте файл frmCalculations.cs в Редакторе кода и найдите метод private void btnFactorial1_Click.
Преобразуйте строку, которая вызывает метод Calculator1.Factorial1, в комментарий следующим образом.
// Calculator1.Factorial()
Добавьте следующую строку для вызова метода Calculator1.ChooseThreads.
// Passes the value 1 to Calculator1, thus directing it to start the // correct thread. Calculator1.ChooseThreads(1);
Внесите аналогичные изменения в другие методы button_click.
Примечание. Убедитесь в том, что для аргумента Threads задано соответствующее значение.
В результате код должен выглядеть примерно следующим образом.
private void btnFactorial1_Click(object sender, System.EventArgs e) // Passes the value typed in the txtValue to Calculator.varFact1 { Calculator1.varFact1 = int.Parse(txtValue.Text); // Disables the btnFactorial1 until this calculation is complete btnFactorial1.Enabled = false; // Calculator1.Factorial(); Calculator1.ChooseThreads(1); } private void btnFactorial2_Click(object sender, System.EventArgs e) { Calculator1.varFact2 = int.Parse(txtValue.Text); btnFactorial2.Enabled = false; // Calculator1.FactorialMinusOne(); Calculator1.ChooseThreads(2); } private void btnAddTwo_Click(object sender, System.EventArgs e) { Calculator1.varAddTwo = int.Parse(txtValue.Text); btnAddTwo.Enabled = false; // Calculator1.AddTwo(); Calculator1.ChooseThreads(3); } private void btnRunLoops_Click(object sender, System.EventArgs e) { Calculator1.varLoopValue = int.Parse(txtValue.Text); btnRunLoops.Enabled = false; // Lets the user know that a loop is running lblRunLoops.Text = "Looping"; // Calculator1.RunALoop(); Calculator1.ChooseThreads(4); }
Маршалинг элементов управления
Теперь можно легко обновить отображение формы. Поскольку элементы управления всегда принадлежат главному потоку выполнения, любой вызов элемента управления из подчиненного потока требует обращения к процедуре маршалинга. Маршалинг — это процесс передачи вызова через границы потоков, который потребляет значительный объем ресурсов. Чтобы снизить до минимума объем необходимых операций маршалинга, а также устранить возможность возникновения конфликтов между потоками при обработке вызовов, следует использовать метод Control.BeginInvoke для вызова методов в главном потоке выполнения. Такой способ вызова необходим при обращении к методам, работающим с элементами управления. Дополнительные сведения см. в разделе Практическое руководство. Управление элементами управления из потоков.
Чтобы создать процедуры для вызова элементов управления, выполните следующие действия.
Откройте редактор кода для формы frmCalculations. В разделе объявлений добавьте следующий код.
public delegate void FHandler(double Value, double Calculations); public delegate void A2Handler(int Value, double Calculations); public delegate void LDHandler(double Calculations, int Count);
В методах Invoke и BeginInvoke в качестве аргумента должен быть указан делегат для вызываемого метода. Эти строки объявляют подписи делегатов, которые будут использоваться методом BeginInvoke для вызова соответствующих методов.
Добавьте в код следующие пустые методы.
public void FactHandler(double Value, double Calculations) { } public void Fact1Handler(double Value, double Calculations) { } public void Add2Handler(int Value, double Calculations) { } public void LDoneHandler(double Calculations, int Count) { }
В меню Правка с помощью команд Вырезать и Вставить вырежьте весь код метода FactorialHandler и вставьте его в метод FactHandler.
Повторите предыдущий шаг для методов FactorialMinusHandler, Fact1Handler, AddTwoHandler, Add2Handler, LoopDoneHandler и LDoneHandler.
В результате в методах FactorialHandler, Factorial1Handler, AddTwoHandler и LoopDoneHandler код остаться не должен. Он должен быть перемещен в соответствующие новые методы.
Для асинхронного вызова методов вызовите метод BeginInvoke. Метод BeginInvoke можно вызвать либо из самой формы (this), либо из любого элемента управления в форме.
В результате код должен выглядеть примерно следующим образом.
protected void FactorialHandler(double Value, double Calculations) { // BeginInvoke causes asynchronous execution to begin at the address // specified by the delegate. Simply put, it transfers execution of // this method back to the main thread. Any parameters required by // the method contained at the delegate are wrapped in an object and // passed. this.BeginInvoke(new FHandler(FactHandler), new Object[] {Value, Calculations}); } protected void FactorialMinusHandler(double Value, double Calculations) { this.BeginInvoke(new FHandler(Fact1Handler), new Object [] {Value, Calculations}); } protected void AddTwoHandler(int Value, double Calculations) { this.BeginInvoke(new A2Handler(Add2Handler), new Object[] {Value, Calculations}); } protected void LoopDoneHandler(double Calculations, int Count) { this.BeginInvoke(new LDHandler(LDoneHandler), new Object[] {Calculations, Count}); }
Может показаться, что обработчик событий просто вызывает очередной метод. На самом деле, обработчик событий инициирует метод в главном потоке операций. Этот же подход сохраняется и для вызовов через границы потоков, и позволяет многопоточным приложениям работать эффективно, не вызывая блокировку. Подробные сведения о работе с этими элементами управления в многопоточной среде см. в разделе Практическое руководство. Управление элементами управления из потоков.
Сохраните результаты работы.
Проверьте решение, выбрав команду Начать отладку в меню Отладка.
Введите в текстовое поле значение 10000000 и нажмите кнопку Выполнить цикл.
В метке под кнопкой будет отображен текст "Выполнение цикла". Выполнение этого цикла занимает значительное количество времени. Если он завершается слишком рано, увеличьте число соответствующим образом.
Быстро нажмите подряд все три кнопки, которые пока доступны. Все кнопки отреагируют на ввод данных. Первым должен появиться результат в метке под кнопкой Прибавить два. Следующими появятся результаты в метках под кнопками факториалов. Результатом в этих случаях будет бесконечность, поскольку число, возвращаемое при вычислении факториала для 10 000 000, слишком велико для хранения в переменной с двойной точностью. Затем после некоторой задержки появятся результаты под кнопкой с надписью Выполнить цикл.
Таким образом, четыре отдельных группы вычислений были выполнены одновременно в четырех отдельных потоках. Интерфейс пользователя мог реагировать на ввод данных, и результаты возвращались после завершения работы каждого потока.
Координирование потоков
Опытный пользователь многопоточных приложений может заметить во введенном коде небольшие ошибки. Рассмотрим вновь следующие строки кода, имеющиеся в каждом методе вычислений в файле Calculator:
varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;
Эти две строки кода увеличивают общую переменную varTotalCalculations и присваивают ее значение локальной переменной varTotalAsOfNow. Это значение затем возвращается в форму frmCalculations и отображается в метке. Однако неизвестно, будет ли возвращено правильное значение. Если работает только один поток выполнения, то будет возвращено правильное значение. Однако если работают несколько потоков, правильность значения гарантировать нельзя. Каждый поток может увеличивать переменную varTotalCalculations. После того как один поток увеличит значение этой переменной, но перед тем как оно скопируется в varTotalAsOfNow, другой поток может также увеличить значение этой переменной. В итоге становится возможным, что каждый из потоков сообщит неточные результаты. В состав Visual C# входит Оператор lock (Справочник по C#), обеспечивающий синхронизацию потоков. Это гарантирует точность результатов, возвращаемых каждым потоком. Синтаксис оператора lock выглядит следующим образом.
lock(AnObject)
{
// Insert code that affects the object.
// Insert more code that affects the object.
// Insert more code that affects the object.
// Release the lock.
}
Если введен блок lock, выполнение указанного выражения блокируется до тех пор, пока данный поток не снимет монопольную блокировку с рассматриваемого объекта. В приведенном выше примере выполнение блокируется для объекта AnObject. Оператор lock следует применять к объекту, который возвращает ссылку, а не значение. Выполнение может затем продолжиться в виде блока, защищенного от воздействия со стороны других потоков. Набор операторов, которые выполняются как единый блок, называется атомарным. При появлении знака } выражение освобождается и потоки могут продолжать работу.
Чтобы добавить оператор "lock" в приложение, выполните следующие действия.
Откройте файл Calculator.cs в Редакторе кода.
Найдите каждый экземпляр следующего кода:
varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations;
Должно присутствовать четыре экземпляра этого кода, по одному для каждого метода вычислений.
Измените этот код следующим образом.
lock(this) { varTotalCalculations += 1; varTotalAsOfNow = varTotalCalculations; }
Сохраните результаты работы и проверьте их, как в предыдущем примере.
Можно заметить небольшое изменение в быстродействии программы. Это связано с тем, что выполнение потоков прекращается, когда в компоненте устанавливается монопольная блокировка. Несмотря на то что этот подход обеспечивает точность, он препятствует использованию некоторых преимуществ многопоточной обработки. Нужно осторожно относиться к блокировке потоков и применять ее только в случае крайней необходимости.
См. также
Задачи
Практическое руководство. Координирование нескольких потоков выполнения
Пошаговое руководство. Разработка простого многопоточного компонента с использованием Visual Basic
Основные понятия
Обзор асинхронной модели, основанной на событиях
Ссылки
Другие ресурсы
Программирование с использованием компонентов