Reputation: 1572
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?
Upvotes: 4
Views: 1063
Reputation: 43886
Here is a polished version of i3arnon's YieldTask
:
public class YieldTask : Task
{
public YieldTask() : base(() => { },
TaskCreationOptions.RunContinuationsAsynchronously)
=> RunSynchronously();
public new YieldAwaitable.YieldAwaiter GetAwaiter()
=> default;
public new YieldAwaitable ConfigureAwait(bool continueOnCapturedContext)
{
if (!continueOnCapturedContext) throw new NotSupportedException();
return default;
}
}
The YieldTask
is immediately completed upon creation, but its awaiter says otherwise. The GetAwaiter().IsCompleted
always returns false
. This mischief makes the await
operator to trigger the desirable asynchronous switch, every time it awaits this task. Actually creating multiple YieldTask
instances is redundant. A singleton would work just as well.
There is a problem with this approach though. The underlying methods of the Task
class are not virtual, and hiding them with the new
modifier means that polymorphism doesn't work. If you store a YieldTask
instance to a Task
variable, you'll get the default task behavior. This is a considerable drawback for my use case, but I can't see any solution around it.
Upvotes: 0
Reputation: 116636
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.
Upvotes: 7