ominug
ominug

Reputation: 1572

How to create a Task which always yields?

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 Tasks so the result of getSthAsync should be a Task.

So is it possible to return a Task which always yields?

Upvotes: 4

Views: 1063

Answers (2)

Theodor Zoulias
Theodor Zoulias

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

i3arnon
i3arnon

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

Related Questions