Waiting for an async function in vb.net - vb.net

I'm trying out some async code to avoid locking up the UI while my program runs a time-consuming function (using Visual Studio 2022).
Here's what I've got so far - the program is running through pdf filename entries from a datagrid and performing the function on the filenames it finds:
Async Sub process_files()
For Each myrow In DGV_inputfiles.Rows
inputPDF = myrow.cells("col_filenamefull").value
outputPDF = myrow.cells("col_outname").value
Await Task.Run(Sub()
time_consuming_function(inputPDF, outputPDF)
End Sub)
Next
End Sub
At the moment, the program is not waiting for the 'time_consuming_function' to finish, so it's getting to the end of the sub before some of the output files are generated - so it appears to the user that it has finished when it's actually still working.
I believe the solution is something to do with returning a value from the function and waiting for it, but I can't quite see how it works - could anyone help please?

in time_consuming_function(...) you can send some infos to UI using invoke like this (assuming textbox1 exists in UI form):
sub time_consuming_function(...)
.... your stuff....
Me.BeginInvoke(Sub() textbox1.Text = "running...")
....
end sub

The effect of Await is that it returns control to the UI until the expression or call that is Awaited completes. It seems to be suited reasonably well to your workflow, you just need to make changes to process_files to be more user-friendly.
For example, you could have something in the UI update with the file that is currently being processed, and change it at the line before Task.Run.
e.g.
'(Inside the loop body)
CurrentOperation = $"Processing {inputPdf} into {outputPdf}..."
RaiseEvent PropertyChanged(Me, New PropertyChangedEventArgs(NameOf(CurrentOperation)))
Await Task.Run(...)
You could disable UI controls before the For loop and re-enable them when it finishes.
The benefit of Await is that these changes will be easy, and the logical flow of the routine will be easy to follow.
Be aware that any Await presents an option for re-entrant code as the user may interact with the UI (this is true even for cases where everything is running on one thread as with async internet or I/O operations).
If you haven't done so already, I would recommend to read everything Stephen Cleary has written about asynchronous operations in .NET.

Related

Recursively searching for file in task freezes form on Mono

I am working on a file browser in VB.Net that will be run on Ubuntu on the Mono framework. Everything was going fine up until I decided to implement the search function. I have it set up so that the search runs in a new task, and the user can cancel it from the form. This works fine on Windows, but when run on Mono, I get weird results:
Sometimes the form freezes (can still be dragged around, but everything inside is unresponsive)
Sometimes a few search results come up, and then the form freezes
Since the form is frozen, I cannot click the cancel button and have to force quit
There are no error messages or exceptions, so I have no idea what is going wrong.
Sometimes the form artifacts if dragged across screen, even though in theory the search code is running in a separate task
I have tried inserting 'Application.DoEvents()' throughout, but that didn't help. I even tried running the code on the UI thread without a task, but that obviously just causes everything to freeze.
Here is the code:
The Search() method is called through a textbox, when it is called a button to cancel is displayed, and if clicked calls tokenSource2.Cancel()
Dim tokenSource2 As New CancellationTokenSource()
Dim ct As CancellationToken = tokenSource2.Token
Private Sub Search(ByVal txt As String, ByVal dir As String)
CancelSearch()
tokenSource2 = New CancellationTokenSource()
ct = tokenSource2.Token
pnl_cancelsearch.Show()
Dim t As Task = Task.Factory.StartNew(Sub()
If ct.IsCancellationRequested Then
Exit Sub
End If
ListView1.Clear()
Dim iscasesensitive As Boolean = ConfigManager.SearchIsCaseSensitive
If Not searchhistory.Contains(txt) Then
searchhistory.Add(txt)
combo_search.Items.Add(txt)
End If
If ct.IsCancellationRequested Then
Exit Sub
End If
If dir = "" Then
For Each item As String In Directory.GetLogicalDrives
If ct.IsCancellationRequested Then
Exit Sub
End If
SearchRec(txt, item, iscasesensitive)
Next
Else
SearchRec(txt, dir)
End If
pnl_cancelsearch.Hide()
End Sub)
End Sub
Private Sub SearchRec(ByVal txt As String, ByVal rootdir As String, Optional ByVal casesensitive As Boolean = True)
For Each item As String In Directory.GetFiles(rootdir)
If ct.IsCancellationRequested Then
Exit Sub
End If
If casesensitive Then
If item.Contains(txt) Then
AddItem(item)
End If
Else
If item.ToLower.Contains(txt.ToLower) Then
AddItem(item)
End If
End If
Next
For Each item As String In Directory.GetDirectories(rootdir)
If ct.IsCancellationRequested Then
Exit Sub
End If
SearchRec(txt, item)
Next
End Sub
How do I fix this? What am I doing wrong? It works perfectly fine on Windows, but not on Mono.
You have here a problem: you're modifying the UI and you want your task to be executed as a new thread (because you're not using await so there's no shared time if it's being excuted on the main thread).
If you force the task to create a new thread (and for that you must explicitly set TaskScheduler.Default as the used task scheduler) then the UI code will lead to a cross threading exception. And if you don't force the task to be run on a new thread then it is executed on the main thread blocking it.
Mono Forms implementation is poor at a minimum, so its a lot slower than on Windows, it can perfectly being that on Windows you don't notice the slowness because it's fast enough and on another OS using Mono it's slower and you notice the UI update.
First of all, if you can, avoid using Forms on Mono, use GTK# or another UI kit.
Also, sepparate the UI logic from the data logic, create a task which retrieves all the data (using a task or the thread pool) and when you got it then update the UI, my bet is you will still find it slow because the UI update.
The solution, thanks to another user, was to use BeginInvoke when updating the UI, which worked perfectly.

Invoke method for multi thread application?

I have a bug in my application which is the same as here which this person was running into the same problem. My application is multi threaded where the worker thread is updating the Waveformgraph on the UI. I believe that is where my problem is and why, periodically, and on occassion I get a big red X in at least one of my waveformgraph objects when running the application. From reading and research, I need to use an Invoke or BeginInvoke method? Can someone please explain better and provide a sample code that is relevant to my code? The samples that I've found so far still have me hazy on how I need to do this or what I need to do. Thank you for your help.
This code is on the swScopeOnOff click event, main thread.
thread2 = New System.Threading.Thread(AddressOf dataAcquiring)
thread2.Start()
This code is in dataAcquiring Sub
Public Sub dataAcquiring()
'While Scope switch is on, stream each Ai channel's data continuously to its respective WaveForm graph
Do While swScopeOnOff.Value = True
data = reader.ReadWaveform(readRate)
i = 0
For Each WaveformGraph In WFGS
WaveformGraph.PlotWaveformAppend(data(i)) 'This line is updating the UI's waveform graphs
i += 1
Next
i = 0
Loop
End Sub
Proper, thread-safe invocation is actually not as hard as one might think (not even for thread-safe events, but that's irrelevant for this question).
I would recommend you to use the normal Invoke method, such as Me.Invoke() (where Me is the current form, if not, use Form1 or whatever it's called instead). Using BeginInvoke() may be asynchronous but it stacks memory usage and can cause memory leaks if EndInvoke() is not called correctly.
If you target .NET 4.0 or higher you can simply do like this:
Me.Invoke(Sub() WaveformGraph.PlotWaveformAppend(data(i)))
However if you target .NET 3.5 or lower it requires a few more lines of code.
'Outside your Sub.
Delegate Sub WaveformAppendDelegate(ByRef WaveformGraph, ByRef data)
'Create a new sub.
Public Sub AppendData(ByRef WaveformGraph, ByRef data)
WaveformGraph.PlotWaveformAppend(data)
End Sub
'Inside your sub, when you're going to invoke.
Me.Invoke(New WaveformAppendDelegate(AddressOf AppendData), WaveformGraph, data(i))

Updating UI while waiting for tasks to finish

I have a vb.net application processing a large amount of data. Due to the memory requirements of the process I am doing this batch-wise, with an overall planned structure as follows:
Do while Start < TotalNumberOfObjects
[cache data used for the upcoming batch]
For i = Start to Stop
[process data using multiple tasks...for example:]
t=taskfactory.startnew(doStuff(i))
TaskList.TryAdd(t.ContinueWith(Sub()
Me.BeginInvoke(DelegateUpdateProgress, {progress})
End Sub))
Next
[Wait for tasks to complete...
Normally I would wait for the tasks using task.waitall(),
but this will cause the UI to wait to update until all tasks are complete]
Start = Stop+1
Stop = Stop+Increment
[clear data from batch that was just completed]
loop
What's the proper way to:
Wait for all the tasks to complete before moving to the next batch?
Update the UI with the overall progress as each task completes?
My target framework is .NET 4.0.
I appreciate any input.
EDIT: Currently I am updating the UI upon completion of each task using task.continuewith() and calling me.beginInvoke to update the form,
TaskList.TryAdd(t.ContinueWith(Sub()
Me.BeginInvoke(DelegateUpdateProgress, {progress})
End Sub))
However, this is incompatible with how I would expect to wait for a list of tasks to complete, task.waitall(tasklist) because calling task.waitall will cause the UI thread to wait to update until all the tasks are complete.
First you need to set up a delegate and then use Dispatcher.Invoke
In the example below, a button is changed from enabled to disable (or the other way around):
Delegate Sub SetRecordButtonEnabledCallback(ByVal Enabled As Boolean)
Friend Sub SetRecordButtonEnabled(ByVal Enabled As Boolean)
Me.btnDGRecord.IsEnabled = Enabled
End Sub
after that all you need to do is call the following code from within your timer to invoke it:
Dim DesiredValue as Boolean = True
Me.Dispatcher.Invoke(New SetRecordButtonEnabledCallback(AddressOf SetRecordButtonEnabled), New
Object() {DesiredValue})
Why don´t you put your routine in a Backgroundworker structure?
So, while you code process data, you´re ready to update any UI component you have.
If necessary, you may also update UI from Backgroundworker, but you need to have some special requirements to do this.
You may consult here:
https://msdn.microsoft.com/en-us//library/ywkkz4s1.aspx

Async event handler - flycapture from PointGrey

I am using Point Grey's FlyCapture API to drive some cameras.
In a public class, I implemented all the starting and initializing code ; in the following _cam refers to a ManagedGigECamera.
Because I have 16 cameras, I want the code to be as fast as possible, so I wanted to use tasks.
Here is the code I use:
_cam.StartCapture(AddressOf OnImageGrabbed)
.../...
Public Sub OnImageGrabbed(ByVal raw_image As ManagedImage)
Dim t As Task = Task.Run(Sub()
'save image to disk or whatever
End Sub)
t.Wait()
End Sub
The above gives -sort of- satisfaction. By viewing image timestamps, I can see that some images are saved seconds after they are grabbed, and even some images are skipped altogether...
I wanted to make sure each call to OnImageGrabbed would start a new task, and tried the following, but it crashes right away with 'object not set to an instance of an object' (can't really debug, the code is running on a remote machine)
_cam.StartCapture(AddressOf OnImageGrabbed)
.../...
Public Async Sub OnImageGrabbed(ByVal raw_image As ManagedImage)
Await Task.Run(Sub()
'save image to disk or whatever
End Sub)
End Sub
All in all, my questions are:
how can I run an event handler asynchronously ?
why, using the first code, do I get (what appears to be) random delays between each call
to OnImageGrabbed ? I mean the differences in time between image timestamps is never the same, and tend to increase on the long run (first few images are almost synchronized, but after 1 minute or so, each image is separated by more and more time). Memory leak ? GC ?
Thanks in advance for any hint !
EDIT:
In the end I changed the way the system works: I fire a software trigger on each camera using a timer, and each trigger is fired 'in parallel':
Parallel.ForEach(Of ListOfCameras)(SingleCamera,
Sub(aCamera, loopstate, num)
aCamera.FireTrigger()
End Sub)
Starting a task and then immediately blocking on it (via Wait) nets you nothing. You may as well just run the saving-image code directly.
The second example is actually asynchronous. You're probably getting an exception because the ManagedImage argument or one of its child objects is being disposed. Remember that the code raising the event has no idea that your code is asynchronous; it's up to you to copy out what you need from the event arguments if you're going to use it asynchronously.

Threading. How does button click interrupt/influence program flow

My project is vb.net 2010 windows desktop form.
So far, single threaded (default).
If a SUBroutine has a for...next loop in it that is running, what happens if a buttonclick event is fired and within that event a variable is changed? Like: does program execution leave the loop that was running? Or does it continue to run while that variable is changed by the buttonclick event?
What I'm aiming for:
If someone clicks the button, blnRequestStop is set to True.
Within that for...next loop, just before the "next" it checks blnRequestStop. If true then it will exit the "for" loop.
I'm guessing I need to use threads? Can anyone give me a simple example, please?
EDIT:
This code below seems to be working fine. But maybe you all see a problem?
If (btnProcess.Text = "Done!") Then
End
ElseIf (btnProcess.Text = "IMPORT") Then
bRequestStop = False
t1 = New Thread(AddressOf ProcessDo)
t1.Start()
Else
t2 = New Thread(AddressOf MyInterrupt)
t2.Start()
End If
Here is the short version of what ProcessDo and MyInterrupt do:
Private Sub ProcessDo()
For each X in blahblah
'do stuff (yes, includes interface)
if (blnInterrupt) then exit For
Next X
End
End Sub
Private Sub MyInterrupt()
blnInterrupt=true
End Sub
Yes, you probably want to do the long-running task on a background thread. Here's a code sample including how you'd get the results back to the UI thread when you're done (otherwise you'll get errors about Cross-thread operation not valid).
ThreadPool is a nice way to do some work on a background thread. You could set stopIt = True in the button click event for the stop button.
ThreadPool.QueueUserWorkItem(
Sub()
For Each thing In things
If stopIt Then Exit Sub
'Do the stuff!
Next
'We're done, update UI
Me.UpdateUI("All done!")
End Sub)
To safely update the UI, you'll need to make sure you get back to the UI thread.
Public Sub UpdateUI(result As String)
If Me.InvokeRequired() Then
'If we aren't on the UI thread, invoke this function on the UI thread
Me.BeginInvoke(Sub() UpdateUI(result))
Exit Sub
End If
'Update UI here
lblResult.Text = result
End Sub
Execution is 'stopped' (in a way) until the Loop finishes him job, so yes, you need to multi-thread.
It it is not a very long operation where you need to update UI controls during the Loop then you just can use Application.DoEvents inside the loop to be able to use other controls as normally in the application when FOR loop is working, but I advise you that this will have a negative impact on UI performance, but if it's not a long duration loop then you maybe would consider to use DoEvents instead introduce into multi-threading it's just an alternative, not recommended but, you can use it.
PS: Forgive my English