如何避免Task内联,你确定异步任务已停止执行?
【导读】目前私下空余时间在研究远程大文件断点续传下载,正在进行时,正当研究Task时,看到有个Unwrap方法基本没怎么用过,这里记录并学习下
说到Task.Run和Task.Factory.StartNew二者区别以及应该推崇使用哪一个,我也建议使用Task.Run,这里不做过多介绍,网上资料一大把。
Task代理Unwrap避免内联
那么使用该方法可以解决什么问题呢?首先我们来看如下一个简单例子
Task<Task<int>> call = Task.Factory.StartNew(async () =>
{
return await DoSomethingAsync();
});
我们通过Task.Factory.StartNew异步方法,此时获取内联任务结果将通过嵌套Task包裹,那么如果要是多层嵌套呢?那也没问题,我们可以直接将返回结果通过var声明即可,就不用再写层层嵌套Task。
这只是其一,事实情况在于对于多层内联Task,我们如何处理以及取消令牌等过程中发生的错误。Unwrap方法的作用就在于此,如下:
Task<int> call = Task.Factory.StartNew(async () =>
{
return await DoSomethingAsync();
}).Unwrap();
直接从该方法单词入手,暂且翻译为“摊开或展开”,就像洋葱般一层层剥开,让我们看到最终想要的东西,我们也看看源码怎么讲
public static Task<TResult> Unwrap<TResult>(this Task<Task<TResult>> task)
{
if (task == null)
{
throw new ArgumentNullException(nameof(task));
}
return
!task.IsCompletedSuccessfully ? Task.CreateUnwrapPromise<TResult>(task,
lookForOce: false) :
task.Result ??
Task.FromCanceled<TResult>(new CancellationToken(true));
}
利用Unwrap方法创建一个代理,它将为我们处理所有复杂逻辑,同时我们也可以不用转发取消令牌,它可以为我们排查所有不同级别的错误,换言之,它所返回的Task包含所有令牌取消请求和异常处理(依托代理与内联Task协调工作)就像我们执行一个简单的任务一样,而不用像任务中的任务而进行嵌套
var call1 = Task.Run(async () =>
{
return await DoSomethingAsync();
});
Task.Run和Task.Factory.StartNew区别也在于此,因为利用Task.Run直接将返回最终的实际结果,所以对于内联Task,利用Task.Run来的更加实际,而利用Task.Factory.StartNew若没调用Unwrap可能会出现结果不符合我们所预期
同样,利用Task.Factory.StartNew进行追加任务(ContinueWith)时,也会返回一个任务内联另外一个任务,通过使用Unwrap方法即可避免这种情况。
上述只是我个人对使用Unwrap方法的概括和总结,这里给出源码中注释解释翻译:
到了这里,想必我们已经清楚Unwrap方法的作用,我们来做个练习:若启动一个长时间执行的异步任务,那么如何停止这个异步任务?
public class UnwrapPractice
{
CancellationTokenSource cancellationToken;
public void Start()
{
cancellationToken = new CancellationTokenSource();
Task.Factory.StartNew(
function: ExecuteAsync,
cancellationToken: cancellationToken.Token,
creationOptions: TaskCreationOptions.LongRunning,
scheduler: TaskScheduler.Default);
}
public void Stop()
{
cancellationToken.Cancel();
}
async Task ExecuteAsync()
{
Console.WriteLine("进入执行操作");
while (!cancellationToken.IsCancellationRequested)
{
//模拟长时间执行操作
await Task.Delay(30000);
}
Console.WriteLine("退出执行操作");
}
}
通过上述简单代码实现作业练习,接下来我们在控制台调用启动和停止,看看结果是否符合预期
static async Task Main(string[] args)
{
var unwrapPractice = new UnwrapPractice();
Console.WriteLine("启动");
unwrapPractice.Start();
Thread.Sleep(3000);
Console.WriteLine("停止");
unwrapPractice.Stop();
Console.Read();
}
反观上述动态图,此时我们将发现长时间异步任务通过调用取消令牌根本没有停止(在控制台并没有打印出【退出执行操作】字眼),此时我们可利用Unwrap方法创建此任务代理,然后在调用令牌取消方法后,再执行代理的等待方法,等待取消任务直至完成即可,貌似有点Thread中Join方法的意味,如下:
public class UnwrapPractice
{
Task task;
CancellationTokenSource cancellationToken;
public void Start()
{
cancellationToken = new CancellationTokenSource();
Task<Task> _task = Task.Factory.StartNew(
function: ExecuteAsync,
cancellationToken: cancellationToken.Token,
creationOptions: TaskCreationOptions.LongRunning,
scheduler: TaskScheduler.Default);
//获取此任务代理
task = _task.Unwrap();
}
public void Stop()
{
cancellationToken.Cancel();
//等待取消任务完成(重要)
task.Wait();
}
async Task ExecuteAsync()
{
Console.WriteLine("进入执行操作");
while (!cancellationToken.IsCancellationRequested)
{
//模拟长时间执行操作
await Task.Delay(50000);
}
Console.WriteLine("退出执行操作");
}
}
💡 由此可见,对于长时间异步任务的执行,当我们想让其停止时,我们想当然的认为只需调用取消令牌的Cancel方法就可万事大吉,经练习实践证明其实依然在执行,并没有完全停止!
💡 若利用Task.Run直接调用Wait方法即可,我们也发现,当我们需要做更多处理,使用Task.Factory.StartNew时,若对各种选项配置没有一个很清楚的认识和理解,很容易用出毛病!
本文分享自微信公众号 - JeffckyShare(JeffckyShare)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

