Background
First I admire: The articles title is kind of lurid. But actually i didn't find anywhere the guidance, how incredible simple the kernel usage of Async/Await really can be.
Table of Contents
To meet the promise
Assume a long lasting loading of Data, for exampte this:
Private Function GetData(count As Integer) As List(Of Point)
Dim result = New List(Of Point)
Dim rnd = New Random(42)
For i = 0 To count - 1
result.Add(New Point(rnd.Next(-100, 100), rnd.Next(-100, 100)))
System.Threading.Thread.Sleep(5)
Next
Return result
End Function
Assume a Button to load and display it:
Private Sub btLoadData_Click(sender As Object, e As EventArgs) Handles btLoadData.Click
DataGridView1.DataSource = GetData(999)
End Sub
Of course this will block the Gui, and if you look closely, you'll see: namely for overall about 5 seconds.
Now unblock this - as said - with no additional Line of Code:
Private Async Sub btLoadData_Click(sender As Object, e As EventArgs) Handles btLoadData.Click
DataGridView1.DataSource = Await Task.Run(Function() GetData(999))
End Sub
Believe it or not: That was it :-D
A closer look - what was changed in Detail?
- The Sub was modified by the
Async
-Modifier-Keyword. This is required, otherwise Await
can't be applied - the call
GetData(999)
is encapsulated within an anonymous Function - the anonymous Function itself is passed to the
Task.Run()
-Method, which is generic, and overloaded, so it accepts any arbitrary Delegate of Type Func(Of T)
or Action
.
A Func(Of T)
is a Delegate, which "points" to a Function without Parameter, but with Return-Value - and that is the case here - the expression Function() GetData(999)
takes no Parameter, but returns, what GetData()
returns: namely a List(Of Point)
- last but not least the calling of
Task.Run()
is marked as Await
, and that does the magic
Keep this reciepe in mind: 1) mark the external Sub
as Async
, 2) encapsulate the inner, working part within an anounymous Function, 3) pass this Function to Await Task.Run()
- and note: you can directly use the Return-Value - just like before in the blocking mode.
Change Gui while Task runs
Of course the above is not sufficiant - in several respects.
The first issue is - since the Gui now is unblocked - that you must prevent the user from clicking the button twice, while the Task is still working.
I love that one - because of its same simplicity:
Private Async Sub btLoadData_Click(sender As Object, e As EventArgs) Handles btLoadData.Click
btLoadData.Enabled = False
DataGridView1.DataSource = Await Task.Run(Function() GetData(999))
btLoadData.Enabled = True
End Sub
Sidenote - try understand the miracle
When you look without thinking at the code above, you might take it as it looks like: 1) Button disabled, 2) Task executed, 3) Button re-enabled.
But wait a minute - why the hack that does not block the Gui? As said - GetData(999)
takes 5 seconds!
Ok - it executes parallel, but if so - why the hacker-hack the button keeps disabled for 5 seconds? If GetData()
runs parallel, the button should be re-enabled at once!
The secret is: at the point of Await
the method terminates and returns to the caller. That is how the Gui keeps responsive.
And when the parallel Process finishes, it jumps back right into the method, and continues at the awaiting-point as if nothing had happened.
It is still a miracle, how the compiler achieves that, it deals with "Task.Continuation", "Task.Completion" and stuff - sorry: I don't know it in detail, but what looks to us as a consistent procedure: In reality it's a kind of very tricky "syntactic-candy", hiding a completely different architecture.
Progress-Report
But back to concret: As next the user certainly wants feedback, to get a feeling, that the application is not kidding him, but is real busy. Here comes the first Async-Helper into Play, the Progress(Of T)
- Class.
You can instantiate one, and the thread can pass arbitrary progress-report-data to it, and in Main-Thread it raises an event, where you can perform a progress-report - eg increment a progressbar.
This requires some changes: I want the Progressbar only appear, when needed, and prerequisite of displaying progresses is, that GetData()
reports them:
Private WithEvents _ProgressPercent As New Progress(Of Integer)
Private Sub Progress_ProgressChanged(sender As Object, e As Integer) Handles _ProgressPercent.ProgressChanged
ProgressBar1.Value = e
End Sub
Private Async Sub btLoadData_Click(sender As Object, e As EventArgs) Handles btLoadData.Click
btLoadData.Enabled = False
ProgressBar1.Visible = True
DataGridView1.DataSource = Await Task.Run(Function() GetData(999))
ProgressBar1.Visible = False
btLoadData.Enabled = True
End Sub
Private Function GetData(count As Integer) As List(Of Point)
Dim result = New List(Of Point)
Dim rnd = New Random(42)
Dim prgs As IProgress(Of Integer) = _ProgressPercent
For i = 0 To count - 1
result.Add(New Point(rnd.Next(-100, 100), rnd.Next(-100, 100)))
System.Threading.Thread.Sleep(5)
prgs.Report(i \ 10)
Next
Return result
End Function
still no rocket-science - is it?
But one Point:
System.Threading.Thread.Sleep(5)
prgs.Report(i \ 10)
Are you shure you want the Progressbar updated every 5 milliseconds? (I tell you: You do not want that - no-one can look so fast!). Too frequently Gui-Updating is wasting CPU-Power - there is no reason to do so.
For that I invented the IntervalProgress(Of T)
-Class, which simply omitts reports, which are sent too frequently.
Its usage is the same as shown above, but it doesn't waste Cpu-Power that much.
For brevity i do not show its code here - if you like, refer to the attached sources.
Cancellation
The next Helpers we need are CancelationToken
and CancelationTokenSource.
The latter provides the first, and together its a mechanism to signal, that cancellation is requested.
Private WithEvents _ProgressPercent As New IntervalProgress(Of Integer)
Private _Cts As CancellationTokenSource
Private Function GetData(count As Integer, ct As CancellationToken) As List(Of Point)
Dim result = New List(Of Point)
Dim rnd = New Random(42)
For i = 0 To count - 1
ct.ThrowIfCancellationRequested()
result.Add(New Point(rnd.Next(-100, 100), rnd.Next(-100, 100)))
System.Threading.Thread.Sleep(5)
_ProgressPercent.Report(i \ 10)
Next
Return result
End Function
Private Sub Any_Click(sender As Object, e As EventArgs) Handles btClear.Click, btCancel.Click, btLoadData.Click
Select Case True
Case sender Is btCancel : _Cts.Cancel()
Case sender Is btClear : DataGridView1.DataSource = Nothing
Case sender Is btLoadData : LaunchGetData()
End Select
End Sub
Private Async Sub LaunchGetData()
btLoadData.Enabled = False
_Cts = New CancellationTokenSource
Try
DataGridView1.DataSource = Await Task.Run(Function() GetData(1010, _Cts.Token))
Catch ex As OperationCanceledException
Msg("cancelled")
End Try
_Cts.Dispose()
btLoadData.Enabled = True
End Sub
I moved the Getting Data to an own Sub: LaunchGetData()
, because it becomes a little too complex to stay in my Any_Click()
-Button-Click-Handler.
Then look first at GetData()
- this now expects a CancellationToken
, which is used in the loop: ct.ThrowIfCancellationRequested()
- a long word, but self-explainatory: it does, what it says.
Catching its OperationCanceledException
in LaunchGetData()
detects, if the Process was finished by cancelation.
A little strange design is, that a CancelationTokenSource
only can be used once - never mind: Microsoft® can't do everything right - can it? ;-)
Exception-Handling - Surprise!
Principally Exception-Handling is already covert with the TryCatch above, so I just had to install a small "CauseError-mechanism" to see it work:
1 Private WithEvents _ProgressPercent As New IntervalProgress(Of Integer)
2 Private _CauseError As Boolean
3 Private _Cts As CancellationTokenSource
4
5 Private Function GetData(count As Integer, ct As CancellationToken) As List(Of Point)
6 Dim result = New List(Of Point)
7 Dim rnd = New Random(42)
8 For i = 0 To count - 1
9 ct.ThrowIfCancellationRequested()
10 If _CauseError Then Throw New ArgumentException("lets be generous and make an exception")
11 result.Add(New Point(rnd.Next(-100, 100), rnd.Next(-100, 100)))
12 System.Threading.Thread.Sleep(5)
13 _ProgressPercent.Report(i \ 10)
14 Next
15 Return result
16 End Function
17
18 Private Sub Any_Click(sender As Object, e As EventArgs) Handles btClear.Click, btCauseException.Click, btCancel.Click, btLoadData.Click
19 Select Case True
20 Case sender Is btCancel : _Cts.Cancel()
21 Case sender Is btClear : DataGridView1.DataSource = Nothing
22 Case sender Is btCauseException : _CauseError = True
23 Case sender Is btLoadData : LaunchGetData()
24 End Select
25 End Sub
26
27 Private Async Sub LaunchGetData()
28 _CauseError = False
29 btLoadData.Enabled = False
30 _Cts = New CancellationTokenSource
31 Try
32 DataGridView1.DataSource = Await Task.Run(Function() GetData(1010, _Cts.Token))
33 Catch ex As OperationCanceledException
34 Msg("cancelled")
35 Catch ex As Exception
36 Msg("a real problem! - ", ex.GetType.Name, Lf, Lf, ex.Message)
37 End Try
38 _Cts.Dispose()
39 btLoadData.Enabled = True
40 End Sub
If you look closely, you'll find the Button (line#22), the Boolean (#2), the Throwing (#10) and the Catching (#35) of my willingly Exception.
LaunchGetData()
s additional Catch-Segment detects other Exceptions, without touching the Cancellation-Logic.
Only a small drop of bitterness: It doesn't work.
What?
Yes, i didn't beleave it too, but fact is, the exceptions, kindly thrown from the side-Thread, get not handled in the main-thread.
I really couldn't believe that, since in another project i use the HttpClient.GetAsync()
-Method, and there Exception-works like a charm, with exactly the same pattern as shown here.
Obviously HttpClient.GetAsync()
does something different than Task.Run()
- and (thanks to Freddy, my Disassembler) - I learned to implement a real async-Method, instead of delegating that to Task.Run()
:
1 Private Function GetDataAsync(count As Integer, ct As CancellationToken) As Task(Of List(Of Point))
2 Dim tcs = New TaskCompletionSource(Of List(Of Point))()
3 Task.Run(Sub()
4 Dim result = New List(Of Point)
5 Dim rnd = New Random(42)
6 Try
7 For i = 0 To count - 1
8 ct.ThrowIfCancellationRequested()
9 If _CauseError Then Throw New ArgumentException("lets be generous and make an exception")
10 result.Add(New Point(rnd.Next(-100, 100), rnd.Next(-100, 100)))
11 System.Threading.Thread.Sleep(5)
12 _ProgressPercent.Report(i \ 10)
13 Next
14 Catch ex As Exception
15 tcs.SetException(ex)
16 Exit Sub
17 End Try
18 tcs.SetResult(result)
19 End Sub)
20 Return tcs.Task
21 End Function
The main-thing is the TaskCompletionSource
, and instead of throwing Exceptions, or returning Results one must Set Exceptions or Set Results to it.
Call GetDataAsync()
as follows:
DataGridView1.DataSource = Await GetDataAsync(1010, _Cts.Token)
Yeah - works fine!
Ähm - seriously - that is, what nowadays the fabulous Async-Pattern provides as: "simple threading"? ... in avoidance of swearwords ... let me try express it gently: "Its not quite as simple as we expected - is it?"
And of course i tried to get rid of that - If you look closely to the above, all the type-stuff deals with List(Of Point)
- maybe there is a way, to get that generic, and encapsulate it to a place, where we will nevermore must see it again?
Yeah - here it goes:
1 Public Class AsyncHelper
2
3 Public Shared Function Run(Of T)(func As Func(Of T)) As Task(Of T)
4 Dim tcs = New TaskCompletionSource(Of T)()
5 Task.Run(Sub()
6 Try
7 tcs.SetResult(func())
8 Catch ex As Exception
9 tcs.SetException(ex)
10 End Try
11 End Sub)
12 Return tcs.Task
13 End Function
14
15 End Class
And now replace Task.Run()
with AsyncHelper.Run()
1 Private Async Sub LaunchGetData()
2 _CauseError = False
3 btLoadData.Enabled = False
4 _Cts = New CancellationTokenSource
5 Try
6 DataGridView1.DataSource = Await AsyncHelper.Run(Function() GetData(1010, _Cts.Token))
7 Catch ex As OperationCanceledException
8 Msg("cancelled")
9 Catch ex As Exception
10 Msg("a real problem! - ", ex.GetType.Name, Lf, Lf, ex.Message)
11 End Try
12 _Cts.Dispose()
13 btLoadData.Enabled = True
14 End Sub
That one works as expected, while the other one, with Task.Run()
does not work.
It's a Bug - not a Feature!
Meanwhile I've got an answer, why Task.Run()
behaves such increadible unexpected: It is a bug, either of the Task
-Class or of the VisualStudio 2013.
Because Tast.Run()
works as expected, if the compiled Exe was started outside the VisualStudio.
You can check it out easily, using the attached Sources.
And now I have a favor ask to you: Maybe you can report that bug to Microsoft? Or - if the Bug-Report already exists - rate it up a little bit?
When I try to do so by myself, I get the meaningful reply:
Quote:
You are not authorized to submit the feedback for this connection.
... end of part one ...
(and now for something completely different...)
Visualize Algorithms with Async/Await
Async/Await opens a new way of visualizing Algorithms.
Before Async/Await one either used the critical Application.DoEvents()
to keep the Gui not completely blocked, or one had to transform an algorithm into a kind of timer-driven "State-Machine", which steps forward each tick.
These State-Machines still are the most economical use of resources, but a transformed algorithm - even the simpliest Foreach-Loop - looks completely different and can no longer be recognized as the original algorithm.
But now we can leave the algorithm as it is, only inserting a single line: Await Task.Delay(100)
- and our algorithm will pause for that time, while Gui stays fully responsive, and we can see the Visualisation.
For instance i created a floodfill-algorithm, in two variants, the one uses a stack, the other a queue - see some code:
1 Private Async Sub FloodFillQueue(grid As DataGridView, start As DataGridViewCell)
2 Dim uBound = New Point(grid.ColumnCount - 1, grid.RowCount - 1)
3 Dim validColor = CInt(start.Value)
4 Dim newColor = If(validColor = _Colors(0), _Colors(1), _Colors(0))
5 Dim queue = New Queue(Of Point)
6 queue.Enqueue(New Point(start.ColumnIndex, start.RowIndex))
7 While queue.Count > 0
8 If _IsStopped Then Return
9 Dim pos = queue.Dequeue
10 If Not (pos.X.IsBetween(0, uBound.X) AndAlso pos.Y.IsBetween(0, uBound.Y)) Then Continue While
11 If CInt(grid(pos.X, pos.Y).Value) <> validColor Then Continue While
12 Await Wait()
13 If grid.IsDisposed Then Return
14 grid(pos.X, pos.Y).Value = newColor
15 For Each offset In _NeighborOffsets
16 queue.Enqueue(pos + offset)
17 Next
18 End While
19 End Sub
I think the algo is not that complicated: In the While-loop each time a Point is taken from the Queue, as current position. Then check, whether it is valid, and if so, set a new color and enqueue all its neighbors.
The position can be invalid depending either on its X/Y-Values, or else on the (color-)Value of the DataGridView at that position - valid is the color of the start-cell at the beginning.
To us the most important is the call: Await Wait()
, because i created an alternative to Task.Delay()
- look:
Private _Blocker As New AutoResetEvent(False)
Private Function Wait() As Task
If ckAutoRun.Checked Then Return Task.Delay(50)
Return task.Run(Sub() _Blocker.WaitOne())
End Function
Private Sub btStep_Click(sender As Object, e As EventArgs) Handles btStep.Click
_Blocker.Set()
End Sub
You see: There are two "waiting-modes", the first - "AutoRun" - is implemented - as one can expect - by Task.Delay(50)
.
The second mode - name it "Stepwise" - is done by starting a Task, which does nothing else but being blocked by an AutoResetEvent
, until btStep
signals to unblock - then immediately runs out. Awaiting this "stupid" Task also results in an gui-unblocking Delay, but now of userdefined duration, instead of predefined delay-time.
Sidenote about FloodFill
By chance i discovered a fascinating behavior: Since Async/Await keeps my Gui responsive I can start several Floodfill-Executions!
And moreover i have two different Algorithms, with different preference, which cell as next is to enter.
So when i start a FloodFill, changing green cells to red, and then another one, meanwhile, changing reds to green, they "eat each other" and the result is a kind of chaos-animation, similar to game-of-live and stuff like that.
I really recomend to play a bit with that :-)
On a rational view the comparison of stack-floodfill with queue-floodfill shows very clearly: A real flooding (like fluid - in every direction) occurs when using a queue. The stack-floodfill behaves more like a path-finder or a "turtle-process" trying to escape in a particular direction.
Moreover the stack-version takes much more memory, because in its "midlife-time" it pushes much more items on than it pops off. On the other hand the queue-floodfill creates a closed region of visited positions, and the inner area consists of positions, which are already dequeued.
(Hopefully you understand what i mean - in fact, to the main-topic it is not that important ;-) )
Summary
This article covered (no, not "covered" - better: "touched") one single purpose of Threading: namely to keep the Gui responsive, while long lasting operations are in progress. We saw several demands immediately come up, so one can see the purpose "responsive gui" as a conglomerate of five challenges: 1) Responsivity, 2) Restrict Gui and release it afterwards, 3) Progress-Report, 4) Cancelation, 5) Exception-Handling.
Whereas Cancelation and Exception-Handling were an unpleasantly surprise with the Task.Run()
-Bug, requiring the AsyncHelper.Run()
-Workaround until Microsoft will fix the Bug.
Async/Await still is a bit miracle to me, and i don't feel absolutely shure with it. Eg the usage of Await in loops - is there each turn a Task
built and destroyed? Or even occupied and released? How performant is it, and how resource-efficient?
Last but not least: Maybe you'd like to refer to Paolo Zemeks Article "Async/Await Could Be Better" - his article dives deeper into questions about performance and resources, than mine.