C#中的 Awaiter
1、背景知识
(1)接口 INotifyCompletion
public interface INotifyCompletion { void OnCompleted(Action continuation); }
实现这个接口的实例(例如一个 Task 对象),在任务完成后,调用 OnCompleted 方法,以执行后续的委托(continuation)。
(2)关键字 await
关键字 await 是一个运算符,它的操作数是一个实现了 GetAwaiter 方法的实例。该方法返回一个 Awaiter。
internal partial class Program { //调用方线程 private static async void ToTest6() { await new Test6(); } } internal class Test6 { public MyAwaiter GetAwaiter() { return new MyAwaiter(); } }
2、Awaiter
要实现一个 Awaiter,必须实现 INotifyCompletion 接口、IsCompleted 属性和 GetResult 方法:
public class MyAwaiter : INotifyCompletion { public bool IsCompleted { get; set; } public void GetResult() { //throw new NotImplementedException(); } public void OnCompleted(Action continuation) { //throw new NotImplementedException(); } }
3、执行流程
(1)先实现一个方法,用于输出当前线程Id,以及调用时给的标记
internal partial class Test6 { public static void PrintThreadInfo(string callTag) { //输出当前线程Id,以及调用时给的标记 Console.WriteLine($" Thread Id:{Environment.CurrentManagedThreadId,2} - {callTag}"); } }
(2)测试代码
在 MyAwaier 中的重要位置都输出调用时给的标记:
public class MyAwaiter : INotifyCompletion { private bool _isCompleted = true; public bool IsCompleted { get { Test6.PrintThreadInfo($"Get IsCompleted:{_isCompleted}"); return _isCompleted; } set { Test6.PrintThreadInfo($"Set IsCompleted:{value}"); _isCompleted = value; } } public void GetResult() { Test6.PrintThreadInfo("Get Result"); } public void OnCompleted(Action continuation) { Test6.PrintThreadInfo("On Completed"); } }
实现一个可作为 await 运算符的操作数的类:
internal partial class Test6 { public MyAwaiter GetAwaiter() { PrintThreadInfo("Get a awaiter"); return new MyAwaiter(); } }
对它测试调用:
internal partial class Program { static void Main(string[] args) { Test6.PrintThreadInfo("Main"); Task.Run(() => { ToTest6(); }); Console.ReadKey(); } private static async void ToTest6() { Test6.PrintThreadInfo("before await"); await new Test6(); Test6.PrintThreadInfo("after await"); } }
运行结果:
结果分析:貌似一切正常,除了 OnCompleted 方法未被调用外。
A、将 _isCompleted 设置为 false,即:private bool _isCompleted = false;
运行结果:
结果分析:未见“Get Result”和“after await”,即:Awaiter 的 GetResult 方法 以及 调用方 await 之后的代码未被执行——线程被挂起了【异常】。
B、修改 INotifyCompletion 接口的实现,添加对参数 continuation 进行是否为 null 的判断,如果不为 null,则调用:
public void OnCompleted(Action continuation) { Test6.PrintThreadInfo("On Completed"); if (continuation != null) { Test6.PrintThreadInfo($"continuation is not null"); continuation.Invoke(); } }
运行结果:
结果分析:
- 对 continuation 的调用,可以使 Awaiter 的 GetResult 方法 以及 调用方 await 之后的代码被执行;
- 但并未见“Set IsCompleted”,可见在代码全部跑完之后,IsCompleted 属性仍然为 false【应设置为 true】;
- 结合之前“_isCompleted 为 true 时的测试结果”,可知,在 IsCompleted 为 true 时,GetResult 方法 以及 调用方 await 之后的代码会立即被调用。
C、再次修改 INotifyCompletion 接口的实现,执行任务之后,将 IsCompleted 属性设置为 true,并调用参数 continuation:
public void OnCompleted(Action continuation) { Test6.PrintThreadInfo("On Completed"); //if (continuation != null) //{ // Test6.PrintThreadInfo($"continuation is not null"); // continuation.Invoke(); //} Task.Run(() => { Test6.PrintThreadInfo("Task Run"); Thread.Sleep(1000); }).ContinueWith((a) => { IsCompleted = true; continuation.Invoke(); }); }
运行结果:
结果分析:Id 为 9 的线程果然被挂起,而由新线程 6 继续之后的代码,看起来一切正常。
(3)测试结论
根据上述测试代码,可以初步得出结论:
- Awaiter 的 GetResult 方法 以及 调用方 await 之后的代码,被自动塞进了委托 continuation 中;
- Awaiter 初始状态 IsCompleted 属性为 true 时,GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码 立即被执行;
- Awaiter 初始状态 IsCompleted 属性为 false 时,须在异步任务结束后,将 IsCompleted 属性设置为 true,并调用委托 continuation,否则线程将被挂起。
测试代码的执行顺序如下:
- 执行调用方 await 之前的代码;
- 执行 GetAwaiter 方法;
- 读取属性 IsCompleted;
- 如果为 true,则执行 GetResult 方法;
- 执行调用方 await 之后的代码;
- 如果为 false,则执行 OnCompleted 方法;
- 挂起线程,等待任务完成
- 任务完成之后,执行 GetResult 方法;
- 执行调用方 await 之后的代码。
- 如果为 true,则执行 GetResult 方法;
4、改进代码
目的:使 Test6 成为一个类似任务(Task-Like)的类。
(1)根据执行流程测试代码的结论,对测试代码做如下改进:
- 合并 Test6 与 MyAwaier,使它们成为一个类;
- 添加一个参数为 Task 的构造函数,使 Awaiter 可以执行指定的任务;
构造函数的参数也可以是委托或方法。
这根据需要来,哪个方便就按哪个来,此处使用任务、方法作为构造函数的参数; - 添加一个工作任务(Work 方法);
- 修改 GetResult 方法,让它返回一个整数。
internal partial class Test6 : INotifyCompletion { private readonly Func<int> _func = null!; public Test6(Func<int> func) { _func = func; } public Test6 GetAwaiter() { PrintThreadInfo("Get a awaiter"); return this; } private bool _isCompleted = false; public bool IsCompleted { get { PrintThreadInfo($"Get IsCompleted:{_isCompleted}"); return _isCompleted; } set { PrintThreadInfo($"Set IsCompleted:{value}"); _isCompleted = value; } } private int _result = 0; public int GetResult() { PrintThreadInfo("Get Result"); return _result; } public void OnCompleted(Action continuation) { PrintThreadInfo("On Completed"); ThreadPool.QueueUserWorkItem(state => { PrintThreadInfo($"Task Run at {DateTime.Now}"); _result = _func.Invoke(); IsCompleted = true; continuation.Invoke(); }); } public static void PrintThreadInfo(string callTag) { //输出当前线程Id,以及调用时给的标记 Console.WriteLine($" Thread Id:{Environment.CurrentManagedThreadId,2} - {callTag}"); } }
调用并输出结果:
internal partial class Program { static void Main(string[] args) { Test6.PrintThreadInfo("Main"); Task.Run(() => { ToTest6(); }); Console.ReadKey(); } private static async void ToTest6() { Test6.PrintThreadInfo("before await"); int awaitResult = await new Test6(Work); Test6.PrintThreadInfo($"after await:{awaitResult}"); } private static int Work() { Thread.Sleep(2000); Test6.PrintThreadInfo($"Work finished at {DateTime.Now}"); return 1; } }
运行结果:
结果分析:Id 为 6 的线程被挂起,而由新线程 10 继续之后的代码,看起来一切正常。
(2)使用泛型改进,并可异步启动(具有 Start 方法):
public interface IAwaiter<out T> : INotifyCompletion { bool IsCompleted { get; } T GetResult(); } public interface IAwaitable<out T> { IAwaiter<T> GetAwaiter(); } internal class Test6<T> : IAwaitable<T>, IAwaiter<T> { private readonly Task<T> _task = null!; public Test6(Task<T> task) { _task = task; } public Test6(Func<T> func) { _task = new Task<T>(func); } public void Start() { _task.ContinueWith(t => { _isCompleted = true; _result = _task.Result; }); _task.Start(); } public IAwaiter<T> GetAwaiter() { TestHelper.PrintThreadInfo("Get a awaiter"); return this; } private bool _isCompleted = false; public bool IsCompleted { get { TestHelper.PrintThreadInfo($"Get IsCompleted:{_isCompleted}"); return _isCompleted; } set { TestHelper.PrintThreadInfo($"Set IsCompleted:{value}"); _isCompleted = value; } } private T _result = default!; public T GetResult() { TestHelper.PrintThreadInfo("Get Result"); return _result; } public void OnCompleted(Action continuation) { TestHelper.PrintThreadInfo("On Completed"); if (!_isCompleted) { if (_task.Status == TaskStatus.Created) { Start(); } _task.Wait(); } continuation.Invoke(); } }
注:将 PrintThreadInfo 方法移到了 TestHelper 类中。
调用并输出结果:
internal partial class Program { static void Main(string[] args) { TestHelper.PrintThreadInfo("Main"); Task.Run(() => { ToTest6(); }); Console.ReadKey(); } //调用方线程 private static async void ToTest6() { var test1 = new Test6<string>(Work1); var test2 = new Test6<int>(Work2); test1.Start(); //test2.Start(); TestHelper.PrintThreadInfo("before await1"); string awaitResult1 = await test1; TestHelper.PrintThreadInfo($"after await1:{awaitResult1}"); TestHelper.PrintThreadInfo("before await2"); int awaitResult2 = await test2; TestHelper.PrintThreadInfo($"after await2:{awaitResult2}"); } private static string Work1() { TestHelper.PrintThreadInfo($"Work1 started at {DateTime.Now}"); Thread.Sleep(2000); return $"Work finished at {DateTime.Now}"; } private static int Work2() { TestHelper.PrintThreadInfo($"Work2 started at {DateTime.Now}"); return 1; } } public static class TestHelper { public static void PrintThreadInfo(string callTag) { //输出当前线程Id,以及调用时给的标记 Console.WriteLine($" Thread Id:{Environment.CurrentManagedThreadId,2} - {callTag}"); } }
运行结果:
结果分析:
- Id 为 6 的线程执行到第一个 await 的 OnCompleted 方法时被挂起;
- test1 的任务 Work1 由新线程 9 完成任务;
- Work1 完成后,线程 6 继续之后的代码,直到下一个await (的 OnCompleted 方法)时被挂起。
- 线程池中线程 9 复用,用于执行 test2 的任务 Work2;
- Work2 完成后,线程 6 继续之后的代码。
至此,一个简单的具有泛型能力的类似任务(Task-Like)的类 Test6 实现完成。
5、总结
(1)要实现一个 Awaiter,必须实现 INotifyCompletion 接口、IsCompleted 属性和 GetResult 方法。
(2)关键字 await 是一个运算符,它的操作数是一个实现了 GetAwaiter 方法的实例。
(3)Awaiter 的 GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码,被自动塞进了一个委托中(continuation)。
(4)Awaiter 的 IsCompleted 属性为 true 时,“GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码” 会立即被执行,并不执行 OnCompleted 方法;
Awaiter 的 IsCompleted 属性为 false 时,立即执行 OnCompleted 方法,并且通过委托(continuation)的调用,来执行“GetResult 方法 以及 调用方 await 之后(下一个 await 之前)的代码”;
如果不调用委托(continuation)将导致线程被挂起,程序无法进行下去,而成为一个 BUG。