Introduction
Support for asynchronous procedures (using async
/ await
) is great in C# and VB languages, and works very well in desktop (WinForms, WPF, console and others) applications. But there are web applications, like based on ASP.NET Web Forms, where support for asynchronous procedures are much less exposed. Microsoft itself states, that support for asynchrony (using async
/ await
or other methods) is only limited to offload working threads, increase throughput and all asynchronous procedures must complete work before rendering phase (generating HTML).
But there are also much more useful scenarios, where asynchronous procedure is started in some postback, and completed in one of subsequent postbacks, so executing spans over multiple postacks. This allows for UI driven asynchronous processing, which is useful for most web apps. There are no examples or explanations how to do this, and some folks even state that this is impossible. Fortunately, this is possible, and here is how to do it.
Description of Method
Everything necessary to do this is to feed await
with non-started task, store this task and run it in some subsequent postback, on user's request. This way, any execution of continuations will be limited only to page processing phase and we have full control over it. But for our needs, using tasks is not the best option. It is better to create custom awaiters similar to triggers, and switch them on user's request, to continue execute of asynchronous procedures.
Guide how to create custom awaiters can be found here: link.
Code Examples
Here is the first example. This is code with html markup (for simplicity). Copy this example into a single file with name AsyncDemo1.aspx.
<!DOCTYPE html>
<script runat="server">
Shared tr As TriggerAwaiter
Shared fi As asyncdemo1_aspx
Public Class TriggerAwaiter
Implements Runtime.CompilerServices.INotifyCompletion
Dim Action As Action = Nothing
Public Sub RunAction()
If Action Is Nothing Then Exit Sub
Dim a = Action
Action = Nothing
a()
End Sub
Public ReadOnly Property IsCompleted As Boolean = False
Public Sub OnCompleted(continuation As Action) _
Implements System.Runtime.CompilerServices.INotifyCompletion.OnCompleted
Action = continuation
End Sub
Public Sub GetResult()
End Sub
Public Function GetAwaiter() As TriggerAwaiter
Return Me
End Function
End Class
Protected Overrides Sub OnLoad(e As EventArgs)
fi = Me
MyBase.OnLoad(e)
End Sub
Protected Sub StartAsyncBTN_Click(sender As Object, e As EventArgs) Handles StartAsyncBTN.Click
Dim af2 = Async Function()
Await GetTrigger()
Dim x = 0
End Function
Dim af = Async Function()
fi.StartAsyncBTN.Enabled = False
fi.MakeProgresBTN.Enabled = True
fi.ResultTXT.Text = "async work started"
Await af2()
fi.ResultTXT.Text &= ", work 1"
Await af2()
fi.ResultTXT.Text &= ", work 2"
Await GetTrigger()
fi.ResultTXT.Text &= ", works finished"
fi.StartAsyncBTN.Enabled = True
fi.MakeProgresBTN.Enabled = False
End Function
af()
End Sub
Public Function GetTrigger() As TriggerAwaiter
tr = New TriggerAwaiter
Return tr
End Function
Protected Sub MakeProgresBTN_Click(sender As Object, e As EventArgs) Handles MakeProgresBTN.Click
tr.RunAction()
End Sub
</script>
Also, modify web.config file by adding:
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
into app settings region. This will ensure that LegacyAspNetSynchronizationContext
is used as synchronization context. This context executes all continuations on a single thread.
For those who want to use newer AspNetSynchronizationContext
, let's change web.config to:
<add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
But additional work is necessary. AspNetSynchronizationContext
executes different continuations using different threads, and we must track execution of continuations, to be able to wait when they finish their work or acquire new awaiter / trigger.
Here is a second example that shows how to wait for continuations executing on different threads. Copy this example into a single file with name AsyncDemo2.aspx.
<!DOCTYPE html>
<script runat="server">
Shared tr As TriggerAwaiter
Shared fi As asyncdemo2_aspx
Shared AT As New AsyncTrack
Public Class TriggerAwaiter
Implements Runtime.CompilerServices.INotifyCompletion
Dim Action As Action = Nothing
Public Sub RunAction()
If Action Is Nothing Then Exit Sub
Dim a = Action
Action = Nothing
a()
End Sub
Public ReadOnly Property IsCompleted As Boolean = False
Public Sub OnCompleted(continuation As Action) _
Implements System.Runtime.CompilerServices.INotifyCompletion.OnCompleted
Action = continuation
End Sub
Public Sub GetResult()
End Sub
Public Function GetAwaiter() As TriggerAwaiter
Return Me
End Function
End Class
Public Class AsyncTrack
Private Class WaitingTasks
Public Awaiter As TriggerAwaiter = Nothing
Public Tasks As New List(Of Threading.Tasks.Task)
End Class
Dim SO As New Object
Dim WTS As New List(Of WaitingTasks)
Dim ExecWT As WaitingTasks = Nothing
Public Sub SetAwaiter(Awaiter As TriggerAwaiter)
SyncLock SO
If ExecWT Is Nothing Then
WTS.Add(New WaitingTasks With {.Awaiter = Awaiter})
Else
ExecWT.Awaiter = Awaiter
End If
End SyncLock
End Sub
Public Function TrackSub(SubProc As Func(Of Threading.Tasks.Task)) As Threading.Tasks.Task
Dim t = SubProc()
If t.Status = Threading.Tasks.TaskStatus.RanToCompletion Then Return t
SyncLock SO
WTS.Last.Tasks.Add(t)
End SyncLock
Return t
End Function
Public Function TrackProc(Of T1)(SubProc As Func(Of Threading.Tasks.Task(Of T1))) As Threading.Tasks.Task(Of T1)
Dim t = SubProc()
If t.Status = Threading.Tasks.TaskStatus.RanToCompletion Then Return t
SyncLock SO
WTS.Last.Tasks.Add(t)
End SyncLock
Return t
End Function
Public Sub SwitchAndWait(Awaiter As TriggerAwaiter)
SyncLock SO
ExecWT = WTS.Where(Function(x) x.Awaiter Is Awaiter).FirstOrDefault()
If ExecWT Is Nothing Then Exit Sub
ExecWT.Awaiter = Nothing
End SyncLock
Awaiter.RunAction()
Dim c = 0
While 1
SyncLock SO
If ExecWT.Awaiter IsNot Nothing Then
ExecWT = Nothing
Exit Sub
End If
If ExecWT.Tasks.All(Function(x) x.Status = _
Threading.Tasks.TaskStatus.RanToCompletion) Then
Exit While
Else
Threading.Thread.Sleep(10)
c += 1
End If
End SyncLock
End While
SyncLock SO
WTS.Remove(ExecWT)
ExecWT = Nothing
End SyncLock
End Sub
End Class
Protected Overrides Sub OnLoad(e As EventArgs)
fi = Me
MyBase.OnLoad(e)
End Sub
Protected Sub StartAsyncBTN_Click(sender As Object, e As EventArgs) Handles StartAsyncBTN.Click
Dim af2 = Async Function()
Await GetTrigger()
fi.ResultTXT.Text &= ", pre work _
(TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
Dim n = 0
End Function
Dim af = Async Function()
fi.StartAsyncBTN.Enabled = False
fi.MakeProgresBTN.Enabled = True
fi.ResultTXT.Text = "async work started"
Await af2()
fi.ResultTXT.Text &= ", work 1 _
(TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
Await af2()
fi.ResultTXT.Text &= ", work 2 _
(TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
Await GetTrigger()
fi.ResultTXT.Text &= ", works finished _
(TH:" & Threading.Thread.CurrentThread.ManagedThreadId & ")"
fi.StartAsyncBTN.Enabled = True
fi.MakeProgresBTN.Enabled = False
End Function
AT.TrackSub(af)
End Sub
Public Function GetTrigger() As TriggerAwaiter
tr = New TriggerAwaiter
AT.SetAwaiter(tr)
Return tr
End Function
Protected Sub MakeProgresBTN_Click(sender As Object, e As EventArgs) Handles MakeProgresBTN.Click
AT.SwitchAndWait(tr)
End Sub
</script>
You do not have to track every procedure, only 'outer' asynchronous procedures, where asynchronous processing starts.
Points of Interest
I invented this method mainly for my needs. Just wanted to remove the previous solution based on traditional approach (without using async
/ await
), because it was convoluted and problematic to use.