C# 异步编程学习 十六:Value Task
1.Value Task 用于微优化场景,您可能永远不需要编写返回此类的方法。
2. Task 和 Task 是引用类型, 实例化他们需要基于堆的内存分配和后续的搜集。
3. 优化的一种极端形式是编写无需分配此类内存的代码。 换句话说,这不会实例化任何应用类型, 不会给垃圾回收集增加负担。
4. 为了支持这种模式,C# 引入了Value Task 和Value Task 这两个struct ,编译器准许他们代替Task 和Task.
5. 如果同步完成,则await value Task 是无分配的。int answer= await foot();
6. 如果操作不是同步完成的, valueTask 实际上就会创建一个普通的Task ,并将await 转发给他。
7. 使用 AsTask方法, 可以把 ValueTask 转化为Task 也包括非泛型版本。
使用Value Task 时的注意事项
- ValueTask 并不常见,它的出现纯粹为了性能。
- 这意味着它被不恰当的值类型语义所困扰,这可能导致意外,为了避免这种情况,必须避免以下情况:
多次await 同一个ValueTask. 操作没有结束就调用 .GetAwait().GetResult(); - 如果你需要这些操作,那么就应该先调用AsTask 返回操作它的Task。避免上述陷阱最简单的办法就是直接用await方法调用。 await foo();
- 将ValueTask 赋给变量时,就可能引发错误。 ValueTask valueTask=Foo();
- 将其立即转化为普通的Task 就可以避免此类错误的发生。Task task=Foo().AsTask();
避免过度弹回
-
对于循环中多次调用的方法,通过调用ConfigureAwait 方法,就可以避免重复的弹回到UI消息循环 带来的开销。
-
这强迫Task 不把continuation弹回给上下文。从而将开销消减接近上下文切换的成本,如果你用await方法同步完成,则开销会小的多。
async void A() {。。。。await B();。。。。}async Task B()
{
for(int i=0;i<1000;i++)
{
await C();
}
}async Task C() {…}
这意味着对于方法B和C, 我们取消了UI程序中的简单线程安全模型,即代码在UI线程上运行,并且只能在await 语句期间被抢占,但是,方法A不受影响,如果在一个UI线程上启动,他将保留在UI线程上
这种优化在编写库时特别重要,您不需要简化线程安全性带来的好处,因为您的代码通常不与调用方法共享状态,也不访问UI控件。
取消
使用取消标志来实现对并发进行取消,可以封装一个类:
class CancellationToken
{
public bool IsCancellationRequested { get;private set; }
public void Cancel()
{
this.IsCancellationRequested = true;
}
public void ThrowIfCancellationRequested()
{
if (this.IsCancellationRequested)
{
throw new OperationCanceledException();
}
}
}
CancellationToken 和CancellationTokenSource 两个预定义类
- 先不管线程安全,应该在读写IsCancellationRequested 是进行 lock ,这个模式非常有效,CRL也提供了CancellationToken 类, 它的功能和前面的例子类似。
- 但是他缺少的一个Cancel方法,Cancel方法是在另外一个类上进行暴露, CancellationTokenSource。
- 这种 分离的设计是出于安全考虑,只能对CancellationToken 访问的方法可以检查取消,但是不能实例化取消。
Delay方法
- CLR里大部分的异步方法都支持Cancellationtoken, 包括Delay方法
async Task Foo(CancellationToken token)
{
for (int i = 0; i < 10000; i++)
{
Console.WriteLine(i);
await Task.Delay(1000, token);
}
} - 这个时候,task 在遇到请求时会立即停止(而不是1s之后才停止)
- 这里边我们无需调用 ThrowIfCancellationRequested, 因为Delay 会替我们做。
- 取消标记在调用栈中能很好地向下传播(就像是因为异常,取消请求在调用栈中向上级联一样)。
同步方法
- 同步方法也支持取消(例如Task的Wait方法)。 这种情况下,取消指令需要异步发出(例如: 来自另外一个Task)
Task.Delay(1000).ContinueWith(_ => { cancelSource.Cancel(); });
其他
- 事实上,您可以在构造CancellationTokenSource 时指定一个时间间隔,以便在一段时间后取消,他对实现超时是非常有用的,无论是同步还是异步。
- CancellationToken 这个struct提供一个Register 方法,他可以让你注册一个回调委托,这个委托会在取消时触发。 他返回一个对象,这个对象在取消注册时可以被Dispose掉。
- 编译器的异步函数生成的Task在遇到OperationCanceledException 异常时会自动进入取消状态(IsCanceled返回true, IsFaulted返回false)
- 使用Task.Run创建Task也是如此,这里是指向 构造函数传递
进度报告
-
有时候,你希望异步方法在运行过程中能实现一些反馈,一个简单地办法就是传入一个Action委托,当进度发生变化时候触发方法调用:
public Task PrintNumberFoo(UInt32 maxNu, Action OperationChangedAct)
{
return Task.Run(() =>
{
for (int i = 0; i <= maxNu; i++)
{
if (i % 10 == 0)
{
OperationChangedAct(i / 10);
}
//do something
Console.WriteLine(i);
//Task.Delay(10000);
Thread.Sleep(1000);
}}); }
注意:在富客户端应用中,从一个work线程中,取执行UI线程中的方法可能会导致线程安全问题。
IProgress 和Progress
-
CLR提供一个对类型来解决此类问题。
-
使用Iprogress
public Task PrintNumberFoo(UInt32 maxNU, IProgress progressAct)
{
return Task.Run(() =>
{
for (int i = 0; i <= maxNU; i++)
{
if (i% 10== 0||i==maxNU)
{
progressAct.Report(i*1.0 / maxNU);
}
Console.WriteLine(i);
Thread.Sleep(10);
}}); }
-
使用 Progress 的构造函数可以接受一个Action 类型的委托
ClassLibrary.Mytext mytext = new ClassLibrary.Mytext();Progress<double> progressAct = new Progress<double>(ret => { textBox1.AppendText(string.Format("已经完成:{0} %", ret*100)); textBox1.AppendText(Environment.NewLine); }); mytext.PrintNumberFoo(845, progressAct);
4.Process 还有一个ProcessChanged 事件,您可以订阅他。而不是【或者附加】将Action委托传递给构造函数。
5. 在实例化 Process 时,类捕获同步上下文(如果存在 ) 。 当foo调用Report 时, 委托通过上下文调用。
6. 异步方法可以通过将int替换为公开一系列属性的自定义类型来实现更精细的进度报告。
拓展Tap 了解 Task组合器
- .net core 暴露了很多返回Task且可以await方法。
- CRL提供了两个Task组合器
Task.WhenAny 和 Task.WhenAll
两个功能相同的方法
public async Task GetTotalSize(string[] uris)
{
IEnumerable<Task<byte[]>> downloadTasks = uris.Select(uri => new WebClient().DownloadDataTaskAsync(uri));
byte[][] contents = await Task.WhenAll(downloadTasks);
return contents.Sum(c => c.Length);
}
public async Task<int> GetTotalSizeTwo(string[] uris)
{
IEnumerable< Task<int>> downLoadTasks = uris.Select(async uri =>
{
return (await new WebClient().DownloadDataTaskAsync(uri)).Length;
});
int[] contextLengths = await Task.WhenAll(downLoadTasks);
return contextLengths.Sum();
}