How to make BackgroundWorker result consistent - vb.net

I am using a background worker to poll digital IO state from my PLC with MODBUS through serial com port. The background worker is used for READ only operation with polling speed of 200 ms between each cycle. The number of I/O to read in one cycle is around 50 bits (1 io state = 1 boolean). I then simulate a WRITE command to the PLC by toggling all I/O to it's opposite state (ON -> OFF / OFF -> ON) from the UI thread. I put the Toggle() sub routine into a timer with an interval of 100 ms and execute for 3000 ms. The result is some of the ports are not consistent. The starting state is either all ON or all OFF. The expected result after the toggle is also either all ON or all OFF. Sometimes the end result is 45 io ON and 5 io OFF. Sometimes it is 40 io OFF and 10 io ON. The result is always random but never the correct result which is all io ON or all io OFF.
Interestingly, if I use Timer instead of background worker to do the polling, the toggle result is always consistent ( all 50 IO either ON or OFF ). I prefer to use background worker for the polling because the UI is much smoother. When I use timer, the UI feels a little bit "laggy". However, the background worker is causing inconsistent result.
Below is simplified version of my codes to illustrate the my situation. I hope somebody can point me to the right direction. The background worker is working fine polling the io state. But as soon as I manipulate the port from the UI thread, the command is not 100% guaranteed to be executed at all time which is a serious problem for me !
I put the MODBUS read operation in the DoWork sub routine and pass the read result to UI thread using the Backgroundworker.ReportProgress(0, objResult). Therefore, the read and write manipulation of the object are both happening in the same UI thread. Why the result is still inconsistent?
Public Class Bit
Private _LastValue As Boolean
Private _Label As Control
Private DictControl As New Dictionary(Of String, Control)
Sub SetCoil(val As Boolean)
'Do work here
'Update last value
SetLastValue(val)
End Sub
Function GetCoil() As Boolean
Dim result As Boolean = True
'Do work here
'Update last value
SetLastValue(result)
Return result
End Function
Sub Toggle()
Dim result As Boolean = Not _LastValue
'invert current value
'set current value = not current value
'Update last value
SetLastValue(result)
End Sub
Sub SetLastValue(val As Boolean)
_LastValue = val
Dim ctlText As String = ""
If _LastValue = True Then
ctlText = "ON"
Else
ctlText = "OFF"
End If
'Update label
For Each c As KeyValuePair(Of String, Control) In DictControl
c.Value.Text = ctlText
Next
End Sub
Sub AddControl(key As String, c As Control)
DictControl.Add(key, c)
End Sub
End Class
Public Class Main
Const BIT_COUNT = 50
Dim bgw As New System.ComponentModel.BackgroundWorker
Dim dictBit As New Dictionary(Of Integer, Bit)
Sub New()
'Create a bunch of label and bit instances
'Add label control to Bit
'Add Bit to dictBit dictionary
'Add Handler to Background Worker
'Run Background Worker
'Run Timer
End Sub
Sub bgw_DoWorkHandler(sender As Object, e As System.ComponentModel.DoWorkEventArgs)
Dim Coils(49) As Boolean
Dim result() As Boolean
Dim run As Boolean = True
Do
'Do some processing & assign Coils value
result = Coils.Clone 'Duplicate the variable
bgw.ReportProgress(0, result)
Sleep(300)
Loop While run = True
End Sub
Sub bgw_ProgressChanged(sender As Object, e As System.ComponentModel.ProgressChangedEventArgs)
Dim results() As Boolean = DirectCast(e.UserState, Boolean())
Dim i As Integer
For i = 0 To UBound(results)
If dictBit.ContainsKey(i) Then
dictBit(i).SetLastValue(results(i)) 'Set last value and update Label
End If
Next
End Sub
Sub TimerTickHandler(ByVal sender As Object, ByVal e As EventArgs)
'Background worker alternative.
Dim Coils(49) As Boolean
Dim result() As Boolean
Dim run As Boolean = True
'Do some work & assign Coils value
result = Coils.Clone
bgw.ReportProgress(0, result)
End Sub
Sub ToggleBits()
Dim i As Integer
For i = 0 To BIT_COUNT - 1
dictBit(i).Toggle()
Next
End Sub
Sub TimerToggleHandler(ByVal sender As Object, ByVal e As EventArgs)
'Interval = 100 ms. Run for 3000 ms before stop.
ToggleBits()
End Sub
End Class

The Background Worker is reading the IO during the toggle. That's why all 50 points aren't the same value.
Actual hardware inputs can change at any time. Even the a split second before or after your read. If this toggle routine is only for testing, then ignore the discrepancies. Otherwise, update a flag or fire an event at the end of the toggle. Anything to force the BW to wait until the toggle is complete before reading.

Related

Need a fix for my deadlocking issue with my parallel.foreach

When running my code I seem to encounter deadlocks while trying to update a GUI element from within one of the parallel tasks.
I've tried surrounding the Output function with "Synclock me" to try to ensure that only one task is trying to update the control at a time.
Private Sub RunParallel(records as list(of DataRecord), ou as String)
Dim ParallelOptions As New ParallelOptions
ParallelOptions.MaxDegreeOfParallelism = 10
Parallel.ForEach(records, ParallelOptions, Sub(myrecord)
ProcessRecord(myrecord, ou)
End Sub)
Output("Done...." & vbCrLf)
End Sub
Private Sub ProcessRecord(ByVal record As DataRecord, ByVal ou As String)
'Output($"BromcomID = {record("ID")}, Forename = {record("Forename")}{vbCrLf}")
Dim ud As New UserDetails With {
.EmployeeID = record("ID"),
.SamAccountName = record("SamAccountName"),
.GivenName = record("Forename"),
.Surname = record("Surname")
}
If Not CreateUser(ou, ud) Then
'Threading.Thread.Sleep(2000)
' Output($"Error creating {ud.EmployeeID}{vbCrLf}")
End If
End Sub
Private Sub Output(ByVal s As String)
SyncLock Me
If Me.InvokeRequired Then
Invoke(Sub()
Outbox.AppendText(s)
Outbox.SelectionStart = Len(Outbox.Text)
Outbox.ScrollToCaret()
Outbox.Select()
End Sub)
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Len(Outbox.Text)
Outbox.ScrollToCaret()
Outbox.Select()
End If
End SyncLock
End Sub
The code as supplied seems to run, but if I uncomment out the Output calls in the ProcessRecord() function, it hangs and never gets exits the Parallel.foreach
--- Update
After playing around with suggestions and comments on here I still can't get it to work correctly.
If I take out all of the output from ProcessRecord it seems to work correctly. However with the following code, it now seems to run each ProcessRecord sequentially (not 10 at a time as I intended), and then hangs after the last one.
Output("Dispatching" & vbCrLf)
Dim ParallelOptions As New ParallelOptions With {
.MaxDegreeOfParallelism = 10
}
Parallel.ForEach(recordList, ParallelOptions, Sub(myrecord)
ProcessRecord(myrecord, ou)
End Sub)
'For Each myrecord As DataRecord In recordList
' Task.Factory.StartNew(Sub() ProcessRecord(myrecord, ou))
'Next
Output("Done...." & vbCrLf)
End Sub
Private Sub ProcessRecord(ByVal record As DataRecord, ByVal ou As String)
Dim ud As New UserDetails With {
.EmployeeID = record("ID"),
.SamAccountName = record("SamAccountName"),
.GivenName = record("Forename"),
.Surname = record("Surname"),
.DisplayName = $"{record("Forename")} {record("Surname")} (Student)"}
If Not CreateUser(ou, ud) Then
' Output($"Error creating {ud.EmployeeID}{vbCrLf}")
End If
Output($"BromcomID = {record("ID")}, Forename = {record("Forename")}{vbCrLf}")
End Sub
Private Sub Output(ByVal s As String)
If Me.InvokeRequired Then
Invoke(Sub()
Output(s)
End Sub)
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Outbox.TextLength
Outbox.ScrollToCaret()
Outbox.Select()
Outbox.Refresh()
End If
End Sub
If I use the commented out Task.Factory code everything seems to work perfectly, except I cant control how many tasks at a time are launched, and I can't wait till all of them have finished, the for loop just launches all the tasks, and then carries on with the Output("Done....) line.
The synclock statements didn't seem to affect anything either way.
Give this a try
Private Sub Output(ByVal s As String)
If Me.InvokeRequired Then
Me.Invoke(Sub() Output(s))
'Me.BeginInvoke(Sub() Output(s))
Else
Outbox.AppendText(s)
Outbox.SelectionStart = Outbox.TextLength
Outbox.ScrollToCaret()
Outbox.Select()
Outbox.Refresh()
End If
End Sub
There may be an issue if you have events tied to Outbox, like text changed. Tested Output method with
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim nums As New List(Of Integer)
For x As Integer = 1 To 500
nums.Add(x)
Next
'because it is in a button, run from a task
Dim t As Task
t = Task.Run(Sub()
Parallel.ForEach(nums, Sub(num)
Output(num.ToString & Environment.NewLine)
End Sub)
End Sub)
End Sub
If you want to go ahead with using a Task-based approach, then you certainly can control how many are launched at a time, and wait for all of them to finish. It requires some additional code for the manual management. This is discussed in some detail in Microsoft documentation: https://learn.microsoft.com/en-us/dotnet/standard/asynchronous-programming-patterns/consuming-the-task-based-asynchronous-pattern
It's not necessarily a bad thing to initiate all of the tasks immediately, then you'll be leaving it to the thread pool to take care of how many to run at a time.
If you want greater control, you can use the "throttling" design from the link. In your "pending" queue, store delegates/lambdas that will themselves kick off Task.Run. Then, as you dequeue from the "pending" queue into the "active" list, you can Invoke on the delegate/lambda to get the Task and Await Task.WhenAny on the "active" list.
One potential benefit of doing things this way is that the work in each top-level Task can be split between UI work running on the UI thread and processor-limited work running on the thread pool.
(I'm not suggesting that this is necessarily the best option for you, just trying to expand on what you should be looking at doing if you really want to pursue using Task instead of Parallel.)

VB.net ContinueWith

I have this code which loops through all my accounts in my list and then does something to the accounts using tasks for each account as a way to speed up the process. Each time the program completes this action, I want the user interface to update the progress bar. I was using Invoke before but it isn't the best option and I couldn't get it working. Now I know this can be done using a background worker but this isn't the best way of making your application multithreaded so I used this. And instead of invoking I heard about ContinueWith but I can't seem to get it working and I get no error message just a red underline.
Code:
progressBar.Value = 0
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(Sub()
While checked = False
If proxies.Count = 0 Then
Exit Sub
'Also can't think of a good way to stop searching through accounts when there are no proxies left in my queue.
End If
Dim proxy As New WebProxy(proxies(0))
proxies.TryDequeue(0)
'Do something
End While
checkedAmount += 1
Dim progress As Integer = ((checkedAmount / combos.Count) * 100)
Task.ContinueWith(progressBar.Value = progress, TaskScheduler.FromCurrentSynchronizationContext()) 'Error here
End Sub)
tasks.Add(t)
Next
Task.WaitAll(tasks.ToArray())
I get no error code as shown here:
I have also tried putting a sub after and stuff but that lead to nothing.
Thanks for any help in advance.
Update tried with invoke:
Private Delegate Sub UpdateProgressBarDelegate(ByVal progressBarUpdate As ProgressBar, ByVal value As Integer)
Dim checkedAmount As Integer = 0
Dim checked As Boolean = False
Private Sub startBtn_Click(sender As Object, e As EventArgs) Handles startBtn.Click
progressBar.Value = 0
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(Sub()
While checked = False
proxies.TryDequeue(0)
'do stuff
End While
checkedAmount += 1
Dim progress As Integer = ((checkedAmount / combos.Count) * 100)
If Me.InvokeRequired = True Then
Me.Invoke(New UpdateProgressBarDelegate(AddressOf UpdateProgressBar), progressBar, progress)
Else
UpdateProgressBar(progressBar, progress)
End If
'Task.ContinueWith(progressBar.Value = progress, TaskScheduler.FromCurrentSynchronizationContext())
End Sub)
tasks.Add(t)
Next
Task.WaitAll(tasks.ToArray())
End Sub
Private Sub UpdateProgressBar(ByVal ProgressBarUpdate As ProgressBar, progress As Integer)
progressBar.Value = progress
End Sub
Still doesn't work not sure why?
Now I know this can be done using a background worker but this isn't the best way of making your application multithreaded
Sort of.
BackgroundWorker is a poor way to run many different Tasks individually. No one wants to deal with a separate BackgroundWorker component for each Task. But one BackgroundWorker is a great way to spawn just one extra thread to manage all your other Tasks and update the progress bar. It's an easy solution here.
Either way, the one thing you'll want to do for sure is move the code to update the ProgressBar out of the individual Tasks. Having that inside a Tasks violates separation of concerns1. Once you do that, you'll also need to change the call to WaitAll() to use WaitAny() in a loop that knows how many tasks you have, so you can still update the ProgressBar as each Task finishes. This will likley have the side effect of fixing your current issue, as well.
Private Async Sub startBtn_Click(sender As Object, e As EventArgs) Handles startBtn.Click
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(Sub()
While Not checked
proxies.TryDequeue(0)
'do stuff
End While
End Sub)
tasks.Add(t)
Next
progressBar.Value = 0
For i As Integer = 1 To tasks.Count
Dim t = Await Task.WhenAny(tasks)
tasks.Remove(t)
progressBar.Value = (i / combos.Count) * 100
Next i
End Sub
1 The problem here illustrates one reason we care about separation of concerns at all. Once I fix this, the code becomes much simpler and the frustrating errors just go away.
The above waitany is unnecessary.
I have found that you might as well put your progress bar code directly into the task run sub:
Dim ProgressBarSync As New Object
Dim tasks As New List(Of Task)()
For Each account In combos
Dim t As Task = Task.Run(
Sub()
'do stuff
SyncLock ProgressBarSync
ProgressBar.Increment(1)
End SyncLock
End Sub)
tasks.Add(t)
Next

VB.NET loop timer, multiple "IF" conditions

Writing the title to try and explain my query I think was harder than the problem I'm actually facing :) - Anyway on to the question.
So I have a 20 second timer but I want two different things to happen on the first and second 10 seconds. Specifically to change the active tab.
So I thought to myself I'll just write an if Statement in the timer tick event that if it = 10 seconds to change to the second tab and when it hits 0 to switch back to the first, then to restart the timer.
Below is my code but nothing happens, I think the problem lies with reading the current remaining time.
Private timeLeft2 As Integer
Private Sub timerCountdown2()
timeLeft2 = 20
End Sub
Private Sub tabTimer_Tick(sender As Object, e As EventArgs) Handles tabTimer.Tick
If timeLeft2 = 10 Then
TabControlVertical1.SelectTab(1)
End If
If timeLeft2 = 0 Then
TabControlVertical1.SelectTab(0)
tabTimer.Stop()
tabTimer.Start()
End If
End Sub
The properties of my timer are enabled = true and Interval = 1000
What am I doing wrong?
You should set the timer to trigger the Tick event every 10 seconds, not every 20 (or 1 as by your edit above).
Every time the Tick event is triggered, you look at the value of a global boolean variable.
If this variable is true you execute the code reserved for the first 10 seconds and invert the value of the boolean. When the timer triggers again, you execute the code for the second case and invert again the value of the boolean
So, somewhere in your code or in the designer set the tabTimer interval to 10 seconds
tabTimer.Interval = 10000
and declare a global boolean variable (In the same forms class probably)
Private tabSwitcher as Boolean = True
Now the Tick event could be written as:
(no need to stop the timer if this process needs to continue)
Private Sub tabTimer_Tick(sender As Object, e As EventArgs) Handles tabTimer.Tick
If tabSwitcher = True Then
TabControlVertical1.SelectTab(1)
else
TabControlVertical1.SelectTab(0)
End If
tabSwitcher = Not tabSwitcher
End Sub
This is what I think you are asking:
do something in 10 timer ticks - timer set to 1000
do something else 10 timer ticks later
repeat
Try this
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Static ct As Integer = 0
ct += 1
If ct = 10 Then
'do 10 stuff here
Debug.WriteLine("10")
ElseIf ct = 20 Then
'do 20 stuff here
Debug.WriteLine("20")
'then reset ct <<<<<<<<<<<<
ct = 0
End If
End Sub
Friend Class timerCtrl : Inherits Timer
Private ReadOnly tickFunc As EventHandler = Nothing
Friend Sub New(ByRef theFunc As EventHandler, ByVal theInterval As Integer, Optional ByVal autoStart As Boolean = True)
tickFunc = theFunc
Interval = theInterval
AddHandler Tick, tickFunc
If (autoStart) Then Start()
End Sub
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
If disposing Then
RemoveHandler Tick, tickFunc
End If
MyBase.Dispose(disposing)
End Sub
End Class
Friend Class TabClass
Private timerStep As Boolean = False
Private timerTabs As timerCtrl = Nothing
Friend Sub New()
timerTabs = New timerCtrl(AddressOf timerTabsTick, 10000)
End Sub
Private Sub timerTabsTick(ByVal sender As Object, ByVal e As EventArgs)
timerStep = Not timerStep
If timerStep Then
' condition 1
Else
' condition 2
End If
End Sub
End Class
a simple timer helper class for abstraction. to kill the timer, not just .Stop it, use timerTabs.Dispose(). eliminates the need to detach the event handler separately.
Seems to me that your timer is never getting to the value you are asking for in the if statements due to the fact that you have set the timer to the value of 20.
Also, I've use visual basics and am not to sure but doens't it need to be timeleft2.value?
Also, by stoping and starting the timer, it isn't actually restarting the timer, when you stop it say on 15 secs, and the restart, the timer restarts from 15 secs.
Try this.
If timeLeft2.Value = 10 Then
TabControlVertical1.SelectTab(1)
else if timeLeft2.Value = 0 Then
TabControlVertical1.SelectTab(0)
tabTimer.Stop()
timeLeft2.value = 0
tabTimer.Start()
End If

How to cancel a function which is called by a background worker?

I have a background worker which calls a function within a separate class. This process may be required to be canceled at any time via a button click from the front end. I have tried using CancelAsync() but this has no effect. The cofunds_downloadfiles is the function which i am calling. How do i go about canceling the process?
TIA.
Private Sub btnProcessdld_Click(sender As System.Object, e As System.EventArgs) Handles btnProcessdld.Click
Dim cfHelper As New CoFundsHelper
If btnProcessdld.Text = "Process" Then
btnProcessdld.Text = "Cancel"
If chkDailyFiles.Checked = False And chkWeeklyFiles.Checked = False Then
MessageBox.Show("Please select which files you want to download")
Else
lblProgress.Text = "Processing...if you are downloading weekly files this may take a few minutes"
uaWaitdld.AnimationEnabled = True
uaWaitdld.AnimationSpeed = 50
uaWaitdld.MarqueeAnimationStyle = MarqueeAnimationStyle.Continuous
uaWaitdld.MarqueeMarkerWidth = 60
_backGroundWorkerdld = New BackgroundWorker
_backGroundWorkerdld.WorkerSupportsCancellation = True
_backGroundWorkerdld.RunWorkerAsync()
End If
ElseIf btnProcessdld.Text = "Cancel" Then
btnProcessdld.Text = "Process"
_backGroundWorkerdld.CancelAsync()
uaWaitdld.AnimationEnabled = False
End If
Private Sub StartProcessdld(ByVal sender As Object, _
ByVal e As System.ComponentModel.DoWorkEventArgs) Handles _backGroundWorkerdld.DoWork
Dim cfHelper As New CoFundsHelper
cfHelper.ConnString = PremiumConnectionString
Dim dateValue As String
Dim weekly As Boolean = False
Dim daily As Boolean = False
If dtePicker.Value IsNot Nothing Then
dateValue = Format(dtePicker.Value, "yyyyMMdd")
If chkWeeklyFiles.Checked = True Then
weekly = True
End If
If chkDailyFiles.Checked = True Then
daily = True
End If
cfHelper.Cofunds_DownloadFiles(dateValue, weekly, daily)
Else
Throw (New Exception("Date Field is empty"))
End If
End Sub
Basically you can do the following:
in the DoWork sub, test for cancellationpending property
if it's true then you simply do not call that function and maybe put e.Cancelled = true then check this in the RunWorkerCompleted and decide what you have to do.
if you need to cancel it, simply make a Stop() sub in your class that does exactly that - stops the procedure. Then, you simply need to invoke it like
Me.Invoke(Sub()
myClass.Stop()
End Sub)
you may need to suspend the background worker until the call from your main thread has returned. You do this using semaphores: Private chk As New Semaphore(1,1,"checking1") You put this as a global variable to both your main thread and the background worker.
in the backgroundworker_doWork you use the semaphore like chk.WaitOne() AFTER the line that need to execute.
in the method of your class, when it has finished with computing you put a.Release
The semaphore is only needed if you need to make sure you wait for a result. It kind of defeats the purpose of multithreading but you can perform other actions in the worker before waiting for the main thread (like starting another thread with something else etc).
Other than that invoking a stop method should be enough. Sorry that i haven't had time to analyze your code but i hope i put you in the right direction.
CancelAsync doesn't actually do the cancelling of the worker (is just sets CancellationPending = True) so you basically have to check the state of the BackGroundWorker in your function:
Do While Not worker.CancellationPending
'some long running process
Loop
I have found that this is not 100% reliable however, so it may be safer to use a cancelled flag of your own.

VB.Net Multiple background workers - Only last task completes

I have been pulling my hair out trying to get this to work. If I step through the code in debugger it all works great.
My problem is if I just run it, only the last task responds. I'm guessing I am overwriting the background working or something. I am sure I am doing a few things wrong but my code is now messy as I tried many way while searching. I know of the threadpool and .Net 4.0 tasks but having a hard time getting to do what I need.
Basicly I am writing a program (trying more likely) that takes a list of computers and pings then, then checks their uptime and reports back.
This works fine in the UI thread (Obviously that locks up my screen). I can have the background worker just do this, but then it does each computer 1 by one, and while the screen is responsive it still takes a long time.
So my answer was to have a for loop for each server launching a new background worker thread. My solution does not work.
I have seen other threads that I could do it, but I need to use with events to call code to update to UI when each is done.
What is the most simple way to do this?
Here is my code. Most is just copy paste + modify till I get it working right.
So In the main class I have the testworker.
(I tried using Testworker() but it said I could not do that WithEvents)
When I click the button the list loads.
Private WithEvents TestWorker As System.ComponentModel.BackgroundWorker
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
Button1.IsEnabled = False
Dim indexMax As Integer
indexMax = DataGridStatus.Items.Count
For index = 1 To (indexMax)
Dim Temp As ServerInfo = DataGridStatus.Items(index - 1)
Temp.Index = index - 1
Call_Thread(Temp)
Next
End Sub
Private Sub Call_Thread(ByVal server As ServerInfo)
Dim localserver As ServerInfo = server
TestWorker = New System.ComponentModel.BackgroundWorker
TestWorker.WorkerReportsProgress = True
TestWorker.WorkerSupportsCancellation = True
TestWorker.RunWorkerAsync(localserver)
End Sub
Private Sub TestWorker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles TestWorker.DoWork
Dim iparray As IPHostEntry
Dim ip() As IPAddress
Dim Server As ServerInfo
Server = e.Argument
Try
'Get IP Address first
iparray = Dns.GetHostEntry(Server.ServerName)
ip = iparray.AddressList
Server.IPAddress = ip(0).ToString
'Try Pinging
Server.PingResult = PingHost(Server.ServerName)
If Server.PingResult = "Success" Then
'If ping success, get uptime
Server.UpTime = GetUptime(Server.ServerName)
Else
Server.PingResult = "Failed"
End If
Catch ex As Exception
Server.PingResult = "Error"
End Try
TestWorker.ReportProgress(0, Server)
Thread.Sleep(1000)
End Sub
Private Sub TestWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles TestWorker.ProgressChanged
Dim index As Integer
Dim serverchange As ServerInfo = DirectCast(e.UserState, ServerInfo)
index = DataGridStatus.Items.IndexOf(serverchange)
' index = serverchange.Index
DataGridStatus.Items.Item(index) = serverchange
' ProgressBar1.Value = e.ProgressPercentage
DataGridStatus.Items.Refresh()
End Sub
You are only getting the last result because you are blowing away your BackgroundWorker each time you call TestWorker = New System.ComponentModel.BackgroundWorker. Since the processing is being done asynchronously, this line is being called multiple times within your for loop before the previous work has finished.
Something like the following might work. (Sorry, my VB is rusty; there are probably more efficient ways of expressing this.)
Delegate Function PingDelegate(ByVal server As String) As String
Private _completedCount As Int32
Private ReadOnly _lockObject As New System.Object
Dim _rnd As New Random
Private _servers As List(Of String)
Private Sub GoButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles GoButton.Click
_servers = New List(Of System.String)(New String() {"adam", "betty", "clyde", "danny", "evan", "fred", "gertrude", "hank", "ice-t", "joshua"})
_completedCount = 0
ListBox1.Items.Clear()
GoButton.Enabled = False
BackgroundWorker1.RunWorkerAsync(_servers)
End Sub
Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Dim servers As List(Of System.String) = DirectCast(e.Argument, List(Of System.String))
Dim waitHandles As New List(Of WaitHandle)
For Each server As System.String In servers
' Get a delegate for the ping operation. .Net will let you call it asynchronously
Dim d As New PingDelegate(AddressOf Ping)
' Start the ping operation async. When the ping is complete, it will automatically call PingIsDone
Dim ar As IAsyncResult = d.BeginInvoke(server, AddressOf PingIsDone, d)
' Add the IAsyncResult for this invocation to our collection.
waitHandles.Add(ar.AsyncWaitHandle)
Next
' Wait until everything is done. This will not block the UI thread because it is happening
' in the background. You could also use the overload that takes a timeout value and
' check to see if the user has requested cancellation, for example. Once all operations
' are complete, this method will exit scope and the BackgroundWorker1_RunWorkerCompleted
' will be called.
WaitHandle.WaitAll(waitHandles.ToArray())
End Sub
Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
ListBox1.Items.Add(String.Format("{0} ({1}% done)", e.UserState, e.ProgressPercentage))
End Sub
Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
GoButton.Enabled = True
End Sub
Private Function Ping(ByVal server As System.String) As System.String
' Simulate a ping with random result and duration
Threading.Thread.Sleep(_rnd.Next(1000, 4000))
Dim result As Int32 = _rnd.Next(0, 2)
If result = 0 Then
Return server & " is ok"
Else
Return server & " is down"
End If
End Function
Private Sub PingIsDone(ByVal ar As IAsyncResult)
' This method is called everytime a ping operation completes. Note that the order in which
' this method fires is completely independant of the order of the servers. The first server
' to respond calls this method first, etc. This keeps optimal performance.
Dim d As PingDelegate = DirectCast(ar.AsyncState, PingDelegate)
' Complete the operation and get the result.
Dim pingResult As String = d.EndInvoke(ar)
' To be safe, we put a lock around this so that _completedCount gets incremented atomically
' with the progress report. This may or may not be necessary in your application.
SyncLock (_lockObject)
_completedCount = _completedCount + 1
Dim percent As Int32 = _completedCount * 100 / _servers.Count
BackgroundWorker1.ReportProgress(percent, pingResult)
End SyncLock
End Sub
Update: I posted this answer focusing on exactly what you were trying to do from a technical standpoint (use many background workers) without really putting much thought into whether or not this was a good way to accomplish your real objective. In fact, I think you could achieve what you're going for much more easily with a single BackgroundWorker and something like a Parallel.ForEach loop in its DoWork event handler (this takes care of a lot of the nitty gritty work in, e.g., Dave's solution).
When you declare WithEvents TestWorker As BackgroundWorker in VB it wraps it up something like this (not exactly—this is just to illustrate the idea):
Private _TestWorker As BackgroundWorker
Private Property TestWorker As BackgroundWorker
Get
Return _TestWorker
End Get
Set(ByVal value As BackgroundWorker)
' This is all probably handled in a more thread-safe way, mind you. '
Dim prevWorker As BackgroundWorker = _TestWorker
If prevWorker IsNot Nothing Then
RemoveHandler prevWorker.DoWork, AddressOf TestWorker_DoWork
' etc. '
End If
If value IsNot Nothing Then
AddHandler value.DoWork, AddressOf TestWorker_DoWork
' etc. '
End If
_TestWorker = value
End Set
End Property
When you realize this, it becomes clear that by setting TestWorker to a new BackgroundWorker on every call to Call_Thread, you are removing any attached handlers from the object previously referenced by the field.
The most obvious fix would simply be to create a new local BackgroundWorker object in each call to Call_Thread, attach the handlers there (using AddHandler and RemoveHandler), and then just let it do its thing:
Private Sub Call_Thread(ByVal server As ServerInfo)
Dim localserver As ServerInfo = server
' Use a local variable for the new worker. '
' This takes the place of the Private WithEvents field. '
Dim worker As New System.ComponentModel.BackgroundWorker
' Set it up. '
With worker
.WorkerReportsProgress = True
.WorkerSupportsCancellation = True
End With
' Attach the handlers. '
AddHandler worker.DoWork, AddressOf TestWorker_DoWork
AddHandler worker.ProgressChanged, AdressOf TestWorker_ProgressChanged
' Do the work. '
worker.RunWorkerAsync(localserver)
End Sub
Creating the worker right there in the method should be fine as long as you do so from the UI thread, since BackgroundWorker automatically attaches to the current SynchronizationContext in its constructor (if I remember correctly).
Ideally you should use only 1 backgroundworker and use it like this:
Assemble all the work that needs to be done: in your case a list of ServerInfo
Do the work in the background: ping all the servers and keep the result
Report progress: for example after each server pinged
Put results back in DoWorkEventArgs.Result
Display the results back in your UI.
You need to attach TestWorker_DoWork and TestWorker_ProgressChanged to the DoWork and ProgressChanged events within Call_Thread. I haven't yet examined the rest of the code, but that is why it isn't doing anything now.
TestWorker = New System.ComponentModel.BackgroundWorker
TestWorker.WorkerReportsProgress = True
TestWorker.WorkerSupportsCancellation = True
AddHandler TestWorker.DoWork, AddressOf TestWorker_DoWork
AddHandler TestWorker.ProgressChanged, AddressOf TestWorker_ProgressChanged
TestWorker.RunWorkerAsync(localserver)