.NET 和 C# 共同给我们带来的 async/await 异步编程模型(TAP)用起来真的很爽。为了实现异步等待,我们只需要在一切能够能够异步等待的方法前面加上 await 即可。能够异步等待的最常见的类型莫过于 Task,但也有一些其他类型。即便有些耗时操作没有返回可等待的类型,我们也可以用一句 Task.Run(action) 来包装(同步转异步 - 林德熙 中也有说明);不过副作用就是 Run 里面的方法在后台线程执行了(谁知道这是好处呢还是坏处呢 ^_^)。

问题就在于,有些“耗时”操作根本就无法放入后台线程,典型的莫过于“耗时”的 UI 操作。本文将通过实现一个适用于 UI 的可等待类型来解决这种 UI 的“耗时”等待问题。


本文代码较多,阅读建议:

  1. 标注为“本文推荐的完整代码”的代码块可直接放入自己的项目中使用,也贴出了 GitHub 上我以 MIT 开源的源代码(可能 GitHub 上会经常更新)。
  2. 标注“此处为试验代码”的代码块表明此处代码并不完善,仅用于本文分析使用,不建议放到自己的项目中使用。
  3. 没有注释标注的代码块是用于研究的代码片段,不需要使用。
  4. 可点击下面的导航跳转到你希望的地方。
 

我们的需求

这里说的 UI “耗时”,“耗时”打了引号,是因为严格来说并不是真的卡死了 UI,而是某个函数的执行需要更多的 UI 操作才能继续。这句话可能比较难懂,但举两个例子就好懂了。

  1. 某个函数的执行需要显示一个用户控件,用户填写控件中的信息并确定后,函数才继续执行。这种感觉很像模态窗口,但我们却是在同一个窗口内实现,不能通过模态窗口来实现我们的功能。(UWP 中的 ContentDialog 就是这么干的。)
  2. 我们需要在后台线程创建一个控件,创建完毕之后在原线程返回。这样我们就能得到一个在后台线程创建的控件了。

本文将以实现第 2 条为目标,一步步完善我们的代码,并做出一个非常通用的 UI 可等待类出来。最终你会发现,我们的代码也能轻松应对第 1 条的需求。

什么样的类是可等待的?

我们已经知道 Task 是可等待的,但是去看看 Task 类的实现,几乎找不到哪个基类、接口或者方法属性能够告诉我们与 await 相关。所以,await 的实现可能是隐式的。

幸运的是,Dixin’s Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern 一文解决了我们的疑惑。async/await 是给编译器用的,只要我们的类包含一个 GetAwaiter 方法,并返回合适的对象,我们就能让这个类的实例被 await 使用了。

既然需要一个 GetAwaiter 方法,那我们先随便写个方法探索一下:

Test DoAsync()
{
return new Test();
}
class Test
{
void GetAwaiter()
{
}
}

尝试调用:

await DoAsync();

编译器告诉我们:

Test.GetAwaiter() 不可访问,因为它具有一定的保护级别。

原来 GetAwaiter 方法需要是可以被调用方访问到的才行。

于是我们将 GetAwaiter 前面的访问修饰符改成 public。现在提示变成了:

await 要求类型 Test 包含适当的 GetAwaiter 方法。

考虑到一定要获取到某个对象才可能有用,于是我们返回一个 Test2 对象:

public class Test
{
public Test2 GetAwaiter()
{
return new Test2();
}
} public class Test2
{
}

这时编译器又告诉我们:

Test2 未包含 IsCompleted 的定义。

加上 public bool IsCompleted { get; },编译器又说:

Test2 不实现 INotifyCompletion。

于是我们实现之,编译器又告诉我们:

Test2 未包含 GetResult 的定义。

于是我们加上一个空的 GetResult 方法,现在编译器终于不报错了。

现在我们一开始的 DoAsync 和辅助类型变成了这样:

// 注:此处为试验代码。
private Test DoAsync()
{
return new Test();
} public class Test
{
public Test2 GetAwaiter()
{
return new Test2();
}
} public class Test2 : INotifyCompletion
{
public bool IsCompleted { get; }
public void GetResult() { }
public void OnCompleted(Action continuation) { }
}

总结起来,要想使一个方法可被 await 等待,必须具备以下条件:

  1. 这个方法返回一个类 A 的实例,这个类 A 必须满足后面的条件。
  2. 此类 A 有一个可被访问到的 GetAwaiter 方法(扩展方法也行,这算是黑科技吗?),方法返回类 B 的实例,这个类 B 必须满足后面的条件;
  3. 此类 B 实现 INotifyCompletion 接口,且拥有 bool IsCompleted { get; } 属性、GetResult() 方法、void OnCompleted(Action continuation) 方法。

定义抽象的 Awaiter/Awaitable

这里我们发现一个神奇的现象——明明那些属性和方法都是不可缺少的,却并没有接口来约束它们,而是靠着编译器来约束。

然而作为团队开发者的一员,我们不可能让每一位开发者都去探索一遍编译器究竟希望我们怎么来实现 await,于是我们自己来定义接口。方便我们自己后续再实现自己的可等待类型。

以下接口在 Dixin’s Blog - Understanding C# async / await (2) The Awaitable-Awaiter Pattern 一文中已有原型;但我增加了更通用却更严格的泛型约束,使得这些接口更加通用,且使用者实现的过程中更加不容易出错。

// 此段代码为本文推荐的完整版本。
// 可复制或前往我的 GitHub 页面下载:
// https://github.com/walterlv/sharing-demo/blob/master/src/Walterlv.Core/Threading/AwaiterInterfaces.cs
public interface IAwaitable<out TAwaiter> where TAwaiter : IAwaiter
{
TAwaiter GetAwaiter();
} public interface IAwaitable<out TAwaiter, out TResult> where TAwaiter : IAwaiter<TResult>
{
TAwaiter GetAwaiter();
} public interface IAwaiter : INotifyCompletion
{
bool IsCompleted { get; } void GetResult();
} public interface ICriticalAwaiter : IAwaiter, ICriticalNotifyCompletion
{
} public interface IAwaiter<out TResult> : INotifyCompletion
{
bool IsCompleted { get; } TResult GetResult();
} public interface ICriticalAwaiter<out TResult> : IAwaiter<TResult>, ICriticalNotifyCompletion
{
}

实现目标 DispatcherAsyncOperation

现在,我们来实现我们的目标。

回顾一下,我们希望实现一个方法,要求能够在后台线程创建一个 UI 控件。

不使用自定义的 Awaiter,使用现有的 Task 可以写出如下代码:

// 注:此处为试验代码。
public static class UIDispatcher
{
public static async Task<T> CreateElementAsync<T>()
where T : Visual, new()
{
return await CreateElementAsync(() => new T());
} public static async Task<T> CreateElementAsync<T>([NotNull] Func<T> @new)
where T : Visual
{
if (@new == null)
throw new ArgumentNullException(nameof(@new)); var element = default(T);
Exception exception = null;
var resetEvent = new AutoResetEvent(false);
var thread = new Thread(() =>
{
try
{
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher));
element = @new();
resetEvent.Set();
Dispatcher.Run();
}
catch (Exception ex)
{
exception = ex;
}
})
{
Name = $"{typeof(T).Name}",
IsBackground = true,
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
await Task.Run(() =>
{
resetEvent.WaitOne();
resetEvent.Dispose();
});
if (exception != null)
{
ExceptionDispatchInfo.Capture(exception).Throw();
}
return element;
}
}

说明一下:SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(Dispatcher.CurrentDispatcher)); 这句话是为了确保创建的新 UI 线程里执行的 async/await 代码在 await 异步等待之后能够继续回到此 UI 线程,而不是随便从线程池找一个线程执行。

试一下:

var element = await UIDispatcher.CreateElementAsync<Button>();

确实拿到了后台线程创建的 UI 对象。

然而,注意这一句:

await Task.Run(() =>
{
resetEvent.WaitOne();
resetEvent.Dispose();
});

这里开启了一个新的线程,专门等待后台线程执行到某个关键位置,实在是太浪费。如果我们实现的是本文开头的第一个需求,需要等待用户输入完信息点击确认后才继续,那么这个 WaitOne 则可能会等非常久的时间(取决于用户的心情,啥时候想点确定啥时候才结束)。

线程池里一个线程就这样白白浪费了,可惜!可惜!

于是,我们换自己实现的 Awaiter,节省这个线程的资源。取个名字,既然用于 UI 线程使用,那么就命名为 DispatcherAsyncOperation 好了。我打算让这个类同时实现 IAwaitableIAwaiter 接口,因为我又不会去反复等待,只用一次。

那么开始,既然要去掉 Task.Run,那么我们需要在后台线程真正完成任务的时候自动去执行接下来的任务,而不是在调用线程中去等待。

经过反复修改,我的 DispatcherAsyncOperation 类如下:

// 此段代码为本文推荐的完整版本。
// 可复制或前往我的 GitHub 页面下载:
// https://github.com/walterlv/sharing-demo/blob/master/src/Walterlv.Demo.Sharing/Utils/Threading/DispatcherAsyncOperation.cs
namespace Walterlv.Demo.Utils.Threading
{
public class DispatcherAsyncOperation<T> : DispatcherObject,
IAwaitable<DispatcherAsyncOperation<T>, T>, IAwaiter<T>
{
private DispatcherAsyncOperation()
{
} public DispatcherAsyncOperation<T> GetAwaiter()
{
return this;
} public bool IsCompleted { get; private set; } public T Result { get; private set; } public T GetResult()
{
if (_exception != null)
{
ExceptionDispatchInfo.Capture(_exception).Throw();
}
return Result;
} public DispatcherAsyncOperation<T> ConfigurePriority(DispatcherPriority priority)
{
_priority = priority;
return this;
} public void OnCompleted(Action continuation)
{
if (IsCompleted)
{
continuation?.Invoke();
}
else
{
_continuation += continuation;
}
} private void ReportResult(T result, Exception ex)
{
Result = result;
_exception = ex;
IsCompleted = true;
if (_continuation != null)
{
Dispatcher.InvokeAsync(_continuation, _priority);
}
} private Action _continuation;
private DispatcherPriority _priority = DispatcherPriority.Normal;
private Exception _exception; public static DispatcherAsyncOperation<T> Create([NotNull] out Action<T, Exception> reportResult)
{
var asyncOperation = new DispatcherAsyncOperation<T>();
reportResult = asyncOperation.ReportResult;
return asyncOperation;
}
}
}

解释一下:

  1. Create() 静态方法会返回一个可以等待的 DispatcherAsyncOperation<T> 实例,在写实现代码的地方当然不是用来等的,这个值是用来给外部使用 await 的开发者返回的。但是,它会 out 一个 Action,调用这个 Action,则可以报告操作已经结束。
  2. OnCompleted 方法会在主线程调用的代码结束后立即执行。参数中的 continuation 是对 await 后面代码的一层包装,调用它即可让 await 后面的代码开始执行。但是,我们却并不是立即就能得到后台线程的返回值。于是我们需要等到后台线程执行完毕,调用 ReportResult 方法的时候才执行。
  3. _continuation += continuation; 需要使用 “+=” 是因为这里的 GetAwaiter() 返回的是 this,也就是说,极有可能发生同一个实例被 await 多次的情况,需要将每次后面的任务都执行才行。
  4. _continuation 可能为空,是因为任务执行完毕的时候也没有任何地方 await 了此实例。

在有了新的 DispatcherAsyncOperation 的帮助下,我们的 UIDispatcher 改进成了如下模样:

// 注:此处为试验代码。
public static class UIDispatcher
{
public static DispatcherAsyncOperation<T> CreateElementAsync<T>()
where T : Visual, new()
{
return CreateElementAsync(() => new T());
} public static DispatcherAsyncOperation<T> CreateElementAsync<T>(
Func<T> @new)
where T : Visual
{
var awaitable = DispatcherAsyncOperation<T>.Create(out var reportResult);
var thread = new Thread(() =>
{
try
{
var dispatcher = Dispatcher.CurrentDispatcher;
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext(dispatcher));
var value = @new();
reportResult(value, null);
Dispatcher.Run();
}
catch (Exception ex)
{
reportResult(null, ex);
}
})
{
Name = $"{typeof(T).Name}",
IsBackground = true,
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return awaitable;
}
}

为了让 UIDispatcher 更加通用,我们把后台线程创建 UI 控件的代码移除,现在 UIDispatcher 里面只剩下用于创建一个后台线程运行的 Dispatcher 的方法了。

// 此段代码为本文推荐的完整版本。
// 可复制或前往我的 GitHub 页面下载:
// https://github.com/walterlv/sharing-demo/blob/master/src/Walterlv.Demo.WPF/Utils/Threading/UIDispatcher.cs
namespace Walterlv.Demo
{
public static class UIDispatcher
{
public static DispatcherAsyncOperation<Dispatcher> RunNewAsync([CanBeNull] string name = null)
{
var awaitable = DispatcherAsyncOperation<Dispatcher>.Create(out var reportResult);
var thread = new Thread(() =>
{
try
{
var dispatcher = Dispatcher.CurrentDispatcher;
SynchronizationContext.SetSynchronizationContext(
new DispatcherSynchronizationContext(dispatcher));
reportResult(dispatcher, null);
Dispatcher.Run();
}
catch (Exception ex)
{
reportResult(null, ex);
}
})
{
Name = name ?? "BackgroundUI",
IsBackground = true,
};
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
return awaitable;
}
}
}

回顾完整的代码

至此,我们得到了三个完整的代码文件(在 GitHub 上,以下所有代码文件均有详尽的中文注释):

  • AwaiterInterfaces.cs 用于定义一组完整的 Awaitable/Awaiter 接口,方便开发者实现自定义可等待对象。
  • DispatcherAsyncOperation.cs 一个自定义的,适用于 UI 的自定义可等待(awaitable)类;使用此类可以避免浪费一个线程用于等待 UI 操作的结束。
  • UIDispatcher.cs 用于在后台线程启动一个 Dispatcher,以便在这个 Dispatcher 中方便地创建控件。

回顾需求

现在,在以上三个完整代码文件的帮助下,我们实现我们的那两个需求。(手动斜眼一下,我只说拿第 2 个需求当例子进行分析,并不是说只实现第 2 个。我们的目标是写出一份通用的组件来,方便实现大部分主流需求。)

实现第 2 个需求

后台创建一个 UI 控件:

public async Task<T> CreateElementAsync<T>([CanBeNull] Dispatcher dispatcher = null)
where T : UIElement, new()
{
return await CreateElementAsync(() => new T(), dispatcher);
} public async Task<T> CreateElementAsync<T>(Func<T> @new, [CanBeNull] Dispatcher dispatcher = null)
where T : UIElement
{
dispatcher = dispatcher ?? await UIDispatcher.RunNewAsync($"{typeof(T).Name}");
return await dispatcher.InvokeAsync(@new);
}

可以这样用:

var result = CreateElementAsync(() =>
{
var box = new TextBox()
{
Text = "123",
Opacity = 0.5,
Margin = new Thickness(16),
};
return box;
});

也可以这样用:

var result = CreateElementAsync<Button>();

还可以不用新建线程和 Dispatcher,直接利用现成的:

var result = CreateElementAsync<Button>(dispatcher);

实现第 1 个需求

显示一个用户控件,等用户点击了确定后异步返回:

private Action<bool, Exception> _reportResult;

public DispatcherAsyncOperation<bool> ShowAsync()
{
var awaiter = DispatcherAsyncOperation<bool>.Create(out _reportResult);
Host.Visibility = Visibility.Visible;
return awaiter;
} private void OkButton_Click(object sender, RoutedEventArgs e)
{
Host.Visibility = Visibility.Collapsed;
_reportResult(true, null);
} private void CancelButton_Click(object sender, RoutedEventArgs e)
{
Host.Visibility = Visibility.Collapsed;
_reportResult(false, null);
}

可以这样用:

var result = await someControl.ShowAsync();
if (result)
{
// 用户点了确定。
}
else
{
// 用户点了取消。。
}

全文总结

读者读到此处,应该已经学会了如何自己实现一个自定义的异步等待类,也能明白某些场景下自己写一个这样的类代替原生 Task 的好处。不过不管是否明白,通过阅读本文还收获了三份代码文件呢!我已经把这些文件以 MIT 开源到了 walterlv/sharing-demo 中,大家可以随意使用。

本文较长,如果阅读的过程中发现了任何不正确的地方,希望能回复帮我指出;如果有难以理解的地方,也请回复我,以便我能够调整我的语句,使之更易于理解。

以上。


参考资料

如何实现一个可以用 await 异步等待的 Awaiter的更多相关文章

  1. 实现一个可以用 await 异步等待的 Awaiter

    总结起来,要想使一个方法可被 await 等待,必须具备以下条件: 这个方法返回一个类 A 的实例,这个类 A 必须满足后面的条件.此类 A 有一个可被访问到的 GetAwaiter 方法(扩展方法也 ...

  2. .NET 中什么样的类是可使用 await 异步等待的?

    我们已经知道 Task 是可等待的,但是去看看 Task 类的实现,几乎找不到哪个基类.接口或者方法属性能够告诉我们与 await 相关. 而本文将探索什么样的类是可使用 await 异步等待的? D ...

  3. .NET 编写一个可以异步等待循环中任何一个部分的 Awaiter

    林德熙 小伙伴希望保存一个文件,并且希望如果出错了也要不断地重试.然而我认为如果一直错误则应该对外抛出异常让调用者知道为什么会一直错误. 这似乎是一个矛盾的要求.然而最终我想到了一个办法:让重试一直进 ...

  4. 定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便

    我在几篇文章中都说到了在 .NET 中自己实现 Awaiter 情况.async / await 写异步代码用起来真的很爽,就像写同步一样.然而实现 Awaiter 没有现成的接口,它需要你按照编译器 ...

  5. 将 async/await 异步代码转换为安全的不会死锁的同步代码

    在 async/await 异步模型(即 TAP Task-based Asynchronous Pattern)出现以前,有大量的同步代码存在于代码库中,以至于这些代码全部迁移到 async/awa ...

  6. c# async/await异步编程死锁的问题

    在异步编程中,如果稍有不注意,就会造成死锁问题.何为死锁:即两个以上的线程同时争夺被互相锁住的资源,两个都不放手. 在UI或asp.net中,容易造成死锁的代码如下所示: private void b ...

  7. Async and Await 异步和等待

    [第一次这么耐下性子认真写博客,虽然觉得很认真了,当毕竟是第一次嘛,以后再看肯定觉得很不咋滴的,更何况园子里有那么多的高人和大侠,这篇文章就权当练练手了,熟悉一下用客户端发表博客了,也希望大家多多照顾 ...

  8. Async和Await异步编程的原理

    1. 简介 从4.0版本开始.NET引入并行编程库,用户能够通过这个库快捷的开发并行计算和并行任务处理的程序.在4.5版本中.NET又引入了Async和Await两个新的关键字,在语言层面对并行编程给 ...

  9. async And await异步编程活用基础

    原文:async And await异步编程活用基础 好久没写博客了,时隔5个月,奉上一篇精心准备的文章,希望大家能有所收获,对async 和 await 的理解有更深一层的理解. async 和 a ...

随机推荐

  1. Matlab 日期频次统计

    一.孕妇建档月份频次统计 源数据样本,为某医院一段时间内的孕妇建档时间 2015-04-22 10:12:522014-11-21 17:16:472013-12-16 17:35:442013-12 ...

  2. Codeforces gym 100971 D. Laying Cables 单调栈

    D. Laying Cables time limit per test 2 seconds memory limit per test 256 megabytes input standard in ...

  3. session.资料

    1. HttpSessionListener https://www.cnblogs.com/diewufeixian/p/4221747.html 2. 3. 4. 5.

  4. 你曾后悔进入 IT 行业吗?为什么?(转自知乎)--一生不悔入IT

    你曾后悔进入 IT 行业吗?为什么?(转自知乎)--一生不悔入IT 一.总结 一句话总结:看了大概200条评论,99%的不后悔,大部分人后悔没有早点干,但是做it最最主要的是要注意身体. 1.it是最 ...

  5. HDU 4417 BIT or ST

    Super Mario Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  6. css3 transform matrix矩阵的使用

      Transform 执行顺序问题 — 后写先执行 matrix(a,b,c,d,e,f) 矩阵函数 •通过矩阵实现缩放 x轴缩放 a=x*a    c=x*c     e=x*e; y轴缩放 b= ...

  7. Nginx启动/重启失败

    解决方案: Nginx启动或重启失败,一般是因为配置文件出错了,我们可以使用nginx -t方法查看配置文件出错的地方.也可以通过查看Nginx日志文件定位到Nginx重启失败的原因,Nginx日志文 ...

  8. linux下面安装maven

    maven作为最近比较火的项目管理工具,对项目的jar包及其开元添加相应的插件的管理,很方便. 安装maven: 在官网上面去下载最新的maven的压缩包,apache-maven-3.3.1-bin ...

  9. 初次使用Bootstrap

    一.概述也好,过程也好,反正就是这样 知道这个还是从群里听到的,一个人的视野是有限,必须借助他人来扩展.好奇使我去搜索了解了一下.原来是一个网页前端框架,可以适合不同大小屏幕,自动适应正常显示.这就是 ...

  10. 【Html 学习笔记】第五节——表格

    表格也是日常用到的. 普通表格:<table> 表格边框:border 表头:th 表格标题:caption 横/纵向合并的单元格:colspan .rawspan 表格内标签: 单元格间 ...