Пошаговое руководство. Разработка простого многопоточного компонента с использованием Visual Basic
Обновлен: Ноябрь 2007
Компонент BackgroundWorker заменяет аналогичный код из пространства имен System.Threading и расширяет его функциональные возможности, но при необходимости исходное пространство имен System.Threading можно сохранить для обеспечения обратной совместимости и использования в будущем. Дополнительные сведения см. в разделе Общие сведения о компоненте BackgroundWorker.
Можно написать приложение, которое будет одновременно выполнять несколько задач. Эта возможность (называемая многопоточностью или созданием свободных потоков) является мощным средством разработки компонентов, для которых требуются значительные процессорные ресурсы и ввод данных пользователем. Примером компонента, в котором можно использовать многопоточность, служит компонент, производящий расчет заработной платы. Этот компонент может в одном потоке обрабатывать данные, введенные пользователем в базу данных, в то время как в другом потоке будут выполняться вычисления, потребляющие значительные ресурсы процессора. При запуске этих действий в отдельных потоках пользователю не нужно ждать, пока компьютер закончит вычисления, чтобы ввести следующие данные. В данном пошаговом руководстве создается простой многопоточный компонент, который выполняет одновременно несколько сложных вычислений.
Создание проекта
Приложение будет состоять из одной формы и компонента. Пользователь будет вводить значения и сообщать компоненту о необходимости начать вычисления. Форма будет получать из компонента значения и отображать их в элементах управления "Label". Компонент будет выполнять вычисления, занимающие процессор, и сообщать форме о завершении. Для хранения значений, полученных из интерфейса пользователя, в компоненте следует создать общие переменные. В компоненте следует также реализовать методы для выполнения вычислений на основе значений этих переменных.
Примечание. |
---|
Несмотря на то что в качестве метода, вычисляющего значение, обычно используется функция, аргументы между потоками передаваться не могут и значения не возвращаются. Существует множество простых способов передачи значений потокам и получения значений из них. В этом примере значения будут возвращаться в интерфейс пользователя путем обновления общих переменных, а для уведомления основной программы о завершении выполнения потока будут использоваться события. Отображаемые диалоговые окна и команды меню могут отличаться от описанных в справке в зависимости от текущих параметров или выпуска среды. Для изменения параметров выберите Импорт и экспорт параметров в меню Сервис. Дополнительные сведения см. в разделе Параметры Visual Studio. |
Чтобы создать форму, выполните следующие действия.
Создайте новый проект Приложение Windows.
Задайте для приложения имя Calculations и переименуйте форму Form1.vb в frmCalculations.vb.
Получив приглашение от 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 varAddTwo As Integer Public varFact1 As Integer Public varFact2 As Integer Public varLoopValue As Integer Public varTotalCalculations As Double = 0
Чтобы добавить в компонент "Калькулятор" методы и события, выполните следующие действия.
Объявите события, которые будут использоваться компонентом для передачи значений в форму. Сразу под объявлением переменных, сделанным на предыдущем шаге, введите следующий код.
Public Event FactorialComplete(ByVal Factorial As Double, ByVal _ TotalCalculations As Double) Public Event FactorialMinusComplete(ByVal Factorial As Double, ByVal _ TotalCalculations As Double) Public Event AddTwoComplete(ByVal Result As Integer, ByVal _ TotalCalculations As Double) Public Event LoopComplete(ByVal TotalCalculations As Double, ByVal _ Counter As Integer)
Непосредственно после объявления переменных, сделанного на шаге 1, введите следующий код.
' This sub will calculate the value of a number minus 1 factorial ' (varFact2-1!). Public Sub FactorialMinusOne() Dim varX As Integer = 1 Dim varTotalAsOfNow As Double Dim varResult As Double = 1 ' Performs a factorial calculation on varFact2 - 1. For varX = 1 to varFact2 - 1 varResult *= varX ' Increments varTotalCalculations and keeps track of the current ' total as of this instant. varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations Next varX ' Signals that the method has completed, and communicates the ' result and a value of total calculations performed up to this ' point RaiseEvent FactorialMinusComplete(varResult, varTotalAsOfNow) End Sub ' This sub will calculate the value of a number factorial (varFact1!). Public Sub Factorial() Dim varX As Integer = 1 Dim varResult As Double = 1 Dim varTotalAsOfNow As Double = 0 For varX = 1 to varFact1 varResult *= varX varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations Next varX RaiseEvent FactorialComplete(varResult, varTotalAsOfNow) End Sub ' This sub will add two to a number (varAddTwo + 2). Public Sub AddTwo() Dim varResult As Integer Dim varTotalAsOfNow As Double varResult = varAddTwo + 2 varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations RaiseEvent AddTwoComplete(varResult, varTotalAsOfNow) End Sub ' This method will run a loop with a nested loop varLoopValue times. Public Sub RunALoop() Dim varX As Integer Dim varY As Integer Dim varTotalAsOfNow As Double For varX = 1 To varLoopValue ' This nested loop is added solely for the purpose of slowing ' down the program and creating a processor-intensive ' application. For varY = 1 To 500 varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations Next Next RaiseEvent LoopComplete(varTotalAsOfNow, varX - 1) End Sub
Передача введенных пользователем данных в компонент
Следующим шагом является добавление в форму frmCalculations кода для получения введенных пользователем данных, а также для обмена значениями с компонентом Calculator.
Чтобы реализовать в форме "frmCalculations" интерфейс пользователя, выполните следующие действия.
В меню Построение выберите Построить решение.
Откройте форму frmCalculations в конструкторе Windows Forms.
В Панели элементов перейдите на вкладку Компоненты вычислений. Перетащите компонент Калькулятор на поверхность разработки.
В окне Свойства нажмите кнопку События.
Дважды щелкните каждое из четырех событий, чтобы создать обработчики событий в форме frmCalculations. После создания каждого обработчика необходимо вернуться в конструктор.
Чтобы обработать события, получаемые формой от компонента Calculator1, вставьте следующий код:
Private Sub Calculator1_AddTwoComplete(ByVal Result As System.Int32, ByVal TotalCalculations As System.Double) Handles Calculator1.AddTwoComplete lblAddTwo.Text = Result.ToString btnAddTwo.Enabled = True lblTotalCalculations.Text = "TotalCalculations are " & _ TotalCalculations.ToString End Sub Private Sub Calculator1_FactorialComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialComplete ' Displays the returned value in the appropriate label. lblFactorial1.Text = Factorial.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 " & _ TotalCalculations.ToString End Sub Private Sub Calculator1_FactorialMinusComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialMinusComplete lblFactorial2.Text = Factorial.ToString btnFactorial2.Enabled = True lblTotalCalculations.Text = "TotalCalculations are " & _ TotalCalculations.ToString End Sub Private Sub Calculator1_LoopComplete(ByVal TotalCalculations As System.Double, ByVal Counter As System.Int32) Handles Calculator1.LoopComplete btnRunLoops.Enabled = True lblRunLoops.Text = Counter.ToString lblTotalCalculations.Text = "TotalCalculations are " & _ TotalCalculations.ToString End Sub
В нижней части редактора кода найдите оператор End Class. Непосредственно над ним добавьте следующий код для обработки нажатий кнопок:
Private Sub btnFactorial1_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnFactorial1.Click ' Passes the value typed in the txtValue to Calculator.varFact1. Calculator1.varFact1 = CInt(txtValue.Text) ' Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = False Calculator1.Factorial() End Sub Private Sub btnFactorial2_Click(ByVal sender As Object, ByVal e _ As System.EventArgs) Handles btnFactorial2.Click Calculator1.varFact2 = CInt(txtValue.Text) btnFactorial2.Enabled = False Calculator1.FactorialMinusOne() End Sub Private Sub btnAddTwo_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnAddTwo.Click Calculator1.varAddTwo = CInt(txtValue.Text) btnAddTwo.Enabled = False Calculator1.AddTwo() End Sub Private Sub btnRunLoops_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnRunLoops.Click Calculator1.varLoopValue = CInt(txtValue.Text) btnRunLoops.Enabled = False ' Lets the user know that a loop is running. lblRunLoops.Text = "Looping" Calculator1.RunALoop() End Sub
Проверка работы приложения
На этом этапе создан проект, который содержит форму и компонент, предназначенный для выполнения некоторых сложных вычислений. Несмотря на то что многопоточность еще не реализована, сейчас следует проверить работоспособность проекта.
Чтобы проверить проект, выполните следующие действия.
В меню Отладка выберите команду Начать отладку. Приложение начнет работу и появится форма frmCalculations.
В текстовом поле введите 4, а затем нажмите кнопку с надписью Прибавить два.
Под кнопкой должна появиться цифра "6", а в метке lblTotalCalculations должен быть отображен текст "Всего вычислений – 1".
Теперь нажмите кнопку с надписью Факториал - 1.
Под кнопкой должна появиться цифра "6", а метка lblTotalCalculations теперь содержит текст "Всего вычислений — 4".
Измените значение текстового поля на 20, а затем нажмите кнопку с надписью Факториал.
Под кнопкой должно появиться число "2.43290200817664E+18", а метка lblTotalCalculations теперь содержит текст "Всего вычислений — 24".
Измените значение текстового поля на 50000, а затем нажмите кнопку с надписью Выполнить цикл.
Заметьте, что перед тем, как кнопка вновь станет доступна, пройдет небольшой, но заметный интервал времени. В метке под кнопкой должно отображаться "50000", а общее количество вычислений теперь равно 25000024.
Измените значение текстового поля на 50000, нажмите кнопку с надписью Выполнить цикл и непосредственно после этого нажмите кнопку Добавить два. Нажмите кнопку Добавить два еще раз.
Кнопка не будет реагировать (так же как и все остальные элементы управления в форме), пока не завершится цикл.
Если в программе запускается только один поток выполнения, занимающие процессор вычисления (аналогичные приведенным выше) могут приостанавливать программу, пока они не будут закончены. В следующем разделе приложение будет дополнено поддержкой многопоточности, в результате чего несколько потоков смогут выполняться одновременно.
Добавление поддержки многопоточности
В предыдущем примере были показаны ограничения, характерные для приложений, в которых запускается только один поток выполнения. В следующем разделе в компонент будет добавлено несколько потоков выполнения с помощью класса Thread.
Чтобы добавить подпрограмму "Threads", выполните следующие действия.
Откройте файл Calculator.vb в Редакторе кода. В верхней части кода найдите строку Public Class Calculator . Сразу после нее введите следующий код:
' Declares the variables you will use to hold your thread objects. Public FactorialThread As System.Threading.Thread Public FactorialMinusOneThread As System.Threading.Thread Public AddTwoThread As System.Threading.Thread Public LoopThread As System.Threading.Thread
Непосредственно перед оператором End Class в нижней части кода добавьте следующий метод:
Public Sub ChooseThreads(ByVal threadNumber As Integer) ' Determines which thread to start based on the value it receives. Select Case threadNumber Case 1 ' Sets the thread using the AddressOf the subroutine where ' the thread will start. FactorialThread = New System.Threading.Thread(AddressOf _ Factorial) ' Starts the thread. FactorialThread.Start() Case 2 FactorialMinusOneThread = New _ System.Threading.Thread(AddressOf FactorialMinusOne) FactorialMinusOneThread.Start() Case 3 AddTwoThread = New System.Threading.Thread(AddressOf AddTwo) AddTwoThread.Start() Case 4 LoopThread = New System.Threading.Thread(AddressOf RunALoop) LoopThread.Start() End Select End Sub
Когда будет создан экземпляр объекта Thread, ему потребуется аргумент в виде объекта ThreadStart. Объект ThreadStart — это делегат, указывающий на адрес подпрограммы, в которой начинается поток. Объект ThreadStart не может принимать параметры и передавать значения, и поэтому не может определять функцию. Оператор Оператор AddressOf возвращает объект-делегат, который служит объектом ThreadStart. Реализованная здесь подпрограмма ChooseThreads будет получать значение из вызывающей ее программы и использовать это значение для определения запускаемого потока.
Чтобы добавить в форму "frmCalculations" код запуска потока, выполните следующие действия.
Откройте файл frmCalculations.vb в Редакторе кода. Найдите подпрограмму Sub btnFactorial1_Click.
Преобразуйте строку, которая вызывает метод Calculator1.Factorial, в комментарий следующим образом.
' 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 Sub btnFactorial1_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnFactorial1.Click ' Passes the value typed in the txtValue to Calculator.varFact1. Calculator1.varFact1 = CInt(txtValue.Text) ' Disables the btnFactorial1 until this calculation is complete. btnFactorial1.Enabled = False ' Calculator1.Factorial() ' Passes the value 1 to Calculator1, thus directing it to start the ' Correct thread. Calculator1.ChooseThreads(1) End Sub Private Sub btnFactorial2_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnFactorial2.Click Calculator1.varFact2 = CInt(txtValue.Text) btnFactorial2.Enabled = False ' Calculator1.FactorialMinusOne() Calculator1.ChooseThreads(2) End Sub Private Sub btnAddTwo_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnAddTwo.Click Calculator1.varAddTwo = CInt(txtValue.Text) btnAddTwo.Enabled = False ' Calculator1.AddTwo() Calculator1.ChooseThreads(3) End Sub Private Sub btnRunLoops_Click(ByVal sender As Object, ByVal e As _ System.EventArgs) Handles btnRunLoops.Click Calculator1.varLoopValue = CInt(txtValue.Text) btnRunLoops.Enabled = False ' Lets the user know that a loop is running. lblRunLoops.Text = "Looping" ' Calculator1.RunALoop() Calculator1.ChooseThreads(4) End Sub
Маршалинг элементов управления
Теперь можно легко обновить отображение формы. Поскольку элементы управления всегда принадлежат главному потоку выполнения, любой вызов элемента управления из подчиненного потока требует обращения к процедуре маршалинга. Маршалинг — это процесс передачи вызова через границы потоков, который потребляет значительный объем ресурсов. Чтобы снизить до минимума объем необходимых операций маршалинга, а также устранить возможность возникновения конфликтов между потоками при обработке вызовов, следует использовать метод BeginInvoke для вызова методов в главном потоке выполнения. Такой способ вызова необходим при обращении к методам, работающим с элементами управления. Дополнительные сведения см. в разделе Практическое руководство. Управление элементами управления из потоков.
Чтобы создать процедуры для вызова элементов управления, выполните следующие действия.
Откройте Редактор кода для компонента frmCalculations. В разделе объявлений добавьте следующий код.
Public Delegate Sub FHandler(ByVal Value As Double, ByVal _ Calculations As Double) Public Delegate Sub A2Handler(ByVal Value As Integer, ByVal _ Calculations As Double) Public Delegate Sub LDhandler(ByVal Calculations As Double, ByVal _ Count As Integer)
В методах Invoke и BeginInvoke в качестве аргумента должен быть указан делегат для вызываемого метода. Эти строки объявляют подписи делегатов, которые будут использоваться методом BeginInvoke для вызова соответствующих методов.
Добавьте в код следующие пустые методы.
Public Sub FactHandler(ByVal Factorial As Double, ByVal TotalCalculations As _ Double) End Sub Public Sub Fact1Handler(ByVal Factorial As Double, ByVal TotalCalculations As _ Double) End Sub Public Sub Add2Handler(ByVal Result As Integer, ByVal TotalCalculations As _ Double) End Sub Public Sub LDoneHandler(ByVal TotalCalculations As Double, ByVal Counter As _ Integer) End Sub
В меню Правка с помощью команд Вырезать и Вставить вырежьте весь код метода Sub Calculator1_FactorialComplete и вставьте его в метод FactHandler.
Повторите предыдущий шаг для методов Calculator1_FactorialMinusComplete, Fact1Handler, Calculator1_AddTwoComplete, Add2Handler, Calculator1_LoopComplete и LDoneHandler.
В результате в методах Calculator1_FactorialComplete, Calculator1_FactorialMinusComplete, Calculator1_AddTwoComplete и Calculator1_LoopComplete код остаться не должен. Он должен быть перемещен в соответствующие новые методы.
Для асинхронного вызова методов вызовите метод BeginInvoke. Метод BeginInvoke можно вызвать либо из самой формы (me), либо из любого элемента управления в форме.
Private Sub Calculator1_FactorialComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialComplete ' 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. Me.BeginInvoke(New FHandler(AddressOf FactHandler), New Object() _ {Factorial, TotalCalculations }) End Sub Private Sub Calculator1_FactorialMinusComplete(ByVal Factorial As System.Double, ByVal TotalCalculations As System.Double) Handles Calculator1.FactorialMinusComplete Me.BeginInvoke(New FHandler(AddressOf Fact1Handler), New Object() _ { Factorial, TotalCalculations }) End Sub Private Sub Calculator1_AddTwoComplete(ByVal Result As System.Int32, ByVal TotalCalculations As System.Double) Handles Calculator1.AddTwoComplete Me.BeginInvoke(New A2Handler(AddressOf Add2Handler), New Object() _ { Result, TotalCalculations }) End Sub Private Sub Calculator1_LoopComplete(ByVal TotalCalculations As System.Double, ByVal Counter As System.Int32) Handles Calculator1.LoopComplete Me.BeginInvoke(New LDHandler(AddressOf Ldonehandler), New Object() _ { TotalCalculations, Counter }) End Sub
Может показаться, что обработчик событий просто вызывает очередной метод. На самом деле, обработчик событий инициирует метод в главном потоке операций. Этот же подход сохраняется и для вызовов через границы потоков, и позволяет многопоточным приложениям работать эффективно, не вызывая блокировку. Подробные сведения о работе с этими элементами управления в многопоточной среде см. в разделе Практическое руководство. Управление элементами управления из потоков.
Сохраните результаты работы.
Проверьте решение, выбрав команду Начать отладку в меню Отладка.
Введите в текстовое поле значение 10000000 и нажмите кнопку Выполнить цикл.
В метке под кнопкой будет отображен текст "Выполнение цикла". Выполнение этого цикла занимает значительное количество времени. Если он завершается слишком рано, увеличьте число соответствующим образом.
Быстро нажмите подряд все три кнопки, которые пока доступны. Все кнопки отреагируют на ввод данных. Первым должен появиться результат в метке под кнопкой Прибавить два. Следующими появятся результаты в метках под кнопками факториалов. Результатом в этих случаях будет бесконечность, поскольку число, возвращаемое при вычислении факториала для 10 000 000, слишком велико для хранения в переменной с двойной точностью. Затем после некоторой задержки появятся результаты под кнопкой с надписью Выполнить цикл.
Таким образом, четыре отдельных группы вычислений были выполнены одновременно в четырех отдельных потоках. Интерфейс пользователя мог реагировать на ввод данных, и результаты возвращались после завершения работы каждого потока.
Координирование потоков
Опытный пользователь многопоточных приложений может заметить во введенном коде небольшие ошибки. Рассмотрим вновь следующие строки кода, имеющиеся в каждой подпрограмме вычислений в компоненте Calculator:
varTotalCalculations += 1
varTotalAsOfNow = varTotalCalculations
Эти две строки кода увеличивают общую переменную varTotalCalculations и присваивают ее значение локальной переменной varTotalAsOfNow. Это значение затем возвращается в форму frmCalculations и отображается в метке. Однако неизвестно, будет ли возвращено правильное значение. Если работает только один поток выполнения, то будет возвращено правильное значение. Однако если работают несколько потоков, правильность значения гарантировать нельзя. Каждый поток может увеличивать переменную varTotalCalculations. После того как один поток увеличит значение этой переменной, но перед тем как оно скопируется в varTotalAsOfNow, другой поток может также увеличить значение этой переменной. В итоге становится возможным, что каждый из потоков сообщит неточные результаты. В состав Visual Basic входит оператор Оператор SyncLock, который обеспечивает синхронизацию потоков. Это гарантирует точность результатов, возвращаемых каждым потоком. Синтаксис оператора SyncLock выглядит следующим образом.
SyncLock AnObject
Insert code that affects the object
Insert some more
Insert even more
' Release the lock
End SyncLock
Если введен блок SyncLock, выполнение указанного выражения блокируется до тех пор, пока данный поток не снимет монопольную блокировку с рассматриваемого объекта. В приведенном выше примере выполнение блокируется для объекта AnObject. Оператор SyncLock следует применять к объекту, который возвращает ссылку, а не значение. Выполнение может затем продолжиться в виде блока, защищенного от воздействия со стороны других потоков. Набор операторов, которые выполняются как единый блок, называется атомарным. При появлении строки End SyncLock выражение освобождается и потоки могут продолжать работу.
Чтобы добавить в приложение оператор SyncLock, выполните следующие действия.
Откройте файл Calculator.vb в Редакторе кода.
Найдите каждый экземпляр следующего кода:
varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations
Должно присутствовать четыре экземпляра этого кода, по одному для каждого метода вычислений.
Измените этот код следующим образом.
SyncLock Me varTotalCalculations += 1 varTotalAsOfNow = varTotalCalculations End SyncLock
Сохраните результаты работы и проверьте их, как в предыдущем примере.
Можно заметить небольшое изменение в быстродействии программы. Это связано с тем, что выполнение потоков прекращается, когда в компоненте устанавливается монопольная блокировка. Несмотря на то что этот подход обеспечивает точность, он препятствует использованию некоторых преимуществ многопоточной обработки. Нужно осторожно относиться к блокировке потоков и применять ее только в случае крайней необходимости.
См. также
Задачи
Практическое руководство. Координирование нескольких потоков выполнения
Пошаговое руководство. Разработка простого многопоточного компонента с помощью Visual C#
Основные понятия
Обзор асинхронной модели, основанной на событиях
Ссылки
Другие ресурсы
Программирование с использованием компонентов