In contrast to Task.Wait()
or Task.Result
, await
’ing a Task
in C# 5 prevents the thread which executes the wait from lying fallow. Instead, the method using the await
keyword needs to be async
so that the call of await
just makes the method to return a new task which represents the execution of the async
method.
But when the await
’ed Task
completes before the async
method has received CPU time again, the await
recognizes the Task
as finished and thus the async
method will return the Task
object only at a later time. In some cases this would be later than acceptable because it probably is a common mistake that a developer assumes the await
’ing always defers the subsequent statements in his async
method.
The mistaken async
method’s structure could look like the following:
async Task doSthAsync()
{
var a = await getSthAsync();
// perform a long operation
}
Then sometimes doSthAsync()
will return the Task
only after a long time.
I know it should rather be written like this:
async Task doSthAsync()
{
var a = await getSthAsync();
await Task.Run(() =>
{
// perform a long operation
};
}
... or that:
async Task doSthAsync()
{
var a = await getSthAsync();
await Task.Yield();
// perform a long operation
}
But I do not find the last two patterns pretty and want to prevent the mistake to occur. I am developing a framework which provides getSthAsync
and the first structure shall be common. So getSthAsync
should return an Awaitable which always yields like the YieldAwaitable
returned by Task.Yield()
does.
Unfortunately most features provided by the Task Parallel Library like Task.WhenAll(IEnumerable<Task> tasks)
only operate on Task
s so the result of getSthAsync
should be a Task
.
So is it possible to return a Task
which always yields?
First of all, the consumer of an async method shouldn't assume it will "yield" as that's nothing to do with it being async. If the consumer needs to make sure there's an offload to another thread they should use Task.Run
to enforce that.
Second of all, I don't see how using Task.Run
, or Task.Yield
is problematic as it's used inside an async method which returns a Task
and not a YieldAwaitable
.
If you want to create a Task
that behaves like YieldAwaitable
you can just use Task.Yield
inside an async method:
async Task Yield()
{
await Task.Yield();
}
Edit:
As was mentioned in the comments, this has a race condition where it may not always yield. This race condition is inherent with how Task
and TaskAwaiter
are implemented. To avoid that you can create your own Task
and TaskAwaiter
:
public class YieldTask : Task
{
public YieldTask() : base(() => {})
{
Start(TaskScheduler.Default);
}
public new TaskAwaiterWrapper GetAwaiter() => new TaskAwaiterWrapper(base.GetAwaiter());
}
public struct TaskAwaiterWrapper : INotifyCompletion
{
private TaskAwaiter _taskAwaiter;
public TaskAwaiterWrapper(TaskAwaiter taskAwaiter)
{
_taskAwaiter = taskAwaiter;
}
public bool IsCompleted => false;
public void OnCompleted(Action continuation) => _taskAwaiter.OnCompleted(continuation);
public void GetResult() => _taskAwaiter.GetResult();
}
This will create a task that always yields because IsCompleted
always returns false. It can be used like this:
public static readonly YieldTask YieldTask = new YieldTask();
private static async Task MainAsync()
{
await YieldTask;
// something
}
Note: I highly discourage anyone from actually doing this kind of thing.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With