Need a fix for my deadlocking issue with my parallel.foreach - vb.net

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.)

Related

VB.NET - System.AccessViolationException: 'Attempted to read or write protected memory...' Occurs sporadically when using autoCompleteSource

System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.'
I started getting this exception after trying to implement AutoComplete into one of my text boxes. It seems to occur somewhat sporadically? It often occurs when trying to use AutoComplete within a couple seconds of loading it's panel but has occured upwards of 5/6 seconds afterwards too. Additionally if it doesn't occur, the AutoComplete can be used endlessly without crashing.
I've had the same error occur on two different machines so I'm really unsure of what could be causing this and would be endlessly grateful if someone could point me in the right direction!
I've tried to strip out all the parts that definitely aren't causing it to save space.
This code runs first:
Private Sub btnManagerEditItem_Click(sender As Object, e As EventArgs) Handles btnManagerEditItem.Click
refreshEditItemsScreen(False) ' clears all on screen data and repopulates DataGrid
txtManagerEditItemsSearchName.AutoCompleteMode = AutoCompleteMode.SuggestAppend
txtManagerEditItemsSearchName.AutoCompleteSource = AutoCompleteSource.CustomSource
End Sub
Private Sub refreshEditItemsScreen(refreshDatabase As Boolean)
dtgrdManagerEditItems.Rows.Clear() ' DataGridView displays items to user.
If refreshDatabase Then
updateItemDatabase() ' reads the database from file and updates _itemDatabase with it
End If
For Each entry In _itemDatabase ' Global variable holding the database
dtgrdManagerEditItems.Rows.Add(entry.Name, entry.Price.ToString("c"), entry.UniqueID, getStaffDictionary(entry.StaffID))
Next
End Sub
Private Sub updateItemDatabase()
_itemDatabase = readItemDatabaseFromFile()
End Sub
Private Function readItemDatabaseFromFile() As List(Of Item)
Dim pathtofile As String = IO.Path.Combine(My.Application.Info.DirectoryPath.Substring(0, My.Application.Info.DirectoryPath.Length - 21), "Item Database.xml")
Dim itemDatabaseFromFile As New List(Of Item)
If System.IO.File.Exists(pathtofile) Then
Dim objStreamReader As New StreamReader(pathtofile)
Dim xsSerialize As New XmlSerializer(itemDatabaseFromFile.GetType)
itemDatabaseFromFile = xsSerialize.Deserialize(objStreamReader)
objStreamReader.Close()
End If
Return itemDatabaseFromFile
End Function
Then this code runs when you start typing in the AutoCorrect box:
Private Sub txtManagerEditItemsName_TextChanged(sender As Object, e As EventArgs) Handles txtManagerEditItemsSearchName.TextChanged
txtManagerEditItemsSearchName.AutoCompleteCustomSource = _autoCompleteSource
End Sub
_autoCompleteSource gets it's value when this code is ran on form load (updateItemDatabase() is also ran on form load):
Private Sub updateAutoCompleteSource()
' reads the id dictionary from file and adds it to the auto complete source
For Each Itm In readItemIDDictionaryFromFile().Keys
_autoCompleteSource.Add(Itm)
Next
End Sub
Private Function readItemIDDictionaryFromFile() As Dictionary(Of String, String)
Dim pathtofile As String = IO.Path.Combine(My.Application.Info.DirectoryPath.Substring(0, My.Application.Info.DirectoryPath.Length - 21), "ID Dictionary.txt")
Dim output As New Dictionary(Of String, String)
If File.Exists(pathtofile) Then
For Each line In IO.File.ReadAllLines(pathtofile)
Dim parts() As String = line.Split(",")
output.Add(parts(1), parts(0))
Next
Return output
End If
End Function
Sorry if some of this code was unnecessary to post, I'm pretty lost as to what's causing the error so I just posted everything that runs before it occurs that I think could be related to it.
Thank you

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 problems with invoke

I have a thread that runs background jobs and is required to update the GUI once in a while. My program has been designed so that when the user clicks off of a form, the thread and background operations still run, yet the controls have been disposed (for memory management purposes).
I have been using Invoke() and "If Control.Created = True" to make sure that the thread can successfully update the controls without running into any exceptions. However, when the form is recreated, all "Control.Created" values are false and Invoke() fails with "{"Invoke or BeginInvoke cannot be called on a control until the window handle has been created."}"
My guess is that this has something to do with the fact that when the form is recreated it is assigned different handles and that the "Invoke()" is looking at the old handle. SO my question is, how do I fix this?
EDIT: As per requested, the code for opening the form and where the bg thread works from
Opening the DropLogMDIalt form is simply
FormCTRL.Show()
The Background Thread runs when the control is modified so that the NumericUpDown is more than 0 (so that there is something to countdown from)
Private Sub NLauncherTerminateInput_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles DLScanInterval.ValueChanged
If DLScanInterval.Created = True Then
DLTimerControlValue = DLScanInterval.Value
If DLTimerControlValue = 0 Then
CancelDropLogTimer()
Else
If DLScanIntervalControl.Active = False Then
BeginDropLogTimer()
End If
End If
End If
End Sub
Public Sub BeginDropLogTimer()
Dim N As New Threading.Thread(AddressOf DropLogTimerIntervalThreadWorker)
N.Start()
DLScanIntervalControl.ThreadID = N.ManagedThreadId
DLScanIntervalControl.Active = True
End Sub
Public Sub CancelDropLogTimer()
DLScanIntervalControl.Active = False
End Sub
Public Sub DropLogTimerIntervalThreadWorker()
DLScanTimerSecondsLeft = DLTimerControlValue * 60
Dim s As Integer = DLTimerControlValue
Do Until 1 = 2
DLScanTimerSecondsLeft = DLTimerControlValue * 60
Do Until DLScanTimerSecondsLeft <= 0
If Not (DLTimerControlValue = 0 Or DLScanIntervalControl.CancelPending = True) Then
Else
Exit Sub
End If
If Not DLTimerControlValue = s Then
DLScanTimerSecondsLeft = DLTimerControlValue * 60
s = DLTimerControlValue
End If
Dim ToInvoke As New MethodInvoker(Sub()
Timer(DLScanTimerSecondsLeft, ":", DLScanIntervalTB)
End Sub)
If (Me.IsHandleCreated) Then
If (InvokeRequired) Then
Invoke(ToInvoke)
Else
ToInvoke()
End If
End If
Threading.Thread.Sleep(1000)
DLScanTimerSecondsLeft -= 1
Loop
CompareScan = True
PerformScan()
Loop
End Sub
The thread is simply called by declaring a new thread.thread, however, I have created a class and a variable that the thread uses to check if it should still be running or not (similarly to how a backgroundworker would) this is illustrated by the "DLScanIntervalControl.CancelPending"
The form is then closed later by
Form.Close()
It can be reopened if the user clicks on a label that then uses the same method as shown above (FormCTRL.Show())
From MSDN:
"If the control's handle does not yet exist, InvokeRequired searches up the control's parent chain until it finds a control or form that does have a window handle. If no appropriate handle can be found, the InvokeRequired method returns false."
In other words, you need to verify that the handle is created and then check if invoke is required.
If(Me.IsHandleCreated) Then
If(Me.InvokeRequired) Then
'...
Else
'...
End If
End If
I had a similar error when trying to use delegates to update controls on a form in another thread. I found that the handle is only created when it's "needed". I'm not sure what constitutes "needed", but you can force it to create the handle by accessing the Handle property of the object.
What I had done in my application is this:
' Iterate through each control on the form, and if the handle isn't created yet, call the
' Handle property to force it to be created
For Each ctrl As Control In Me.Controls
While Not ctrl.IsHandleCreated
Dim tmp = ctrl.Handle
tmp = Nothing
End While ' Not ctrl.IsHandleCreated
Next ' ctrl As Control In Me.Controls
It's rather ghetto, but it may help you here (If you still need the help)
I think the issue here has nothing to do with invoking, but references. Following this pseudocode...
Dim A As New Form1
A.Show()
''Spawn background thread with a reference to A
A.Dispose()
Dim B As New Form1
B.Show()
The thread is attempting to refer to the first instance of Form1 above which is disposed and will always stay that way.
If you want the thread to be able to update any form then you need to give the thread a (synchronised) way to refer to the form...
Public Class Worker
Private Target As Form1
Private TargetLock As New Object
Public Sub SetTargetForm(Frm as Form1)
SyncLock TargetLock
Target = Frm
End SyncLock
End Sub
Public Sub DoWork() ''The worker thread method
''Do work as usual then...
SyncLock TargetLock
If Target IsNot Nothing AndAlso Target.IsHandleCreated Then
If Target.InvokeRequired
Target.Invoke(...)
Else
...
End If
End If
End SyncLock
End Sub
End Class
This way, when a new form is available, you can inform the worker thread using SetTargetForm() and it will update the appropriate one.
Of course, you'd be better off refactoring the "Update UI" checks and invoke calls into a different method for simplicity/maintainability but you get the point.
Note that I haven't got an IDE to hand so there may be typos.
One final point... I'd question the value of disposing a form for memory management purposes. Controls are fairly lightweight in terms of memory and it's far more common for an object used by the form to be a memory hog than the form itself. Are you sure you're getting a real benefit for this added complexity?

Trying to use Threading.Tasks.Task to kick off a background search while user types, but after they pause

I've searched but I can't find the solution I'm looking for.
I specifically want to use Threading.Tasks.Task for this project, just to understand it better.
I have a lengthy XML file that I want to search based on text that a user types in. Because it's lengthy, I want to wait for the user to stop typing for 250ms or so before I actually start searching in the background. I tried to kick off my Task, having it sleep for 250ms, then check if my CancelTokenSource had canceled because another character was typed. I'm not seeing a cancellation, though, so I end up seeing my results flash several times as the queued up search tasks complete, one after the other, after I finish typing.
My code has turned into a terrible mess and I need to start over, but was hoping someone could point me in the right direction.
Start with a thread-safe property that determines when the search should begin. Initialise it to Date.MaxValue to prevent it running before it's asked to.
Private Property SearchTriggerTime As Date
Get
SyncLock SearchTriggerTimeLock
Return _SearchTriggerTime
End SyncLock
End Get
Set(value As Date)
SyncLock SearchTriggerTimeLock
_SearchTriggerTime = value
End SyncLock
End Set
End Property
Private _SearchTriggerTime As Date = Date.MaxValue
Private ReadOnly SearchTriggerTimeLock As New Object
When the search text box text changes, set the timer to when you want to start searching. As the user types quickly, the search timer will be reset before it triggers. In the code below, if the user clears the text box, the timer is set to never trigger, ie. do not search.
Private Const SearchDelay As Integer = 250
Private Sub TextBox1_TextChanged(sender As Object, e As EventArgs) Handles TextBox1.TextChanged
If TextBox1.Text <> "" Then
SearchTriggerTime = Date.Now.AddMilliseconds(SearchDelay)
Else
SearchTriggerTime = Date.MaxValue
End If
End Sub
Start a thread to perform the searching when the form loads.
Public Sub Form_Load(sender As Object, e As EventArgs) Handles Me.Load
With New Thread(AddressOf SearchThread)
.Start()
End With
End Sub
This thread passes through three states. The first state is waiting for the trigger time to start the search. It checks the trigger time every 50ms. The second state is performing the search. During the search, it checks if the form closes or if the user types more, and abandons the search in those cases. In the third state, if the search completes normally, the form's original thread is asked to display the results. If you need to change a control, always do so on the form's thread by using Form.Invoke(Delegate).
Private Sub SearchThread()
Do Until IsDisposed
' Wait for the user to stop typing.
Do Until IsDisposed OrElse SearchTriggerTime <= Date.Now
Thread.Sleep(50)
Loop
' Search until the form is disposed, the user types more, or the search is complete.
' TODO: Initialise the search variables.
Dim SearchComplete As Boolean = False
Do Until IsDisposed OrElse SearchTriggerTime > Date.Now OrElse SearchComplete
' TODO: Insert search code here.
If condition Then SearchComplete = True
Loop
' Reset the search timer.
SearchTriggerTime = Date.MaxValue
' Only display results if the search was completed.
If SearchComplete Then Invoke(New Action(AddressOf DisplaySearchResults))
Loop
End Sub
Lastly, create a method to display your search results. This will be run in the form's thread to prevent invalid cross-thread operations.
Private Sub DisplaySearchResults()
' TODO: Display search results.
End Sub
I managed to get it to work and it actually looks fairly clean (I think).
I'm using 'Static' variables so that I can keep all of the code within this method but still have my CancellationTokenSource available on subsequent calls.
I'm pretty pleased with the result, but still welcome comments and notes for improvement.
The actual search is actually happening on the UI thread (I think) but I'm doing that because it's easy to populate a treeview while I find valid nodes in my XML. My next refactor will be to do the search on a background thread (rather than just the 250ms wait) and use a TaskScheduler to post UI updates.
Private Sub txtQuickSearch_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles txtQuickSearch.TextChanged
Static scCancelTokenSource As Threading.CancellationTokenSource
Static scCancelToken As Threading.CancellationToken
If txtQuickSearch.Text.Length >= 3 Then
If scCancelTokenSource IsNot Nothing Then
scCancelTokenSource.Cancel()
scCancelTokenSource = Nothing
End If
scCancelTokenSource = New Threading.CancellationTokenSource
scCancelToken = scCancelTokenSource.Token
Dim lfSearch = Sub()
Dim ltToken As Threading.CancellationToken = scCancelToken
Threading.Thread.Sleep(250)
If Not ltToken.IsCancellationRequested Then
Me.Invoke(Sub() DoQuickSearch(txtQuickSearch.Text))
End If
End Sub
Threading.Tasks.Task.Factory.StartNew(lfSearch, scCancelToken)
End If
End Sub

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)