5

In my current project, I have a piece of code that, after simplifying it down to where I'm having issues, looks something like this:

private async Task RunAsync(CancellationToken cancel)
{
    bool finished = false;
    while (!cancel.IsCancellationRequested && !finished)
        finished = await FakeTask();
}

private Task<bool> FakeTask()
{
    return Task.FromResult(false);
}

If I use this code without awaiting, I end up blocking anyway:

// example 1
var task = RunAsync(cancel); // Code blocks here...
... // Other code that could run while RunAsync is doing its thing, but is forced to wait
await task;

// example 2
var task = RunAsync(cancelSource.Token); // Code blocks here...
cancelSource.Cancel(); // Never called

In the actual project, I'm not actually using FakeTask, and there usually will be some Task.Delay I'm awaiting in there, so the code most of the time doesn't actually block, or only for a limited amount of iterations.

In unit testing, however, I'm using a mock object that does pretty much do what FakeTask does, so when I want to see if RunAsync responds to its CancellationToken getting cancelled the way I expect it to, I'm stuck.

I have found I can fix this issue by adding for example await Task.Delay(1) at the top of RunAsync, to force it to truly run asynchronous, but this feels a bit hacky. Are there better alternatives?

B. Ball
  • 114
  • 8
  • 1
    "something like this". Is it *enough* like this that you've executed *the code you're showing us* and seen the same problem? – Damien_The_Unbeliever Mar 19 '19 at 15:01
  • 2
    If you don't have any asynchrony, you will run synchronously. Read https://blog.slaks.net/2014-12-23/parallelism-async-threading-explained/ – SLaks Mar 19 '19 at 15:01
  • The async is only going to do something if you have other threads running. If you have only one thread then it going to look like it blocks. – jdweng Mar 19 '19 at 15:03
  • 1
    @jdweng If that were true, the `async` keyword would be useless in the language. What you're describing is _not_ asynchronous work. – 41686d6564 stands w. Palestine Mar 19 '19 at 15:05
  • Assuming that `cancel` is a CancellationToken, this code won't even compile. – 41686d6564 stands w. Palestine Mar 19 '19 at 15:10
  • @AhmedAbdelhameed You are right, I've been mixing up some CancellationToken and a CancellationTokenSource, got sloppy in copying my example. – B. Ball Mar 20 '19 at 09:18
  • @Damien_The_Unbeliever Yes, that is enough. As SLaks says, the problem is there is no asynchrony in there if all calls just end up in Task.CompletedTask or Task.FromResult(). Adding await Task.Delay(1) introduces some asynchrony, but perhaps not in the best way, so I was wondering if there was something better. – B. Ball Mar 20 '19 at 09:23

4 Answers4

8

You have an incorrect mental picture of what await does. The meaning of await is:

  • Check to see if the awaitable object is complete. If it is, fetch its result and continue executing the coroutine.
  • If it is not complete, sign up the remainder of the current method as the continuation of the awaitable and suspend the coroutine by returning control to the caller. (Note that this makes it a semicoroutine.)

In your program, the "fake" awaitable is always complete, so there is never a suspension of the coroutine.

Are there better alternatives?

If your control flow logic requires you to suspend the coroutine then use Task.Yield.

Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
4

Task.FromResult actually runs synchronously, as would await Task.Delay(0). If you want to actually simulate asynchronous code, call Task.Yield(). That creates an awaitable task that asynchronously yields back to the current context when awaited.

Jesse de Wit
  • 3,867
  • 1
  • 20
  • 41
  • `await Task.Delay(0)` isn't synchronous, it awaits asynchronously – OlegI Mar 19 '19 at 15:16
  • 2
    @OlegI No it does not. See [here](https://stackoverflow.com/a/18527223/3883866) or [here](https://stackoverflow.com/questions/33407007/task-delay0-not-asynchronous) – Jesse de Wit Mar 19 '19 at 15:20
  • Thanks! It seems it is not asynchronous only because of a too short delay(0) – OlegI Mar 19 '19 at 15:40
  • Thanks. `await Task.Yield()` does look much better than the `await Task.Delay(1)` fix I had, without introducing an unnecessary delay. – B. Ball Mar 20 '19 at 09:56
1

As @SLaks said, your code will run synchronously. One thing is running async code, and another thing is running parallel code.

If you need to run your code in parallel you can use Task.Run.

class Program
{
    static async Task Main(string[] args)
    {
        var tcs = new CancellationTokenSource();
        var task = Task.Run(() => RunAsync("1", tcs.Token));
        var task2 = Task.Run(() => RunAsync("2", tcs.Token));
        await Task.Delay(1000);
        tcs.Cancel();
        Console.ReadLine();
    }

    private static async Task RunAsync(string source, CancellationToken cancel)
    {
        bool finished = false;
        while (!cancel.IsCancellationRequested && !finished)
            finished = await FakeTask(source);
    }

    private static Task<bool> FakeTask(string source)
    {
        Console.WriteLine(source);
        return Task.FromResult(false);
    }
}
hardkoded
  • 18,915
  • 3
  • 52
  • 64
1

C#'s async methods execute synchronously up to the point where they have to wait for a result.

In your example there is no such point where the method has to wait for a result, so the loop keeps running forever and thereby blocking the caller.

Inserting an await Task.Yield() to simulate some real async work should help.

stmax
  • 6,506
  • 4
  • 28
  • 45