您的位置:首页 > 编程语言 > C#

C#多线程(15):任务基础③

2020-04-29 16:20 1161 查看
[TOC] 任务基础一共三篇,本篇是第三篇,之后开始学习异步编程、并发、异步I/O的知识。 本篇会继续讲述 Task 的一些 API 和常用的操作。 ### TaskAwaiter 先说一下 `TaskAwaiter`,`TaskAwaiter` 表示等待异步任务完成的对象并为结果提供参数。 Task 有个 `GetAwaiter()` 方法,会返回`TaskAwaiter` 或`TaskAwaiter`,`TaskAwaiter` 类型在 `System.Runtime.CompilerServices` 命名空间中定义。 `TaskAwaiter` 类型的属性和方法如下: 属性: | 属性 | 说明 | | ----------- | ---------------------------------------- | | IsCompleted | 获取一个值,该值指示异步任务是否已完成。 | 方法: | 方法 | 说明 | | ------------------------- | ----------------------------------------------------------- | | GetResult() | 结束异步任务完成的等待。 | | OnCompleted(Action) | 将操作设置为当 TaskAwaiter 对象停止等待异步任务完成时执行。 | | UnsafeOnCompleted(Action) | 计划与此 awaiter 相关异步任务的延续操作。 | 使用示例如下: ```csharp static void Main() { Task task = new Task(()=> { Console.WriteLine("我是前驱任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); return 666; }); TaskAwaiter 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](https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f) 其使用方法如下: ```csharp static void Main() { Task task = new Task(()=> { Console.WriteLine("我是前驱任务"); Thread.Sleep(TimeSpan.FromSeconds(1)); return 666; }); ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter(); awaiter.OnCompleted(()=> { Console.WriteLine("前驱任务完成时,我就会继续执行"); }); task.Start(); Console.ReadKey(); } ``` `ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ` 拥有跟 `TaskAwaiter` 一样的属性和方法。 `.ContinueWith()` 跟 ` .ConfigureAwait(false)` 还有一个区别就是 前者可以延续多个任务和延续任务的任务(多层)。后者只能延续一层任务(一层可以有多个任务)。 ### 另一种创建任务的方法 前面提到提到过,创建任务的三种方法:`new Task()`、`Task.Run()`、`Task.Factory.SatrtNew()`,现在来学习第四种方法:`TaskCompletionSource` 类型。 我们来看看 `TaskCompletionSource` 类型的属性和方法: 属性: | 属性 | 说明 | | ---- | ------------------------------------------- | | 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` 类可以对任务的生命周期做控制。 首先要通过 `.Task` 属性,获得一个 `Task` 或 `Task` 。 ```csharp TaskCompletionSource task = new TaskCompletionSource(); Task myTask = task.Task; // Task myTask = task.Task; ``` 然后通过 `task.xxx()` 方法来控制 `myTask` 的生命周期,但是呢,myTask 本身是没有任务内容的。 使用示例如下: ```csharp static void Main() { TaskCompletionSource task = new TaskCompletionSource(); Task 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://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/) 这篇文章讲得不错,而且有图:[https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/](https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/) ### 实现一个支持同步和异步任务的类型 这部分内容对 `TaskCompletionSource` 继续进行讲解。 这里我们来设计一个类似 Task 类型的类,支持同步和异步任务。 - 用户可以使用 `GetResult()` 同步获取结果; - 用户可以使用 `RunAsync()` 执行任务,使用 `.Result` 属性异步获取结果; 其实现如下: ```csharp /// /// 实现同步任务和异步任务的类型 /// /// public class MyTaskClass { private readonly TaskCompletionSource source = new TaskCompletionSource(); private Task task; // 保存用户需要执行的任务 private Func _func; // 是否已经执行完成,同步或异步执行都行 private bool isCompleted = false; // 任务执行结果 private TResult _result; /// /// 获取执行结果 /// public TResult Result { get { if (isCompleted) return _result; else return task.Result; } } public MyTaskClass(Func func) { _func = func; task = source.Task; } /// /// 同步方法获取结果 /// /// public TResult GetResult() { _result = _func.Invoke(); isCompleted = true; return _result; } /// /// 异步执行任务 /// public void RunAsync() { Task.Factory.StartNew(() => { source.SetResult(_func.Invoke()); isCompleted = true; }); } } ``` 我们在 Main 方法中,创建任务示例: ```csharp class Program { static void Main() { // 实例化任务类 MyTaskClass myTask1 = new MyTaskClass(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); return "www.whuanle.cn"; }); // 直接同步获取结果 Console.WriteLine(myTask1.GetResult()); // 实例化任务类 MyTaskClass myTask2 = new MyTaskClass(() => { Thread.Sleep(TimeSpan.FromSeconds(1)); return "www.whuanle.cn"; }); // 异步获取结果 myTask2.RunAsync(); Console.WriteLine(myTask2.Result); Console.ReadKey(); } } ``` ### Task.FromCanceled() 微软文档解释:创建 Task,它因指定的取消标记进行的取消操作而完成。 这里笔者抄来了一个[示例](https://stackoverflow.com/questions/25510766/how-to-create-a-cancelled-task): ```csharp var token = new CancellationToken(true); Task task = Task.FromCanceled(token); Task genericTask = Task.FromCanceled(token); ``` 网上很多这样的示例,但是,这个东西到底用来干嘛的?new 就行了? 带着疑问我们来探究一下,来个示例: ```csharp public static Task Test() { CancellationTokenSource source = new CancellationTokenSource(); source.Cancel(); return Task.FromCanceled(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)/](https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/) ### 如何在内部取消任务 之前我们讨论过,使用 `CancellationToken` 取消令牌传递参数,使任务取消。但是都是从外部传递的,这里来实现无需 `CancellationToken` 就能取消任务。 我们可以使用 `CancellationToken` 的 `ThrowIfCancellationRequested()` 方法抛出 `System.OperationCanceledException` 异常,然后终止任务,任务会变成取消状态,不过任务需要先传入一个令牌。 这里笔者来设计一个难一点的东西,一个可以按顺序执行多个任务的类。 示例如下: ```csharp /// /// 能够完成多个任务的异步类型 /// public class MyTaskClass { private List _actions = new List(); private CancellationTokenSource _source = new CancellationTokenSource(); private CancellationTokenSource _sourceBak = new CancellationTokenSource(); private Task _task; /// /// 添加一个任务 /// /// public void AddTask(Action action) { _actions.Add(action); } /// /// 开始执行任务 /// /// 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; } /// /// 取消任务 /// /// public Task Cancel() { _source.Cancel(); // 这里可以省去 _task = Task.FromCanceled(_source.Token); return _task; } } ``` Main 方法中: ```csharp 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](https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield) 网上的示例大多数都是 `foreach` 的,有些同学不理解这个到底是啥意思。笔者这里简单说明一下。 我们也可以这样写一个示例: 这里已经没有 `foreach` 了。 ```csharp private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; private static IEnumerable ForAsync() { int i = 0; while (i < list.Length) { i++; yield return list[i]; } } ``` 但是,同学又问,这个 return 返回的对象 要实现这个 `IEnumerable` 才行嘛?那些文档说到什么迭代器接口什么的,又是什么东西呢? 我们可以先来改一下示例: ```csharp private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; private static IEnumerable ForAsync() { int i = 0; while (i < list.Length) { int num = list[i]; i++; yield return num; } } ``` 你在 Main 方法中调用,看看是不是正常运行? ```csharp static void Main() { foreach (var item in ForAsync()) { Console.WriteLine(item); } Console.ReadKey(); } ``` 这样说明了,`yield return` 返回的对象,并不需要实现 `IEnumerable` 方法。 其实 `yield` 是语法糖关键字,你只要在循环中调用它就行了。 ```csharp static void Main() { foreach (var item in ForAsync()) { Console.WriteLine(item); } Console.ReadKey(); } private static IEnumerable ForAsync() { int i = 0; while (i < 100) { i++; yield return i; } } } ``` 它会自动生成 `IEnumerable` ,而不需要你先实现 `IEnumerable` 。 ### 补充知识点 - 线程同步有多种方法:临界区(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 篇,后面会继续深入多线程和任务的更多使用方法和场景。 喜欢我的作者记得关注我哟~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: