Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 484
ominug Avatar asked Mar 15 '23 00:03

ominug


1 Answers

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.

like image 141
i3arnon Avatar answered Mar 17 '23 20:03

i3arnon