Click here to Skip to main content
15,116,516 members
Articles / Programming Languages / Visual Basic 10
Article
Posted 12 Sep 2015

Stats

18.9K views
467 downloads
18 bookmarked

Async/Await: Unblock Gui without any additional Line of Code

Rate me:
Please Sign up or sign in to vote.
4.91/5 (16 votes)
12 Sep 2015CPOL10 min read
After simple introduction the Article turns to less known, but useful/important/interesting Aspects of unblocking the Gui with Async/Await

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:

VB.NET
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)  ' blocks for 5 milliseconds
   Next
   Return result
End Function

Assume a Button to load and display it:

VB.NET
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:

VB.NET
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:

VB.NET
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:

VB.NET
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:

VB.NET
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.

VB.NET
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:

VB.NET
  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():

VB.NET
  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) 'pass any exception to the Completion
 16                    Exit Sub
 17                 End Try
 18                 tcs.SetResult(result) 'pass result to the Completion
 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:

VB.NET
  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 ' long lasting, in parallel...
  7                       tcs.SetResult(func()) 'pass the result to the Completion, or...
  8                    Catch ex As Exception
  9                       tcs.SetException(ex) '...on arbitrary exception pass it to the Completion
 10                    End Try
 11                 End Sub)
 12        Return tcs.Task ' but return the Completion immediately
 13     End Function
 14  
 15  End Class

And now replace Task.Run() with AsyncHelper.Run()

VB.NET
  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:

VB.NET
  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:

VB.NET
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.

Image 1

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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Mr.PoorEnglish
Germany Germany
No Biography provided

Comments and Discussions

 
GeneralMy vote of 5 Pin
Member 122885164-Jul-16 20:09
MemberMember 122885164-Jul-16 20:09 
GeneralVery Nice Pin
RaviBattula14-Sep-15 1:09
professionalRaviBattula14-Sep-15 1:09 
GeneralRe: Very Nice Pin
Mr.PoorEnglish14-Sep-15 21:30
MemberMr.PoorEnglish14-Sep-15 21:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.