TaskAwaiter
先说一下 TaskAwaiter
,TaskAwaiter
表示等待异步任务完成的对象并为结果提供参数。
Task 有个 GetAwaiter()
方法,会返回TaskAwaiter
或TaskAwaiter<TResult>
,TaskAwaiter
类型在 System.Runtime.CompilerServices
命名空间中定义。
TaskAwaiter
类型的属性和方法如下:
属性:
属性 | 说明 |
IsCompleted | 获取一个值,该值指示异步任务是否已完成。 |
方法:
方法 | 说明 |
GetResult() | 结束异步任务完成的等待。 |
OnCompleted(Action) | 将操作设置为当 TaskAwaiter 对象停止等待异步任务完成时执行。 |
UnsafeOnCompleted(Action) | 计划与此 awaiter 相关异步任务的延续操作。 |
使用示例如下:
static void Main() { Task<int> task = new Task<int>(()=> { Console.WriteLine("我是前驱任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); return 666; }); TaskAwaiter<int> awaiter = task.GetAwaiter(); awaiter.OnCompleted(()=> { Console.WriteLine("前驱任务完成时,我就会继续执行"); }); task.Start(); Console.ReadKey(); }
另外,我们前面提到过,任务发生未经处理的异常,任务被终止,也算完成任务。
延续的另一种方法
上一节我们介绍了 .ContinueWith()
方法来实现延续,这里我们介绍另一个延续方法 .ConfigureAwait()
。
.ConfigureAwait()
如果要尝试将延续任务封送回原始上下文,则为 true
;否则为 false
。
我来解释一下, .ContinueWith()
延续的任务,当前驱任务完成后,延续任务会继续在此线程上继续执行。这种方式是同步的,前者和后者连续在一个线程上运行。
.ConfigureAwait(false)
方法可以实现异步,前驱方法完成后,可以不理会后续任务,而且后续任务可以在任意一个线程上运行。这个特性在 UI 界面程序上特别有用。
其使用方法如下:
static void Main() { Task<int> task = new Task<int>(()=> { Console.WriteLine("我是前驱任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); return 666; }); ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter(); awaiter.OnCompleted(()=> { Console.WriteLine("前驱任务完成时,我就会继续执行"); }); task.Start(); Console.ReadKey(); }
ConfiguredTaskAwaitable<int>.ConfiguredTaskAwaiter
拥有跟 TaskAwaiter
一样的属性和方法。
.ContinueWith()
跟 .ConfigureAwait(false)
还有一个区别就是 前者可以延续多个任务和延续任务的任务(多层)。后者只能延续一层任务(一层可以有多个任务)。
另一种创建任务的方法
前面提到提到过,创建任务的三种方法:new Task()
、Task.Run()
、Task.Factory.SatrtNew()
,现在来学习第四种方法:TaskCompletionSource<TResult>
类型。
我们来看看 TaskCompletionSource<TResulr>
类型的属性和方法:
属性:
属性 | 说明 |
Task | 获取由此 Task 创建的 TaskCompletionSource。 |
方法:
方法 | 说明 |
SetCanceled() | 将基础 Task 转换为 Canceled 状态。 |
SetException(Exception) | 将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。 |
SetException(IEnumerable) | 将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。 |
SetResult(TResult) | 将基础 Task 转换为 RanToCompletion 状态。 |
TrySetCanceled() | 尝试将基础 Task 转换为 Canceled 状态。 |
TrySetCanceled(CancellationToken) | 尝试将基础 Task 转换为 Canceled 状态并启用要存储在取消的任务中的取消标记。 |
TrySetException(Exception) | 尝试将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。 |
TrySetException(IEnumerable) | 尝试将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。 |
TrySetResult(TResult) | 尝试将基础 Task 转换为 RanToCompletion 状态。 |
TaskCompletionSource<TResulr>
类可以对任务的生命周期做控制。
首先要通过 .Task
属性,获得一个 Task
或 Task<TResult>
。
TaskCompletionSource<int> task = new TaskCompletionSource<int>(); Task<int> myTask = task.Task; // Task myTask = task.Task;
然后通过 task.xxx()
方法来控制 myTask
的生命周期,但是呢,myTask 本身是没有任务内容的。
使用示例如下:
static void Main() { TaskCompletionSource<int> task = new TaskCompletionSource<int>(); Task<int> myTask = task.Task; // task 控制 myTask // 新开一个任务做实验 Task mainTask = new Task(() => { Console.WriteLine("我可以控制 myTask 任务"); Console.WriteLine("按下任意键,我让 myTask 任务立即完成"); Console.ReadKey(); task.SetResult(666); }); mainTask.Start(); Console.WriteLine("开始等待 myTask 返回结果"); Console.WriteLine(myTask.Result); Console.WriteLine("结束"); Console.ReadKey(); }
其它例如 SetException(Exception)
等方法,可以自行探索,这里就不再赘述。
参考资料:https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/
这篇文章讲得不错,而且有图:https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/
实现一个支持同步和异步任务的类型
这部分内容对 TaskCompletionSource<TResult>
继续进行讲解。
这里我们来设计一个类似 Task 类型的类,支持同步和异步任务。
- 用户可以使用
GetResult()
同步获取结果; - 用户可以使用
RunAsync()
执行任务,使用.Result
属性异步获取结果;
其实现如下:
/// <summary> /// 实现同步任务和异步任务的类型 /// </summary> /// <typeparam name="TResult"></typeparam> public class MyTaskClass<TResult> { private readonly TaskCompletionSource<TResult> source = new TaskCompletionSource<TResult>(); private Task<TResult> task; // 保存用户需要执行的任务 private Func<TResult> _func; // 是否已经执行完成,同步或异步执行都行 private bool isCompleted = false; // 任务执行结果 private TResult _result; /// <summary> /// 获取执行结果 /// </summary> public TResult Result { get { if (isCompleted) return _result; else return task.Result; } } public MyTaskClass(Func<TResult> func) { _func = func; task = source.Task; } /// <summary> /// 同步方法获取结果 /// </summary> /// <returns></returns> public TResult GetResult() { _result = _func.Invoke(); isCompleted = true; return _result; } /// <summary> /// 异步执行任务 /// </summary> public void RunAsync() { Task.Factory.StartNew(() => { source.SetResult(_func.Invoke()); isCompleted = true; }); } }
我们在 Main 方法中,创建任务示例:
class Program { static void Main() { // 实例化任务类 MyTaskClass<string> myTask1 = new MyTaskClass<string>(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); return "www.whuanle.cn"; }); // 直接同步获取结果 Console.WriteLine(myTask1.GetResult()); // 实例化任务类 MyTaskClass<string> myTask2 = new MyTaskClass<string>(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); return "www.whuanle.cn"; }); // 异步获取结果 myTask2.RunAsync(); Console.WriteLine(myTask2.Result); Console.ReadKey(); } }
Task.FromCanceled()
微软文档解释:创建 Task,它因指定的取消标记进行的取消操作而完成。
这里笔者抄来了一个示例:
var token = new CancellationToken(true); Task task = Task.FromCanceled(token); Task<int> genericTask = Task.FromCanceled<int>(token);
网上很多这样的示例,但是,这个东西到底用来干嘛的?new 就行了?
带着疑问我们来探究一下,来个示例:
public static Task Test() { CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); return Task.FromCanceled<object>(source.Token); } static void Main() { var t = Test(); // 在此设置断点,监控变量 Console.WriteLine(t.IsCanceled); }
Task.FromCanceled()
可以构造一个被取消的任务。我找了很久,没有找到很好的示例,如果一个任务在开始前就被取消,那么使用 Task.FromCanceled()
是很不错的。
如何在内部取消任务
之前我们讨论过,使用 CancellationToken
取消令牌传递参数,使任务取消。但是都是从外部传递的,这里来实现无需 CancellationToken
就能取消任务。
我们可以使用 CancellationToken
的 ThrowIfCancellationRequested()
方法抛出 System.OperationCanceledException
异常,然后终止任务,任务会变成取消状态,不过任务需要先传入一个令牌。
这里笔者来设计一个难一点的东西,一个可以按顺序执行多个任务的类。
示例如下:
/// <summary> /// 能够完成多个任务的异步类型 /// </summary> public class MyTaskClass { private List<Action> _actions = new List<Action>(); private CancellationTokenSource _source = new CancellationTokenSource(); private CancellationTokenSource _sourceBak = new CancellationTokenSource(); private Task _task; /// <summary> /// 添加一个任务 /// </summary> /// <param name="action"></param> public void AddTask(Action action) { _actions.Add(action); } /// <summary> /// 开始执行任务 /// </summary> /// <returns></returns> public Task StartAsync() { // _ = new Task() 对本示例无效 _task = Task.Factory.StartNew(() => { for (int i = 0; i < _actions.Count; i++) { int tmp = i; Console.WriteLine($"第 {tmp} 个任务"); if (_source.Token.IsCancellationRequested) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("任务已经被取消"); Console.ForegroundColor = ConsoleColor.White; _sourceBak.Cancel(); _sourceBak.Token.ThrowIfCancellationRequested(); } _actions[tmp].Invoke(); } },_sourceBak.Token); return _task; } /// <summary> /// 取消任务 /// </summary> /// <returns></returns> public Task Cancel() { _source.Cancel(); // 这里可以省去 _task = Task.FromCanceled<object>(_source.Token); return _task; } }
Main 方法中:
static void Main() { // 实例化任务类 MyTaskClass myTask = new MyTaskClass(); for (int i = 0; i < 10; i++) { int tmp = i; myTask.AddTask(() => { Console.WriteLine(" 任务 1 Start"); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine(" 任务 1 End"); Thread.Sleep(TimeSpan.FromSeconds(1)); }); } // 相当于 Task.WhenAll() Task task = myTask.StartAsync(); Thread.Sleep(TimeSpan.FromSeconds(1)); Console.WriteLine($"任务是否被取消:{task.IsCanceled}"); // 取消任务 Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("按下任意键可以取消任务"); Console.ForegroundColor = ConsoleColor.White; Console.ReadKey(); var t = myTask.Cancel(); // 取消任务 Thread.Sleep(TimeSpan.FromSeconds(2)); Console.WriteLine($"任务是否被取消:【{task.IsCanceled}】"); Console.ReadKey(); }
你可以在任一阶段取消任务。
Yield 关键字
迭代器关键字,使得数据不需要一次性返回,可以在需要的时候一条条迭代,这个也相当于异步。
迭代器方法运行到 yield return
语句时,会返回一个 expression
,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。
可以使用 yield break
语句来终止迭代。
官方文档:https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield
网上的示例大多数都是 foreach
的,有些同学不理解这个到底是啥意思。笔者这里简单说明一下。
我们也可以这样写一个示例:
这里已经没有 foreach
了。
private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; private static IEnumerable<int> ForAsync() { int i = 0; while (i < list.Length) { i++; yield return list[i]; } }
但是,同学又问,这个 return 返回的对象 要实现这个 IEnumerable<T>
才行嘛?那些文档说到什么迭代器接口什么的,又是什么东西呢?
我们可以先来改一下示例:
private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; private static IEnumerable<int> ForAsync() { int i = 0; while (i < list.Length) { int num = list[i]; i++; yield return num; } }
你在 Main 方法中调用,看看是不是正常运行?
static void Main() { foreach (var item in ForAsync()) { Console.WriteLine(item); } Console.ReadKey(); }
这样说明了,yield return
返回的对象,并不需要实现 IEnumerable<int>
方法。
其实 yield
是语法糖关键字,你只要在循环中调用它就行了。
static void Main() { foreach (var item in ForAsync()) { Console.WriteLine(item); } Console.ReadKey(); } private static IEnumerable<int> ForAsync() { int i = 0; while (i < 100) { i++; yield return i; } } }
它会自动生成 IEnumerable<T>
,而不需要你先实现 IEnumerable<T>
。
补充知识点
- 线程同步有多种方法:临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphores)、事件(Event)、任务(Task);
Task.Run()
和Task.Factory.StartNew()
封装了 Task;Task.Run()
是Task.Factory.StartNew()
的简化形式;- 有些地方
net Task()
是无效的;但是Task.Run()
和Task.Factory.StartNew()
可以;
本篇是任务基础的终结篇,至此 C# 多线程系列,一共完成了 15 篇,后面会继续深入多线程和任务的更多使用方法和场景。