DispatcherTaskWaiter

ConfigureAwait(Dispatcher)

Posted by eagleboost on May 31, 2020

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 if await SomeTask is called on GUI thread, after SomeTask is completed, execution would continue on GUI 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 the INotifyCompletion interface (and optionally the ICriticalNotifyCompletion 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:

  1. Since a Task can be awaited multiple times, so the generated codes would access the awaiter’s IsCompleted property first to determine whether it needs to call UnsafeOnCompleted or not. If IsCompleted==true it directly jump to call GetResult. In the IsCompleted all we need to do is to check if the original task is completed and if we’re on currently the GUI Thread.

  2. If IsCompleted==false then UnsafeOnCompleted would be called with a continuation delegate passed in as callback. In our case once the Task is complete, before calling continuation, we call Dispatcher.BeginInvoke to marshal back to the GUI Thread.

  3. Exception handling. The codes need to make sure exceptions thrown by the original Task can be captured by the try...catch block in caller codes. One easy to think of approach is to throw in the UnsafeOnCompleted when the original Task has exceptions. But in fact because the exception handling logic is inside of the continuation, exceptions thrown in UnsafeOnCompleted won’t be captured and will become visible only to the event listener of TaskScheduler.UnobservedTaskException when GC happens. GetResult method is the only place can be used to handle the exception, exceptions thrown by VerifyException() would be captured by the caller on GUI Thread, which matches the same behavior of 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>
  /// 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从开始执行到结束有多快)

通常的解决办法是重构一下,把获取result1result2的过程合并到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 the INotifyCompletion interface (and optionally the ICriticalNotifyCompletion 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的实例,后者实现了ICriticalNotifyCompletionINotifyCompletion,编译器检测到代码满足规定的模式于是生成相应状态机代码来实现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) 使用AsyncMethodBuilderAsyncMethodBuilder是高级版的用法,通常用于实现某种类似于Task的类型以提供自定义的awaitable行为,比如ValueTaskAsync 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 Toubdotnet/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是真实行为的实现者。其原理简述如下:

  1. 由于一个Task可以多次被await,因此AsyncMethodBuilder生成的代码首先会访问IsCompleted属性来确定是否满足调用UnsafeOnCompleted的条件。如果IsCompleted==true则会直接调用GetResult。所以在IsCompleted中我们简单地检查原始Task是否已完成以及是否当前处于GUI线程。

  2. 如果IsCompleted==false则调用UnsafeOnCompleted并传入一个continuation委托作为callback。对于本例来说只需要在原始Task结束后,调用continuation之前通过Dispatcher.BeginInvoke切换到GUI线程。

  3. 异常处理。需要确保原始Task中抛出的异常能被调用者的try...catch代码块捕捉到。容易想到在UnsafeOnCompleted中判断原始Task的状态并抛出异常,但因为异常处理的逻辑包含在continuation中,所以UnsafeOnCompleted中抛出的异常没法被状态机代码捕获,只能通过挂载TaskSchedulerUnobservedTaskException事件处理函数在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

参考资料