C#多线程系列之任务基础(三)
作者:痴者工良 发布时间:2023-10-08 13:43:34
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()
可以;
来源:https://www.cnblogs.com/whuanle/p/12802943.html


猜你喜欢
- 本文实例讲述了C#使用round函数四舍五入的方法。分享给大家供大家参考。具体分析如下:C#中的round函数实际上不是真正的四舍五入函数,
- 一、对Canvas进行操作对Canvas的一系列操作,是指对Canvas进行旋转、平移、缩放等操作。这些操作可以让Canvas对象使用起来更
- 本文实例讲述了java数据结构与算法之冒泡排序。分享给大家供大家参考,具体如下:前面文章讲述的排序算法都是基于插入类的排序,这篇文章开始介绍
- 前言:如果简单地拍照片并非您应用的主要目标,那么您可能希望从相机应用中获取图片并对该图片执行一些操作。一、这就是第一种方法,比较简单,不用将
- 一、默认静态资源路径类路径下:staticpublicresources这几个目录为默认静态资源访问的目录二、增加静态资源路径前缀动态资源和
- 1、JDK:Java Development Kit,java开发工具包。http://www.oracle.com/technetwork
- 1、使用排序2、原理事实上Collections.sort方法底层就是调用的array.sort方法,而且不论是Collections.so
- 一:背景1. 讲故事最近发现 C#7 之后的 is 是越来越看不懂了,乍一看花里胡哨的,不过当我静下心来仔细研读,发现这 is 是越来越短小
- 我的环境* JDK 1.8 * maven 3.6.0 * node环境1.为什么需要前后端项目开发时分离,部署时合并?
- C++的函数指针(function pointer)是通过指向函数的指针间接调用函数。相信很多人对指向一般函数的函数指针使用的比较多,而对指
- 本文实例为大家分享了C#实现窗体抖动的具体代码,供大家参考,具体内容如下原理:围绕中心点运动一圈方法一:通过线程实现需求:需要using S
- idea2019导入maven项目中的某些问题idea2019导入maven项目,会出现很多莫名其妙的问题,需要注意的是如果是idea201
- HttpServletRequest对象代表客户端的请求,当客户端通过HTTP
- JSON.toJSONString()空字段不忽略修改使用JSON.toJSONString(object)方法,返回的json中,默认会将
- ELK环境安装ELK是指Elasticsearch、Kibana、Logstash这三种服务搭建的日志收集系统,具体搭建方式可以参考《Spr
- 前言一说到Socket,想必大家都或多或少有所涉及,从最初的计算机网络课程,讲述了tcp协议,而Socket就是对协议的进一步封装,使我们开
- 上文对数据结构与算法,有了一个简单的概述与介绍,这篇文章,我们介绍一中典型数据结构——线性结构。什么是线性结构,线性结构是最简单、最基本、最
- HttpResponse 讲解HttpServletResponse概述:在创建Servlet时会覆盖service()方法,或doGet(
- 本文实例演示了visual C#下一个类的定义及实现方法,虽然是一个较为基础的C#代码实例,对于新手来说仍然有很好的参考价值。具体的实例代码
- 一般表单数据分为两类<form method="post" action="${pageContext.