在C#异步编程中使用await/async时,会经常使用Task类,除了void之外,Task是异步方法的主要返回类型,还有后来的TaskValue(较少使用)。那偶尔也会看见异步中使用TaskCompletionSource,它有什么用呢?
目录
1.理解TaskCompletionSource
2.将一个回调换成TaskCompletionSource
3.利用TaskCompletionSource实现暂停
4.关于TaskCompletionSource的建议
5.TaskCompletionSource其它用途
简而言之,TaskCompletionSource是用于适配异步。
一般而言我们直接使用Task.Run() 或者Task.Factory.StartNew()生产一个Task类型,并将其运行在其它线程中。问题是,不用上面的这些也能创建异步代码吗?
这是一个好问题!虽然Task.Run能够让一些同步的代码变成一个Task(通过在一个单独的线程上运行),TaskCompletionSource能够让一些已经异步的代码变成Task(当然它最重要的功能是能够让一些基于回调的代码变成更好看的async/await)
Task类需要启用c#的async/await语言支持,而在.NET第一个版本发布时,Task类还不存在。因此,在c#中还有一些遗留的代码不涉及Task类的方法来实现异步,因此与async/await不兼容。例如,有一个异步编程模型,你需要传递一个AsyncCallback到一个BeginInvoke方法。还有基于事件的异步模式(Event-Based Asynchronous Pattern),在该模式中,订阅一个完成的事件。这两种方法都是回调的形式,其中你提供的方法在操作完成后被调用。
不管使用的确切模式是什么,在你的c#职业生涯的某个时刻,你都可能遇到回调。当你这样做时,可能会发现自己希望有一种方法将回调代码转换为可以等待的Task。幸运的是,TaskCompletionSource类让你做到了这一点!因此,TaskCompletionSource本身不是可等待的,它也不是有效的异步方法返回类型。一旦TaskCompletionSource给了你一个任务,你可以简单地返回那个任务,就像你在异步方法中做其他任何事情一样。
假设你有一个库,库中有个上传文件的函数:
public static void UploadFile(string name, byte[] data, Action onCompleted);
参考库的文档,你会发现onCompleted是上传完成时调用的回调函数,如果上传成功,它的值为true,如果上传失败,它的值为false。要使用这个库方法上传一个文件,你必须做如下的事情,假设你的应用程序有一个叫做statusText的文本显示:
public async void OnUploadButtonClicked()
{
statusText.Text = "Generating Image...";
byte[] imageData = await GenerateImage();
statusText.Text = "Uploading Image...";
MyBox.UploadFile("image.jpg", imageData, success =>
{
statusText.Text = success ? string.Empty : "Error Uploading";
});
}
这个方法能够很好的运行,但你确实更喜欢使用async/await而不是回调方法,特别是如果您计划在您的应用程序中大量使用该库。你可以通过创建一个使用TaskCompletionSource的helper方法从helper方法的调用方隐藏回调。首先,定义一个TaskCompletionSource的实例。它接受一个泛型参数,该参数表示您想要返回的类型。在本例中,我们希望返回成功变量的值,这是一个布尔值,因此使用bool作为泛型参数。
var taskCompletionSource = new TaskCompletionSource();
我们把它定义在一个helper方法中:
public static class MyBoxHelper
{
public static Task UploadFile(string name, byte[] data)
{
var taskCompletionSource = new TaskCompletionSource();
}
}
注意,我们使用了返回类型Task,这是为了预期我们能够使用TaskCompletionSource生成一个返回布尔值的Task。
接下来,我们需要告诉TaskCompletionSource它的操作何时完成,并将操作的结果传递给它。在TaskCompletionSource中有一个setreresult方法就是为了这个目的。毫不奇怪,当我们知道结果时,我们必须在回调中调用它:
public static Task UploadFile(string name, byte[] data)
{
var taskCompletionSource = new TaskCompletionSource();
MyBox.UploadFile(name, data, success =>
{
taskCompletionSource.SetResult(success);
});
}
再然后,我们需要请求TaskCompletionSource给我们一个Task。这超级简单,因为TaskCompletionSource有一个属性叫做Task!直接返回它。
public static Task UploadFile(string name, byte[] data)
{
var taskCompletionSource = new TaskCompletionSource();
MyBox.UploadFile(name, data, success =>
{
taskCompletionSource.SetResult(success);
});
return taskCompletionSource.Task;
}
为了避免一个永远不会完成的Task,请确保添加异常处理,以便在出现任何问题时可以完成Task。在异常的情况下,可以简单地使用setcancel取消(这反过来会抛出一个通用的TaskCanceledException),但是,更好的方法是使用SetException提供导致问题的异常。
public static Task UploadFile(string name, byte[] data)
{
var taskCompletionSource = new TaskCompletionSource();
try
{
MyBox.UploadFile(name, data, success =>
{
taskCompletionSource.SetResult(success);
});
}
catch (Exception ex)
{
taskCompletionSource.SetException(ex);
}
return taskCompletionSource.Task;
}
OK!现在,每当MyBox.UploadFile调用回调函数或抛出异常,TaskCompletionSource生成的任务将被完成以反映这一点。
重要的是,每个可能的结果都应该由你的逻辑来处理;如果有额外的回调函数指示结束(例如onCanceled, onTimeout),你会想要在每一个函数中调用TaskCompletionSource的SetXXX方法。
一旦有了辅助方法,现在就可以在代码的任何地方await它,而不必使用回调。让我们更新按钮点击事件处理程序:
public async void OnUploadButtonClicked()
{
statusText.Text = "Generating Image...";
byte[] imageData = await GenerateImage();
statusText.Text = "Uploading Image...";
bool success = await MyBoxHelper.UploadFile("image.jpg", imageData);//这一行变了
statusText.Text = success ? string.Empty : "Error Uploading";
}
完美!通过在助手类中隐藏回调的实现细节,这段代码现在可以有一个线性流,缩进更少,这都是使用async/await的好处。
如果使用setcancel或SetException,实际上还有一件事要做,那就是添加一个封装对MyBoxHelper.UploadFile调用的try/catch块。这只是为了避免你的应用程序在TaskCanceledException或类似的情况下崩溃。你可以随意修改MyBoxHelper,让它在异常处理程序中调用SetResult(false);在这种情况下,调用者将不需要任何异常处理。
案例:火车票选购>选好车票>支付(软件界面暂停)>跳到支付页面>支付成功( 支付页面继续执行)>跳到支付页面
internal class TaskCompletionSourceStudy
{
public static async Task MockPuase()
{
TaskCompletionSource task = new TaskCompletionSource();
var t1=Task.Run(() => ChoiceTicket(task));
var t2=Task.Run(() => PayForMoney(task));
await Task.WhenAll(t1, t2);
// await t1;
Console.WriteLine("祝您一路顺风");
}
public static async Task ChoiceTicket(TaskCompletionSource tcs)
{
Console.WriteLine("选购火车票");
for(int i = 0;i<3;i++)
{
Console.WriteLine("浏览中...");
await Task.Delay(1000);
}
Console.WriteLine("跳转到支付页面");
// await Task.Run(() => PayForMoney(tcs));
await tcs.Task;
Console.WriteLine("订票完成...");
return await tcs.Task;
}
public static async Task PayForMoney(TaskCompletionSource tcs)
{
await Task.Delay(5000).ContinueWith(t=>Console.WriteLine("请选择支付类型"));
Thread.Sleep(1000);
Console.WriteLine("您已经支付完成");
tcs.SetResult(true);
Console.WriteLine("即将跳回到购票页面。。。");
}
}
实际运行结果如下:
这个例子其实并不是特别恰当,因为它站在上帝视角,假设一开始就在执行支付任务,如果将支付任务的等待时间改为1秒,则结果如下:
真正的实际是在浏览之后发起支付请求,然后等待支付完毕后返回,这里其实没有必要用2个线程,因为函数调用天生就是会中断购票程序。当然如果是大型购票系统,那肯定是要用事件,消息队列,缓存等来处理。
不过这个例子虽然不恰当,但是我们还是能体会到利用TaskCompletionSource来实现中断的方法。
创建TaskCompletionSource时建议使用TaskCreationOptions.RunContinuationAsynchronously属性。
对于编写类库的人来说TaskCompletionSource
是一个具有非常重要的作用,默认情况下任务延续可能会在调用try/set(Result/Exception/Cancel)的线程上进行运行,这也就是说作为编写类库的人来说必须需要考虑上下文,这通常是非常危险,可能就会导致死锁' 线程池饥饿 *数据结构损坏(如果代码异常运行)
所以在创建TaskCompletionSourece
时,应该使用TaskCreationOption.RunContinuationAsyncchronously
参数将后续任务交给线程池进行处理
TaskCompletionSource类可以将任何异步操作转换为Task,但开发人员经常忽略的一个用例是,它也可以用于用户界面交互。例如,假设你有一个带有确认对话框的按钮。要处理确认对话框的结果,你可能需要如下代码:
public void OnDeleteButtonClicked()
{
ShowModalDialog("Are you sure?");
}
public void ModalDialog_OkButtonClicked()
{
HideModalDialog();
Delete();
}
public void ModalDialog_CancelButtonClicked()
{
HideModalDialog();
}
Task的一个有用的方面是它没有内置的超时机制,所以它可以在任何时间内处于活动状态。在这种情况下,您可以将用户界面交互(自然是异步的)表示为Task,只要用户需要,就可以等待它。使用TaskCompletionSource,将一个用户界面交互映射到一个Task,就像我们在上面的回调中做的一样简单。例如,对一个模态确认对话框进行这样的操作,会得到如下的结果:
public async void OnDeleteButtonClicked()
{
if (await ModalDialogHelper.Show("Are you sure?"))
{
Delete();
}
}
这不仅减少了代码,而且更易于阅读,目的也更明确。只是要确保处理了所有可能的方法,可以解散对话框,以便Task不会“孤立”。例如,如果用户点击模态对话框的后面,或者点击键盘上的Esc键,模态对话框会被取消吗?如果是这样,你也需要在TaskCompletionSource上调用一个Set*方法。