How to cancel a function which is called by a background worker? - vb.net

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.

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

Why does my delegate called from backgroundworker runs in main thread

I am writing a program that will allow me to run a number of different hardware test routines.
The routines can be quite time consuming, lasting up to 30mins. During this time, I have to control a selection of test equipment to set up conditions and take measurements.
I was thinking that using a background worker to carry out the tasks would be ideal, and allow the UI to stay responsive. This worked well until one of the routines requires me to take measurements every 1.5seconds. I am using a system timer to trigger these events. The timer is created and started in the doWork sub of the background worker, however, I find that the delegate is running in the main(UI) thread and not in the background worker thread as I thought.
Am I doing something wrong? I have attached the main parts of a simplified program that has the same structure.
Private Sub getMeasurement()
' Runs in backgroundWorker thread
Me.TextBox2.Text = (System.DateTime.Now - startTime).TotalSeconds.ToString
startTime = System.DateTime.Now
Debug.Print("Thread name is " & Thread.CurrentThread.Name & ", ID = " & Thread.CurrentThread.ManagedThreadId)
End Sub
Private Sub OnTimedEvent()
'Runs in own thread, Calls getMeasurement which runs in BackgroundWorker thread
Thread.CurrentThread.Name = "OTE"
Debug.Print("In OnTimedEvent, thread = " & Thread.CurrentThread.Name & ", ID = " & Thread.CurrentThread.ManagedThreadId)
Dim ServiceTimerDelegate As New ServiceTimerDelegate(AddressOf getMeasurement)
Me.BeginInvoke(ServiceTimerDelegate)
End Sub
Private Sub backgroundWorker1_DoWork(ByVal sender As System.Object, _
ByVal e As DoWorkEventArgs) Handles BackgroundWorker1.DoWork
Thread.CurrentThread.Name = "BW1"
mTimer = New Timers.Timer(1490) ' 14.9secs (allow for some latency)
AddHandler mTimer.Elapsed, New Timers.ElapsedEventHandler(AddressOf OnTimedEvent)
Dim worker As BackgroundWorker = CType(sender, BackgroundWorker)
Dim i As Integer
'main timer for measurements every 1.5secs (may change to take interval from UI)
Select Case e.Argument
Case "Sunday"
mTimer.Start()
Debug.Print("Thread in DoWork = " & Thread.CurrentThread.Name & ", ID = " & Thread.CurrentThread.ManagedThreadId)
startTime = System.DateTime.Now
'main loop for temperature ramping
For i = 20 To 70
If (worker.CancellationPending = True) Then
e.Cancel = True
Else
Thread.Sleep(500)
worker.ReportProgress((i - 19) * (100 / 50))
i += 1
End If
Next
Case Else
e.Cancel = True
End Select
End Sub
Three things are confusing here, and make me think that the code you've posted above doesn't actually use the BackgroundWorker for its function.
The DoWork event always runs in a separate thread from the UI. That's the entire point of it. ReportProgress runs in the UI thread, but you're not making use of that.
You don't actually appear to be running the BackgroundWorker at all in the above code. Rather, you seem to be using a ServiceTimerDelegate - not 100% sure I know what that is. You've defined a BackgroundWorker within your DoWork handler (which makes no sense), but you never seem to be actually hooking a BackgroundWorker up to that handler nor calling RunWorkerAsync on it.
Even if you were running getMeasurement() in a BackgroundWorker, it should fail - you're altering the contents of a UI item, which should cause an exception due to InvalidCrossThreadAccess. If you need to alter a UI control, use the ReportProgress event, which occurs within the UI thread.

VB2010: How Would I run this extraction process in a backgroundworker with progressbar

So I made a small extraction program which just extracts a zip file to a location, and it also shows the progress of the extraction. But the problem is that whenever it's extracting large zips, the program kinda freezes while extracting and if you go off the process, you can't go back onto it until it's finished extracting, but you can still see the progressbar's progress. This is the code I have so far:
Form2.vb
Private Sub Button2_Click(sender As Object, e As EventArgs) Handles Button2.Click
If TextBox1.Text = "" Then
Else
ProgressBar1.Visible = True
Button2.Enabled = False
Button3.Enabled = False
TextBox1.Enabled = False
Unzip("FileToExtract.zip", "PathToExtractTo")
End If
End Sub
Unzip.vb
Imports Ionic.Zip
Module SimpleUnzip
Public Sub Unzip(ByVal ZipToUnpack As String, ByVal DirectoryToExstractTo As String)
Try
Using zip As ZipFile = ZipFile.Read(ZipToUnpack)
Form2.ProgressBar1.Maximum = zip.Entries.Count
Dim entry As ZipEntry
For Each entry In zip
entry.Extract(DirectoryToExstractTo, ExtractExistingFileAction.OverwriteSilently)
Form2.ProgressBar1.Value = Form2.ProgressBar1.Value + 1
Next
End Using
Catch ex1 As Exception
End Try
End Sub
End Module
So I have tried things like putting the SimpleUnzip sub in a background worker on the main forum and calling that, but that doesn't work at all, I have also tried a background worker on the module, it extracts but the progressbar doesn't work. Anyone know how to solve this problem?
As with any task using a BackgroundWorker, you do the work in the DoWork event handler and then you call ReportProgress to report the progress. This line:
Form2.ProgressBar1.Maximum = zip.Entries.Count
and this line:
Form2.ProgressBar1.Value = Form2.ProgressBar1.Value + 1
are going to have to be replaced with calls to ReportProgress. In the ProgressChanged event handler, you do what you normally would, i.e. update the ProgressBar.

Pause and resume a Thread (Background Worker)

I am working in VB.Net and I need to pause one thread as it waits for the other to finish.
I've seen a very close question but can't seem to figure it out (And couldn't comment on that post Pause/Resume loop in Background worker)
My scenario is that I have 2 background workers. Worker1 is passing fileNames to Worker2 which processes the files. I need to pause Worker1 if Worker2 hasn't finished. I.e. Worker1 only releases the next fileName after Worker2 has finished
Any ideas on how to do this?
WORKING CODE AFTER COMMENTS FROM #user1666788
Code below is for the scenario stated above of two background workers where one has to wait for the other to finish before proceeding.
Dim isFinished as boolean
Dim currentFiile as integer
Private Sub StartWork_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles StartWork.Click
bgWorker1.WorkerSupportsCancellation = True
isFinished = True
currentFile = 0
bgWorker1.RunWorkerAsync()
End Sub
Private Sub bgWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgWorker1.DoWork
If isFinished = False Then
bgWorker1.CancelAsync()
End If
isFinished = False
For i = currentFile To fileNames.Count - 1
Dim fileName As String = fileNames(i)
LoadRules(myValidator.GetFileType(fileName))
If i = fileNames.Count Then bgWorker1.CancelAsync()
Exit Sub
Next
End Sub
Private Function LoadRules(ByVal fileType As String) As Boolean
' Function to load some rules for file processing
Try
' Start Thread for actual file processing using bgworker2
bgWorker2.WorkerSupportsCancellation = True
bgWorker2.RunWorkerAsync()
Return True
Catch ex As Exception
End Try
End Function
Private Sub bgWorker2_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles bgWorker2.DoWork
Try
' Do your thing here
' for x is 0 to 1million
' next
' Mark is finished to true
isFinished = True
' Set currentFile
currentFile += 1
' The bgWorker1 is restarted when bgWorker2 has finished.
' Note however that bgWorker1 will "Continue" where it left off due to the property "currentFile"
bgWorker1.RunWorkerAsync()
'++++++++++++++++++++++++++++++++++++
Catch ex As Exception
End Try
End Sub
There you go. Its working as expected. Now need to figure out how to "monitor" progress of writing a file to disk so that I can start off another process after the file has fully been created.....
I had a similar issue as this. from what I've been told by VS it can't be done.
However I found a way around the subroutines for pausing/resuming threads no longer being used.
The easiest way is to check progress, and if it's still working end the call.
Here is an example, imagine S1 as your first thread and S2 as your second.
sub S1()
if(processingfile)then exit sub
'Insert code here for sending next file for processing
end sub
sub S2()
processingfile = true
'Insert code for processing files
processingfile = false
end sub
That is more or less how I worked around the problem. I hope my advice helped :)
Oh and one more thing, you might want to sleep the first thread before checking if the file is processing so that it doesn't use up a bunch of CPU power. But that's just a guess, I haven't tried it without a short sleeping period