C#中的TaskCompletionSource

        在C#异步编程中使用await/async时,会经常使用Task类,除了void之外,Task是异步方法的主要返回类型,还有后来的TaskValue(较少使用)。那偶尔也会看见异步中使用TaskCompletionSource,它有什么用呢?

目录

1.理解TaskCompletionSource

2.将一个回调换成TaskCompletionSource

3.利用TaskCompletionSource实现暂停 

4.关于TaskCompletionSource的建议

5.TaskCompletionSource其它用途



1.理解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给了你一个任务,你可以简单地返回那个任务,就像你在异步方法中做其他任何事情一样。

2.将一个回调换成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);在这种情况下,调用者将不需要任何异常处理。

3.利用TaskCompletionSource实现暂停 

        案例:火车票选购>选好车票>支付(软件界面暂停)>跳到支付页面>支付成功( 支付页面继续执行)>跳到支付页面

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("即将跳回到购票页面。。。");

        }
    }

        实际运行结果如下:

C#中的TaskCompletionSource_第1张图片

        这个例子其实并不是特别恰当,因为它站在上帝视角,假设一开始就在执行支付任务,如果将支付任务的等待时间改为1秒,则结果如下:

C#中的TaskCompletionSource_第2张图片

        真正的实际是在浏览之后发起支付请求,然后等待支付完毕后返回,这里其实没有必要用2个线程,因为函数调用天生就是会中断购票程序。当然如果是大型购票系统,那肯定是要用事件,消息队列,缓存等来处理。

        不过这个例子虽然不恰当,但是我们还是能体会到利用TaskCompletionSource来实现中断的方法。

4.关于TaskCompletionSource的建议

        创建TaskCompletionSource时建议使用TaskCreationOptions.RunContinuationAsynchronously属性。         

        对于编写类库的人来说TaskCompletionSource是一个具有非常重要的作用,默认情况下任务延续可能会在调用try/set(Result/Exception/Cancel)的线程上进行运行,这也就是说作为编写类库的人来说必须需要考虑上下文,这通常是非常危险,可能就会导致死锁线程池饥饿 *数据结构损坏(如果代码异常运行)

        所以在创建TaskCompletionSourece时,应该使用TaskCreationOption.RunContinuationAsyncchronously参数将后续任务交给线程池进行处理

                                                                                

5.TaskCompletionSource其它用途

        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*方法。


6.参考文档

  1. Task vs. TaskCompletionSource in C#

  2. TaskCompletionSource

你可能感兴趣的:(前端,javascript,开发语言)