A Tour of Task, Part 10: Promise Tasks• Comments
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 is the asynchronous equivalent of
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.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 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:
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.
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.
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
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:
Task.Yield is occasionally useful when unit testing, but much less so for production code.
Task.FromResult will create a completed task with the specified value:
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:
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<object>(null) are commonly used. This use case is similar to the synchronous implementation of an asynchronous API:
In the preview builds of .NET 4.6, there is a static
Task.CompletedTask that should be used instead of
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.FromException methods to return synchronously canceled or faulted tasks.