最近在看同事代码的时候,看到了一个之前从来没用过的类TaskCompletionSource,研究了一下这个类是干嘛的,在此记录一下。

TaskCompletionSource类的作用

简单来说,TaskCompletionSource类提供了一种手动控制Task完成状态的方式。

回想一下我们平时是怎么使用Task的。在创建Task时,必须提供一个delegate,表示需要执行什么操作。然后,这个操作会被自动执行,直到完成后,Task会被自动设置上相应的完成状态,如RanToCompletion, Faulted或者Canceled

我们可以创建一个什么都不干的Task,然后根据情况灵活地设置它的完成状态吗?用Task类是无法实现的,但是用TaskCompletionSource类可以。下面是一个简单的例子。

在创建TaskCompletionSource时,内部会创建一个什么都不干的Task。生产者可以通过TaskCompletionSourceSetResult等方法设置Task的完成状态,而消费者可以在生产者设置完Task的完成状态后获取结果。

TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
Task<int> task = tcs.Task;

// Start a background task that will complete tcs.Task
Task.Run(() =>
{
    Thread.Sleep(1000);
    tcs.SetResult(15);
});

// The attempt to get the result of task blocks the current thread until the completion source gets signaled
Stopwatch sw = Stopwatch.StartNew();
int result = task.Result;
sw.Stop();

// ElapsedTime should be ~1000ms, task.Result should be 15
Console.WriteLine("(ElapsedTime={0}): task.Result={1}", sw.ElapsedMilliseconds, result);

TaskCompletionSource中,有六个方法可以用来设置Task的完成状态:

  • SetResultTrySetResult:设置为RanToCompletion状态
  • SetExceptionTrySetException:设置为Faulted状态
  • SetCanceledTrySetCanceled:设置为Canceled状态

SetXXXTrySetXXX的区别在于,重复调用SetXXX会抛异常,但重复调用TrySetXXX不会。

TaskCompletionSource类中的所有成员都是线程安全的。

源码解析

TaskCompletionSource的源码比较简单,主要功能是通过调用Task的internal方法实现的。

  1. 创建TaskCompletionSource时,内部会创建一个啥都不干的Task

     private readonly Task<TResult> _task;
    
     public TaskCompletionSource() => _task = new Task<TResult>();
    
  2. 可以通过属性获取该内部Task

     public Task<TResult> Task => _task;
    
  3. 设置内部Task的完成状态是通过调用Task的方法实现的

     public bool TrySetResult(TResult result)
     {
         bool rval = _task.TrySetResult(result);
         if (!rval)
         {
             _task.SpinUntilCompleted();
         }
    
         return rval;
     }