Ask Learn
Preview
Please sign in to use this experience.
Sign inThis browser is no longer supported.
Upgrade to Microsoft Edge to take advantage of the latest features, security updates, and technical support.
Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
[This post is part of a series How to await a storyboard, and other things]
The normal behavior of the "await" operator on a task is to suspend execution of the method; then, when the task operand has finished, to resume execution on the same SynchronizationContext and with the same ExecutionContext.
You can replace the part in italics with your own logic by awaiting an operand of your own type (not a task) and writing a custom awaiter for it. It's not a common need. Stephen Toub has already written an excellent article that covers this topic ("Await anything"). He explains that a custom awaiter is for when "you need full control over how (rather than when) the method resumes". I wanted to build on his article from a compiler-writer's perspective and with some more example code. Here are some examples I've seen of custom awaiters.
The above two scenarios were thought common enough that they're provided in the .NET45 framework. The rest aren't...
Here's some background reading to explain the concepts...
When you use the Await operator, the compiler expands it out into something a bit like this:
' ORIGINAL CODE
Dim r = Await t
' APPROXIMATELY WHAT THE COMPILER GENERATES
Dim tmp = t.GetAwaiter()
If Not tmp.IsCompleted Then
Dim ec = ExecutionContext .Capture()
CType(tmp, INotifyCompletion).OnCompleted( Sub () ExecutionContext .Run(ec, K1 ))
Return
End If
K1:
Dim r = tmp.GetResult()
' Some things not shown in the above simplification:
' The two lines in italics are executed from within a routine inside mscorlib
' The argument "K1" represents a delegate that, when executed, resumes execution below.
' If "tmp" is a struct, any mutations after IsCompleted before GetResult may be lost.
' If tmp implements ICriticalNotifyCompletion, then it calls tmp.UnsafeOnCompleted instead.
' The variable "ec" gets disposed at the right time inside the Sub()
' If ec is null, then it invokes K1 directly instead of within ExecutionContext.Run
' There are optimizations for the case where ExecutionContext is unmodified.
From the compiler perspective, here's the bare minimum that you need to implement to be able to compile "Await t":
If you do a late-bound await then the rules are a little different. A late-bound await is "await t" where t has type dynamic (C#) or Object (VB with Option Strict Off).
1. Dim tmp = t.GetAwaiter() must compile as a late-bound invocation, and it will use the normal late-binder rules: it works with optional parameters, and IDynamicMetaObjectProvider, and even with a delegate field named GetAwaiter.
2. Dim b = tmp.IsCompleted must compile as a normal late-bound property access.
3. tmp must implement INotifyCompletion or ICriticalNotification. As before, it must implement them through CLR interface implementation; not through dynamic casts.
4. tmp.GetResult() must compile as a normal late-bound invocation, and the result of the late-bound invocation is the result of the late-bound Await operator.
These are only the bare-minimum requirements for the code to compile and have kind of specified behavior at runtime. The rest of it, of how you implement GetAwaiter/IsCompleted/OnCompleted/GetResult, is entirely up to you and convention.
Here is code for a custom awaiter that satisfies all the compiler's requirements, and behaves approximately similarly to how the default TaskAwaiter behaves ("suspend execution of the method; then, when the operand has finished, resume execution on the same SynchronizationContext and with the same ExecutionContext").
This code is pointless! The only reason you'd write a custom awaiter is because you want something different from the default TaskAwaiter. (After all, if you were happy with the default TaskAwaiter behavior, then it'd be more efficient and more robust to just create one via TaskCompletionSource.Task.GetAwaiter). I'm putting this code here just as a starting point, to diverge from it. I've also made fields public to keep the code simple.
Public Class MyTask(Of T)
Public result As T
Public isCompleted As Boolean
Public continuations As New List(Of Action)
Public mutex As New Object
Public Function GetAwaiter() As MyTaskAwaiter(Of T)
Return New MyTaskAwaiter(Of T) With {.task = Me}
End Function
Public Sub SetResult(value As T)
Dim cc As List(Of Action)
SyncLock mutex
result = value
isCompleted = True
cc = continuations
continuations = Nothing
End SyncLock
For Each c In cc : c() : Next
End Sub
Public Sub AddContinuation(continuation As action)
SyncLock mutex
If Not isCompleted Then continuations.Add(continuation) : Return
End SyncLock
Task.Run(continuation)
End Sub
End Class
Public Class MyTaskAwaiter(Of T)
Implements System.Runtime.CompilerServices.INotifyCompletion
Public task As MyTask(Of T)
Public ReadOnly Property IsCompleted As Boolean
Get
SyncLock task.mutex
Return task.isCompleted
End SyncLock
End Get
End Property
Public Function GetResult() As T
Return task.result
End Function
Public Sub OnCompleted(continuation As Action) _
Implements INotifyCompletion.OnCompleted
Dim sc = If(SynchronizationContext.Current, New SynchronizationContext)
task.AddContinuation(Sub() sc.Post(Sub() continuation(), Nothing))
End Sub
End Class
Alternatively, if your code allows partially trusted callers, then the above code would be a security hole. Here's how we could write MyTaskAwaiter instead for this case:
<Assembly: Security.AllowPartiallyTrustedCallers>
Public Class MyTaskAwaiter(Of T) : Implements ICriticalNotifyCompletion
Public task As MyTask(Of T)
Public ReadOnly Property IsCompleted As Boolean
Get
SyncLock task.mutex
Return task.isCompleted
End SyncLock
End Get
End Property
Public Function GetResult() As T
Return task.result
End Function
<Security.SecurityCritical>
Public Sub UnsafeOnCompleted(continuation As Action) _
Implements ICriticalNotifyCompletion.UnsafeOnCompleted
Dim sc = If(SynchronizationContext.Current, New SynchronizationContext)
task.AddContinuation(Sub() sc.Post(Sub() continuation(), Nothing))
End Sub
Public Sub OnCompleted(continuation As Action) _
Implements INotifyCompletion.OnCompleted
Throw New NotSupportedException()
End Sub
End Class
AllowPartiallyTrustedCallers. Part of the .NET security model is that, if a method in your assembly can be invoked from a partially trusted caller, then none of your APIs can allow that caller to break free of the ExecutionContext they started in. A partially-trusted attacker might call the first MyTaskAwaiter.OnCompleted() directly (i.e. bypassing the Await operator) and hence execute an arbitrary lambda in the ExecutionContext of whoever invoked MyTask.SetResult(). The second version works around this by implementing ICriticalNotifyCompletion and putting <SecurityCritical> on the UnsafeOnCompleted method to prevent partially-trusted callers from invoking it. We already saw that the compiler prefers to use the ICriticalNotifyCompletion interface if it's available, over INotifyCompletion. We also saw that the generated code's call to UnsafeOnCompleted is made indirectly via a routine in mscorlib, which is why it's able to invoke <SecurityCritical> code even if your assembly is only partially trusted. (The code for MyTask would also have to be tightened up, so that continuations/AddContinuation can't be touched by partially trusted callers).
Race conditions. I believe the above code is free of race conditions and other concurrency bugs but I'm not sure. My first version suffered from several concurrency bugs. First bug: I forgot the lock on MyTask.AddContinuation(), allowing this interleaving:
Second bug: I forgot the lock on MyTaskAwaiter.IsCompleted, so the code was vulnerable to memory reordering:
Third bug: In MyTask.SetResult(), the first version of my code looped over the continuations and executed them from inside the SyncLock. But this would have led to deadlock if any of the continuations themselves tried to await mytask from a different thread. The solution was to execute the continuations outside the SyncLock.
Fourth bug: In AddContinuation, the first version of my code executed the continuation directly. But this would have blown the stack if I had a loop which awaited a MyTask that had already completed. The solution was to schedule the continuation for later execution with Task.Run(continuation).
NotSupportedException. The AllowPartiallyTrustedCallers version of the code throws a NotSupportedException for anyone who calls its implementation of INotifyCompletion.OnCompleted. I figured there was no need, and it's hard to get it right so I'm better off just throwing the exception. After all, I'm only doing my custom awaiter to support the Await operator, and the compiler specs spell out the fact that the compiler won't invoke INotifyCompletion.OnCompleted on an awaiter that implements ICriticalNotifyCompletion.
What to do if SynchronizationContext.Current is null? My custom awaiter uses this code:
Dim sc = If(SynchronizationContext.Current, New SynchronizationContext) ' VB
var sc = SynchronizationContext.Current ?? new SynchronizationContext(); // C#
I did as part of implementing the common convention, that the "Await" operator will resume execution on whatever SynchronizationContext was current before it started. The question arises of what to do if Synchronization.Current was null, as is the case in a Console application or a unit test, or in the body of Task.Run(): where should the continuation be posted? In my code, the new synchronization context will post the continuation-delegate to the threadpool, which is decent-enough behavior.
The custom awaiter code is difficult – difficult to follow the conventions, and difficult to write without concurrency bugs. If at all possible it's better to stick to the standard TaskAwaiter, or at least to build your own custom awaiter as a wrapper around TaskAwaiter.
Anonymous
May 08, 2014
Thank you for this informative article. But I don't understand your synchronization issues.
Dim r = Await t
Is is allowed to await the same object (t in your case) in multiple threads at the same time?
This is very uncommon and won't work for the mot objects (like files...) that maintain internal state.
Thanks in advance
Anonymous
May 09, 2014
Hi Thomas. Sure you can await it on multiple threads at the same time! After all, "Await t" is just the compiler expansion I indicated in the post. It depends on the semantics of the thing being awaited whether this makes sense. Here's an example where it does make sense:
t = httpClient.GetStringAsync(url)
Dim worker1 = Task.Run(AddressOf Worker1)
Dim worker2 = Task.Run(AddressOf Worker2)
Await Task.WhenAll(worker1, worker2)
Async Function Worker1() As Task
Dim s = Await t : ...
End Function
Async Function Worker2() As Task
Dim s = Await t : ...
End Function
In fact, the beautiful thing about Task is that lots of threads can await it, and once it has completed then it is immutable. This actually makes it a really good synchronization primitive for passing data around between threads. e.g.
' STARTUP CODE EXECUTED ON MAIN THREAD
t = Await folder.GetFileAsync("info.txt")
' WORKER CODE GETS KICKED OFF ON A BACKGROUND THREAD
Dim file = Await t
This makes it really easy to transfer data (e.g. the reference to the StorageFile object) from the main thread onto a background thread.
Anonymous
December 06, 2017
Hi, yeah this article is genuinely good and I have learned lot of things from it concerning blogging.thanks.
Please sign in to use this experience.
Sign in