1. The problem
The concept of the [DispatcherWaiter] presented in my previous post Convert BeginInvoke to async/await solves the problem of switching back to GUI thread after await
some task in a almost perfect way:
1
2
3
4
5
6
7
8
9
10
11
private async Task DoSomethingAsync()
{
////can be any thread
await SomeTask.ConfigureAwait(false);
////can be any thread
await waiter.WaitAsync();
////The thread associated with the Dispatcher of the waiter, aka GUI thread
DoSomethingOnGUIThread();
}
But await waiter.WaitAsync()
still seems clumsy, in this article we’ll push further to remove it.
To briefly recap the usages of ConfigureAwait(bool continueOnCapturedContext)
, it provides a hook to change the behavior of how the await behaves via this custom awaiter.
-
ConfigureAwait(true)
- it’s the default behavior and can be omitted. The state machine code generated by the compiler would automatically marshal the continuation back to the original context captured, aka ifawait SomeTask
is called onGUI
thread, afterSomeTask
is completed, execution would continue onGUI
thread. -
ConfigureAwait(false)
- needs to be specified explicitly. It’s recommended to always doing so in library codes to improve performance and avoid potential deadlocks. Basically the captured context would be ignored when the generated code sees this and it would continue execution on the same thread the task is complete.
While ConfigureAwait(bool)
is enough to serve most of the cases, however, it’s tricky in the case of await
several tasks, and use their results on the GUI
thread.
In the below example, if DoSomethingAsync
is called on GUI
thread, aka Thread 1
, then Thread 2
and Thread 3
will also be GUI
thread, eventually DoSomethingOnGUIThread
will be executed on GUI
thread too, as expected:
1
2
3
4
5
6
7
8
9
10
private async Task DoSomethingAsyncV1()
{
////Thread 1 => GUI Thread
var result1 = await BackgroundTask1; ////Equivalent to ConfigureAwait(true)
////Thread 2 => GUI Thread
var result2 = await BackgroundTask2; ////Equivalent to ConfigureAwait(true)
////Thread 3 => GUI Thread
DoSomethingOnGUIThread(result1, result2);
}
Since there’re two background tasks, it seems unnecessary to spin off the GUI
thread and marshal back to GUI
thread two times, we might want to save the first one by adding ConfigureAwait(false)
, but it might not work because Thread 2
and Thread 3
can be end up called on background thread, depends on how fast the BackgroundTask1
is complete.
To fix it, we need to refactor the code a little bit to extract the execution of BackgroundTask1
and BackgroundTask2
to a separate method GetResultAsync()
, and ConfigureAwait(false)
all the way inside it, as shown below. It has more codes to write but works, in fact it’s the correct way to do the job:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private async Task DoSomethingAsyncV2()
{
////Thread 1 => GUI Thread
var tuple = await GetResultAsync(); ////Equivalent to ConfigureAwait(true)
////Thread 2 => GUI Thread.
DoSomethingOnGUIThread(tuple.Item1, tuple.Item2);
}
private async Task<Tuple<int, int>> GetResultAsync()
{
////Thread 1 => GUI Thread
var result1 = await BackgroundTask1.ConfigureAwait(false);
////Thread 2 => might be background Thread
var result2 = await BackgroundTask2.ConfigureAwait(false);
////Thread 3 => might be background Thread
return Tuple.Create(result1, result2);
}
Although it’s the correct way but developers might not recognize it and stick to the original way of adding ConfigureAwait(false)
to await BackgroundTask1
and get exceptions. By using [DispatcherWaiter] it saves the refactoring work of the DoSomethingAsyncV2
above and keeps the simplicity similar to DoSomethingAsyncV1
:
1
2
3
4
5
6
7
8
9
10
11
12
13
private async Task DoSomethingAsyncV3()
{
////Thread 1 => GUI Thread
var result1 = await BackgroundTask1.ConfigureAwait(false);
////Thread 2 => might be background Thread
var result2 = await BackgroundTask2.ConfigureAwait(false);
////Thread 3 => might be background Thread
await waiter.WaitAsync();
////Thread 4 => GUI Thread
DoSomethingOnGUIThread(result1, result2);
}
So far it’s already pretty elegant to use [DispatcherWaiter] to simplify things, but what if we can use ConfigureAwait(false)
all the way on all of the task calls and only add something like ConfigureAwait(DispatcherWaiter)
to the last one to automatically marshal back to GUI
thread (or whatever thread we want it be)?
1
2
3
4
5
6
7
8
9
10
private async Task DoSomethingAsync()
{
////Thread 1 => GUI Thread
var result1 = await BackgroundTask1.ConfigureAwait(false);
////Thread 2 => might be background Thread
var result2 = await BackgroundTask2.ConfigureAwait(DispatcherWaiter); ////Note the parameter is DispatcherWaiter
////Thread 4 => GUI Thread
DoSomethingOnGUIThread(result1, result2);
}
2. Analysis
Given that [DispatcherWaiter] gets its behavior from Dispatcher
, to simplify discussion we only look into implement ConfigureAwait(Dispatcher)
.
.net
gives us two ways to implement custom awaitable
behavior:
1) Match the pattern recognized by the compiler. See below from await anything.
The languages support awaiting any instance that exposes the right method (either instance method or extension method):
GetAwaiter
. A GetAwaiter needs to implement theINotifyCompletion
interface (and optionally theICriticalNotifyCompletion
interface) and return a type that itself exposes three members:
1
2
3
bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult(); // TResult can also be void
Open source code of the ConfigureAwait(bool)
method we can see that it creates an instance of ConfiguredTaskAwaitable
, which has a GetAwaiter
method that returns an instance of ConfiguredTaskAwaiter
, the latter implements the ICriticalNotifyCompletion
and INotifyCompletion
interface, then compiler generates corresponding state machine codes to get the awaitable
behavior.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
{
return new ConfiguredTaskAwaitable(this, continueOnCapturedContext);
}
public struct ConfiguredTaskAwaitable
{
internal ConfiguredTaskAwaitable(Task task, bool continueOnCapturedContext)
{
this.m_configuredTaskAwaiter = new ConfiguredTaskAwaitable.ConfiguredTaskAwaiter(task, continueOnCapturedContext);
}
public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter()
{
return this.m_configuredTaskAwaiter;
}
public struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion
{
}
}
2) Use AsyncMethodBuilder
. AsyncMethodBuilder
is used in advanced scenarios to implement some custom type, for example ValueTask
, that is awaitable
just like Task
. Here’s some details from Async Task Types in C#:
A task type is a class or struct with an associated builder type identified with
System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
. The task type may be non-generic, for async methods that do not return a value, or generic, for methods that return a value.
This article demonstrates one example of using AsyncMethodBuilder
to achieve something similar to our goal. However, as mentioned by Stephen Toub in this dotnet/csharplang proposal, currently there’s no way to pass context to AsyncMethodBuilder
because Attribute
knows static information only.
1
2
3
4
5
6
7
8
9
10
11
public struct AsyncCoolTypeMethodBuilder
{
public static AsyncCoolTypeMethodBuilder Create();
......
}
[AsyncMethodBuilder(typeof(AsyncCoolTypeMethodBuilder))]
public struct CoolType { ...... }
//// will implicitly use AsyncCoolTypeMethodBuilder.Create()
public async CoolType SomeMethodAsync() { ...... }
AsyncMethodBuilder
can still be used for applications that only has one GUI
thread - the custom Task Type
can directly access Application.Current.Dispatcher
. But for more advanced and complicated application that has multiple GUI
threads running, Dispatcher
of the current GUI
thread has to be passed as context. So although AsyncMethodBuilder
aim for advanced usages but it cannot help in our case.
To summaries, #1
is the only possible way to implement what we want under the current .net
versions.
3. Implementations
Coding is fairly straightforward. Please not that the only difference between Task
andTask<T>
in terms of behavior is whether the task has return value or not, so we extract the common logic out to DispatchTaskAwaiterHelper
to reuse. Non-generic version of DispatchTaskAwaiter
and generic version of DispatchTaskAwaiter<T>
just call void GetResult
and GetResult<T>
respectively.
One thing worth paying attention is the that even though ICriticalNotifyCompletion
inherits from INotifyCompletion
, codes generated by the default AsyncMethodBuilder
shipped with .net
would only call UnsafeOnCompleted(Action)
for Tasks, so there’s no need to implement OnCompleted(Action)
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public readonly struct DispatchTaskAwaiter : ICriticalNotifyCompletion
{
private readonly DispatchTaskAwaiterHelper _helper;
public DispatchTaskAwaiter(Task task, Dispatcher dispatcher)
{
_helper = new DispatchTaskAwaiterHelper(task, dispatcher);
}
public DispatchTaskAwaiter GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get { return _helper.IsCompleted; }
}
public void OnCompleted(Action continuation)
{
////This is not called, check out https://devblogs.microsoft.com/pfxteam/whats-new-for-parallelism-in-net-4-5-beta/ for more details
throw new NotImplementedException();
}
public void UnsafeOnCompleted(Action continuation)
{
_helper.UnsafeOnCompleted(continuation);
}
public void GetResult()
{
_helper.GetResult();
}
}
DispatchTaskAwaiterHelper
does the real job:
-
Since a
Task
can beawaited
multiple times, so the generated codes would access the awaiter’sIsCompleted
property first to determine whether it needs to callUnsafeOnCompleted
or not. IfIsCompleted==true
it directly jump to callGetResult
. In theIsCompleted
all we need to do is to check if the original task is completed and if we’re on currently theGUI
Thread. -
If
IsCompleted==false
thenUnsafeOnCompleted
would be called with acontinuation
delegate passed in ascallback
. In our case once theTask
is complete, before callingcontinuation
, we callDispatcher.BeginInvoke
to marshal back to theGUI
Thread. -
Exception handling. The codes need to make sure exceptions thrown by the original
Task
can be captured by thetry...catch
block in caller codes. One easy to think of approach is to throw in theUnsafeOnCompleted
when the originalTask
has exceptions. But in fact because the exception handling logic is inside of thecontinuation
, exceptions thrown inUnsafeOnCompleted
won’t be captured and will become visible only to the event listener ofTaskScheduler.UnobservedTaskException
whenGC
happens.GetResult
method is the only place can be used to handle the exception, exceptions thrown byVerifyException()
would be captured by the caller onGUI
Thread, which matches the same behavior ofConfigureAwait(true)
.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public readonly struct DispatchTaskAwaiterHelper
{
private readonly Task _task;
private readonly Dispatcher _dispatcher;
public DispatchTaskAwaiterHelper(Task task, Dispatcher dispatcher)
{
_task = task;
_dispatcher = dispatcher;
}
public bool IsCompleted
{
get { return _task.IsCompleted && _dispatcher.CheckAccess(); }
}
public void UnsafeOnCompleted(Action continuation)
{
var tmp = this;
_task.ContinueWith(t => tmp._dispatcher.BeginInvoke(continuation));
}
/// <summary>
/// Called by non-generic DispatchTaskAwaiter
/// </summary>
public void GetResult()
{
VerifyException();
}
/// <summary>
/// Called by generic DispatchTaskAwaiter<T>
/// </summary>
public T GetResult<T>()
{
VerifyException();
return ((Task<T>) _task).Result;
}
private void VerifyException()
{
if (!_task.IsCompleted)
{
throw new InvalidOperationException("Task is unexpectedly not completed");
}
if (_task.IsCanceled)
{
throw new TaskCanceledException();
}
if (_task.Exception != null)
{
throw _task.Exception;
}
}
}
4. Usages
To use DispatchTaskAwaiter
we can use below extension methods for Task
and Task<T>
:
1
2
3
4
5
6
7
8
9
10
11
12
public static class TaskExt
{
public static DispatchTaskAwaiter ConfigureAwait(this Task task, Dispatcher dispatcher)
{
return new DispatchTaskAwaiter(task, dispatcher);
}
public static DispatchTaskAwaiter<T> ConfigureAwait<T>(this Task<T> task, Dispatcher dispatcher)
{
return new DispatchTaskAwaiter<T>(task, dispatcher);
}
}
And use them as simple as this:
1
2
3
4
5
6
7
8
9
10
11
12
private async Task DoSomethingAsync()
{
////Thread 1 => GUI Thread
var result1 = await BackgroundTask1.ConfigureAwait(false);
////The _dispatcher is the reference to the GUI thread dispatcher saved when the view model is created
////Thread 2 => might be background Thread
var result2 = await BackgroundTask2.ConfigureAwait(_dispatcher);
////Thread 3 => GUI Thread
DoSomethingOnGUIThread(result1, result2);
}
It’s fairly easy to extend the theory to ConfigureAwait(DispatcherWaiter)
, that can be covered in some other posts.
Please visit github for the full implementations.
References
1. 问题
前文《从BeginInvoke到async/await》
给出了DispatcherWaiter
的概念和实现,近乎完美地解决了await
某个Task
之后优雅地切换到GUI
线程的问题:
1
2
3
4
5
6
7
8
9
10
11
private async Task DoSomethingAsync()
{
////此处为任何线程
await SomeTask.ConfigureAwait(false);
////此处为任何线程
await waiter.WaitAsync();
////此处为waiter所包含的Dispatcher所在线程,即GUI线程
DoSomethingOnGUIThread();
}
然而如果我们把追求完美的步伐再往前推进一步,上述代码还是略显累赘,能否省掉await waiter.WaitAsync()
呢?
先简单回顾一下Task
后面的ConfigureAwait(bool continueOnCapturedContext)
的作用,它实际上提供了一个改变await
行为的钩子
-
ConfigureAwait(true)
——缺省值,可不写。使得编译器生成的状态机代码在异步代码执行完成后回到调用异步代码的线程。也就是说如果在主线程await
了一个在后台线程执行的任务,任务结束后会自动回到主线程。 -
ConfigureAwait(false)
——需明确指明。使得编译器生成的状态机代码在异步代码执行完成后不必切换线程。一般推荐在库代码中一路使用到底,因为在库函数内部虽然可能调用其它在后台执行的异步代码,但通常并不关心GUI
线程,这样做可以省去线程切换带来的额外开销,也可以避免使用不当情况下的死锁。
ConfigureAwait(bool)
一般来说下够用,然而在界面代码中难免会有调用多个Task
后回到主线程的情形。比如下面的代码异步获得两个Task
的结果并在主线程上使用。
如下的例子中,在没有ConfigureAwait(true)
的缺省情况下,如果DoSomethingAsync
在主线程(线程1
)上被调用,那么线程2
也会是主线程,同样线程3
也会是主线程。最终DoSomethingOnGUIThread
将会正确地在主线程上被调用。
1
2
3
4
5
6
7
8
9
10
private async Task DoSomethingAsyncV1()
{
////线程 1 => 主线程
var result1 = await BackgroundTask1; ////等价于ConfigureAwait(true)
////线程 2 => 主线程
var result2 = await BackgroundTask2; ////等价于ConfigureAwait(true)
////线程 3 => 主线程
DoSomethingOnGUIThread(result1, result2);
}
但如果我们希望省去BackgroundTask1
结束后的线程切换而加上ConfigureAwait(false)
,那么代码会有问题——线程2
和线程3
都可能会是后台线程(取决于BackgroundTask1
从开始执行到结束有多快)
通常的解决办法是重构一下,把获取result1
和result2
的过程合并到GetResultAsync()
,在其内部一路ConfigureAwait(false)
到底。下面的代码麻烦了一点(实际上是正确的做法),但可以正常工作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private async Task DoSomethingAsyncV2()
{
////线程 1 => 主线程
var tuple = await GetResultAsync(); ////等价于ConfigureAwait(true)
////线程 2 => 主线程。
DoSomethingOnGUIThread(tuple.Item1, tuple.Item2);
}
private async Task<Tuple<int, int>> GetResultAsync()
{
////线程 1 => 主线程
var result1 = await BackgroundTask1.ConfigureAwait(false);
////线程 2 => 后台线程
var result2 = await BackgroundTask2.ConfigureAwait(false);
////线程 3 => 后台线程
return Tuple.Create(result1, result2);
}
尽管DoSomethingAsyncV2
是正确的做法,但开发者也许意识不到而仍旧使用了原来的调用方式并在BackgroundTask1
后面加上ConfigureAwait(false)
而最终报错。这时候如果使用DispatcherWaiter
则上述重构可以省去并简化如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
private async Task DoSomethingAsyncV3()
{
////线程 1 => 主线程
var result1 = await BackgroundTask1.ConfigureAwait(false);
////线程 2 => 后台线程
var result2 = await BackgroundTask2.ConfigureAwait(false);
////线程 3 => 后台线程
await waiter.WaitAsync();
////线程 4 => 主线程
DoSomethingOnGUIThread(result1, result2);
}
虽然使用DispatcherWaiter
已经算是优雅,但是如果我们能把await waiter.WaitAsync()
那一行省掉,代码会更加简洁——在回到主线程之前无论有多少个ConfigureAwait(false)
,只要在最后一个后台线程调用后面加上ConfigureAwait(DispatcherWaiter)
即可。
1
2
3
4
5
6
7
8
9
10
private async Task DoSomethingAsync()
{
////线程 1 => 主线程
var result1 = await BackgroundTask1.ConfigureAwait(false);
////线程 2 => 后台线程
var result2 = await BackgroundTask2.ConfigureAwait(waiter); ////注意此处参数为waiter
////线程 4 => 主线程
DoSomethingOnGUIThread(result1, result2);
}
2. 分析
由于DispatcherWaiter
的行为基于Dispatcher
,为简化叙述我们仅讨论面向Dispatcher
的实现,即ConfigureAwait(Dispatcher)
,结论很容易可以推广到DispatcherWaiter
。
一般说来实现awaitable
的行为有两种方式:
1) 实现编译器可识别的签名,也就是await
关键字后面的对象需要提供一个GetAwaiter
方法,该方法返回的对象需要至少实现INotifyCompletion
接口,一个布尔型IsCompleted
属性和一个GetResult
方法。如下参考来自await anything。
The languages support awaiting any instance that exposes the right method (either instance method or extension method):
GetAwaiter
. A GetAwaiter needs to implement theINotifyCompletion
interface (and optionally theICriticalNotifyCompletion
interface) and return a type that itself exposes three members:
1
2
3
bool IsCompleted { get; }
void OnCompleted(Action continuation);
TResult GetResult(); // TResult can also be void
打开ConfigureAwait(bool)
的代码可以看到一个ConfiguredTaskAwaitable
的实例被创建,该实例包含的GetAwaiter
方法返回一个ConfiguredTaskAwaiter
的实例,后者实现了ICriticalNotifyCompletion
和INotifyCompletion
,编译器检测到代码满足规定的模式于是生成相应状态机代码来实现awaitable
的行为。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public ConfiguredTaskAwaitable ConfigureAwait(bool continueOnCapturedContext)
{
return new ConfiguredTaskAwaitable(this, continueOnCapturedContext);
}
public struct ConfiguredTaskAwaitable
{
internal ConfiguredTaskAwaitable(Task task, bool continueOnCapturedContext)
{
this.m_configuredTaskAwaiter = new ConfiguredTaskAwaitable.ConfiguredTaskAwaiter(task, continueOnCapturedContext);
}
public ConfiguredTaskAwaitable.ConfiguredTaskAwaiter GetAwaiter()
{
return this.m_configuredTaskAwaiter;
}
public struct ConfiguredTaskAwaiter : ICriticalNotifyCompletion, INotifyCompletion
{
}
}
2) 使用AsyncMethodBuilder
。AsyncMethodBuilder
是高级版的用法,通常用于实现某种类似于Task
的类型以提供自定义的awaitable
行为,比如ValueTask
,Async Task Types in C#中有更多叙述。
A task type is a class or struct with an associated builder type identified with
System.Runtime.CompilerServices.AsyncMethodBuilderAttribute
. The task type may be non-generic, for async methods that do not return a value, or generic, for methods that return a value.
medium.com上有一篇使用AsyncMethodBuilder
来实现本文类似功能的例子,但正如Stephen Toub在dotnet/csharplang的一个proposal中给出的如下概要,目前来说没有办法传递上下文信息给AsyncMethodBuilder
,因为Attribute
只能提供基于类型的静态信息。
1
2
3
4
5
6
7
8
9
10
public struct AsyncCoolTypeMethodBuilder
{
public static AsyncCoolTypeMethodBuilder Create();
......
}
[AsyncMethodBuilder(typeof(AsyncCoolTypeMethodBuilder))]
public struct CoolType { ...... }
public async CoolType SomeMethodAsync() { ...... } // will implicitly use AsyncCoolTypeMethodBuilder.Create()
要求不高的情况下在自定义的Task Type
中可直接访问Application.Current.Dispatcher
,但对于包含多个GUI
线程的应用来说Dispatcher
就变成了必须传递给AsyncMethodBuilder
的上下文。所以AsyncMethodBuilder
虽然高级,但是对于解决本文要解决的问题实际上用处不大,因此在目前的.net
版本下实际上只有#1
华山一条路。
3. 实现
一切搞清楚之后代码实现起来非常简单。注意到非泛型Task
和泛型Task<T>
在行为上唯一的区别是有无返回值,我们把公用的代码提取到DispatchTaskAwaiterHelper
中加以重用,非泛型的DispatchTaskAwaiter
和泛型DispatchTaskAwaiter<T>
分别调用无返回值的GetResult
和有返回值的GetResult<T>
即可。
值得注意的是虽然ICriticalNotifyCompletion
继承自INotifyCompletion
,但.net
默认使用的AsyncMethodBuilder
生成的针对Task
的代码只会调用UnsafeOnCompleted(Action)
,因此无需实现OnCompleted(Action)
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public readonly struct DispatchTaskAwaiter : ICriticalNotifyCompletion
{
private readonly DispatchTaskAwaiterHelper _helper;
public DispatchTaskAwaiter(Task task, Dispatcher dispatcher)
{
_helper = new DispatchTaskAwaiterHelper(task, dispatcher);
}
public DispatchTaskAwaiter GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get { return _helper.IsCompleted; }
}
public void OnCompleted(Action continuation)
{
////This is not called, check out https://devblogs.microsoft.com/pfxteam/whats-new-for-parallelism-in-net-4-5-beta/ for more details
throw new NotImplementedException();
}
public void UnsafeOnCompleted(Action continuation)
{
_helper.UnsafeOnCompleted(continuation);
}
public void GetResult()
{
_helper.GetResult();
}
}
DispatchTaskAwaiterHelper
是真实行为的实现者。其原理简述如下:
-
由于一个
Task
可以多次被await
,因此AsyncMethodBuilder
生成的代码首先会访问IsCompleted
属性来确定是否满足调用UnsafeOnCompleted
的条件。如果IsCompleted==true
则会直接调用GetResult
。所以在IsCompleted
中我们简单地检查原始Task
是否已完成以及是否当前处于GUI
线程。 -
如果
IsCompleted==false
则调用UnsafeOnCompleted
并传入一个continuation
委托作为callback
。对于本例来说只需要在原始Task
结束后,调用continuation
之前通过Dispatcher.BeginInvoke
切换到GUI
线程。 -
异常处理。需要确保原始
Task
中抛出的异常能被调用者的try...catch
代码块捕捉到。容易想到在UnsafeOnCompleted
中判断原始Task
的状态并抛出异常,但因为异常处理的逻辑包含在continuation
中,所以UnsafeOnCompleted
中抛出的异常没法被状态机代码捕获,只能通过挂载TaskScheduler
的UnobservedTaskException
事件处理函数在GC
发生的时候接收到。唯一可以处理异常的地方是GetResult
方法,VerifyException
方法抛出的异常会在GUI
线程被捕捉到,这与ConfigureAwait(true)
的行为一致。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public readonly struct DispatchTaskAwaiterHelper
{
private readonly Task _task;
private readonly Dispatcher _dispatcher;
public DispatchTaskAwaiterHelper(Task task, Dispatcher dispatcher)
{
_task = task;
_dispatcher = dispatcher;
}
public bool IsCompleted
{
get { return _task.IsCompleted && _dispatcher.CheckAccess(); }
}
public void UnsafeOnCompleted(Action continuation)
{
var tmp = this;
_task.ContinueWith(t => tmp._dispatcher.BeginInvoke(continuation));
}
/// <summary>
/// 供非泛型DispatchTaskAwaiter调用
/// </summary>
public void GetResult()
{
VerifyException();
}
/// <summary>
/// 供泛型DispatchTaskAwaiter<T>调用
/// </summary>
public T GetResult<T>()
{
VerifyException();
return ((Task<T>) _task).Result;
}
private void VerifyException()
{
if (!_task.IsCompleted)
{
throw new InvalidOperationException("Task is unexpectedly not completed");
}
if (_task.IsCanceled)
{
throw new TaskCanceledException();
}
if (_task.Exception != null)
{
throw _task.Exception;
}
}
}
4. 使用
实现了DispatchTaskAwaiter
后我们只需要为非泛型Task
和泛型Task<T>
各添加一个扩展方法:
1
2
3
4
5
6
7
8
9
10
11
12
public static class TaskExt
{
public static DispatchTaskAwaiter ConfigureAwait(this Task task, Dispatcher dispatcher)
{
return new DispatchTaskAwaiter(task, dispatcher);
}
public static DispatchTaskAwaiter<T> ConfigureAwait<T>(this Task<T> task, Dispatcher dispatcher)
{
return new DispatchTaskAwaiter<T>(task, dispatcher);
}
}
然后这样使用,极为清爽:
1
2
3
4
5
6
7
8
9
10
11
12
private async Task DoSomethingAsync()
{
////线程 1 => 主线程
var result1 = await BackgroundTask1.ConfigureAwait(false);
////此处_dispatcher为创建ViewModel时保存的GUI线程Dispatcher
////线程 2 => 后台线程
var result2 = await BackgroundTask2.ConfigureAwait(_dispatcher);
////线程 4 => 主线程
DoSomethingOnGUIThread(result1, result2);
}
上述代码扩展为ConfigureAwait(DispatcherWaiter)
极其容易,不再赘述。
本文所有代码请移步github。