C#多线程 任务基础(三)
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 界面程序上特别有用。
可以参考:https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f
其使用方法如下:
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()是很不错的。
这里有很多示例可以参考:https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/
如何在内部取消任务
之前我们讨论过,使用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#多线程系列之任务基础(三)的文章就介绍到这了。希望对大家的学习有所帮助,也希望大家多多支持编程宝库。
最近在测试一个第三方API,准备集成在我们的网站应用中。API的调用使用的是.NET中的HttpClient,由于这个API会在关键业务中用到,对调用API的整体响应速度有严格要求,所以对 ...