A Tour of Task, Part 10: Promise Tasks

Last time, we looked at ways to start Delegate Tasks. Today we’ll look at the most common ways to create Promise Tasks. As a reminder, Promise Tasks are tasks that represent a kind of “event” within a system; they don’t have any user-defined code to execute.

Task.Delay

The int argument is treated as a number of milliseconds; I usually prefer the TimeSpan versions since they are more explicit. Using int millisecond values for timeouts is a holdover from an older API design; many Win32-level APIs only take timeout values as integer milliseconds. So, it makes sense to expose an int parameter for lower-level waits like WaitHandle.WaitOne or even Task.Wait. However, Task.Delay isn’t a thin wrapper over any Win32 API; the int parameter in this case is just provided for tradition.

Delay may also take a CancellationToken, which allows the delay to be cancelled.

Under the hood, Delay starts a timer and completes its returned task when that timer fires. Or, if the CancellationToken is signaled first, then Delay cancels its returned task.

In real-world code, Delay is almost never used. Its primary use case is as a retry timeout, i.e., if an asynchronous operation failed, the code will (asynchronously) wait a period of time before trying again. Generally, retry logic is wrapped into a separate library (such as Transient Fault Handling or Polly), and Delay is only used internally by those libraries, not directly by application code.

Task.Yield

Task.Yield has several interesting aspects. To begin with, it doesn’t actually return a Task, so it’s not really a Promise Task kind of method:

YieldAwaitableYield();

But it does kind of act kind of like a Promise Task. The YieldAwaitable type interacts with the async compiler transformation to force an asynchronous point within a method. By default, if await is used on an operation that has already completed, then the execution of the async method continues synchronously. YieldAwaitable throws a wrench into this by always claiming it is not completed, and then scheduling its continuations immediately. This causes await to schedule the rest of the async method for immediate execution and return.

I’ve used Task.Yield only occasionally during unit testing, when I needed to ensure that a particular method would in fact work if its asynchronous operation did not complete synchronously. I’ve found Yield most useful when the asynchronous operation in question normally does complete synchronously, and I need to force asynchrony to ensure the method behavior is correct.

However, I’ve never needed Yield in production code. There is one use case where developers sometimes (incorrectly) attempt to use Yield: to try to “refresh” the UI.

// Bad code, do not use!!asyncTaskLongRunningCpuBoundWorkAsync(){// This method is called directly from the UI, and// does lots of CPU-bound work.// Since this blocks the UI, this method is given// an async signature and periodically "yields".for(inti=0;i!=1000000;++i){...// CPU-bound work.awaitTask.Yield();}}

However, this approach will not work. The reason is that UI message loops are priority queues, and any scheduled continuations have a much higher priority than “repaint the window”. So, the Yield schedules the continuation and returns to the message loop, and the message loop immediately executes that continuation without processing any of its WM_PAINT messages.

Some developers have discovered that using Task.Delay instead of Task.Yield will allow message processing (messages are processed until the timer fires). However, a far cleaner solution is to do the CPU-bound work on a background thread:

In conclusion, Task.Yield is occasionally useful when unit testing, but much less so for production code.

Task.FromResult

Task.FromResult will create a completed task with the specified value:

Task<TResult>FromResult<TResult>(TResult);

It might seem silly at first to return a task that is already completed, but this is actually useful in several scenarios.

For instance, an interface method may have an asynchronous (task-returning) signature, and if an implementation is synchronous, then it can use Task.FromResult to wrap up its (synchronous) result within a task. This is particularly useful when creating stubs for unit testing, but is also occasionally useful in production code:

interfaceIMyInterface{// Implementations *might* need to be asynchronous,// so we define an asynchronous API.Task<int>DoSomethingAsync();}classMyClass:IMyInterface{// This particular implementation is not asynchronous.publicTask<int>DoSomethingAsync(){intresult=42;// Do synchronous work.returnTask.FromResult(result);}}

Be careful, though, that your synchronous implementation is not blocking. Implementing an asynchronous API with a blocking method is surprising behavior.

Another use case of Task.FromResult is when doing some form of caching. In this case, you have some data that is synchronously retrieved (from the cache), and need to return it directly. In the case of a cache miss, then a true asynchronous operation is performed:

Tip: If you can, cache the task objects themselves instead of their resulting values; maintain a cache of operations rather than results.

As of this writing, one final common use of Task.FromResult is just as a completed task. For this, the expressions Task.FromResult(0) or Task.FromResult<object>(null) are commonly used. This use case is similar to the synchronous implementation of an asynchronous API:

interfaceIPlugin{// Permit each plugin to initialize asynchronously.TaskInitializeAsync();}classMyPlugin:IPlugin{publicTaskInitializeAsync(){// The async equivalent of a noop.returnTask.FromResult<object>(null);}}

In the preview builds of .NET 4.6, there is a static Task.CompletedTask that should be used instead of Task.FromResult(0) or Task.FromResult<object>(null).

You might be wondering if there’s a way to return already-completed tasks in other states - particularly, canceled or faulted tasks. As of now, you have to write this yourself (using TaskCompletionSource), but .NET 4.6 will introduce the Task.FromCanceled and Task.FromException methods to return synchronously canceled or faulted tasks.