dingalla
dingalla

Reputation: 1239

Does calling .Result on a task that has already been awaited break the async pattern?

By the time the code calls Task.Result, it has already been awaited, so does the asynchronous pattern here still hold?

class Program
{
    static async Task Main(string[] args)
    {
        var addNumbersTask = AddNumbers(10, 20);

        var result = AwaitForResult(addNumbersTask).Result;

        Console.WriteLine(result);
    }

    static async Task<int> AddNumbers(int a, int b)
    {
        await Task.Delay(250);
        return a + b;
    }

    static async Task<int> AwaitForResult(Task<int> task)
    {
        await task;

        return task.Result;
    }
}

Background if you're interested: Trying to emit IL code for a proxy class that needs to handle async calls, but I don't want to generate the async state machine in the IL. So I figured I could delegate the actual "await" part to a helper outside of the IL. Also, I know there are proxy types out there but the hopeless engineer in me wants to write it myself.

Edit: Updated example.

interface IService
{
    Task<int> AddAsync(int a, int b);
}

class Service : IService
{
    public async Task<int> AddAsync(int a, int b)
    {
        await Task.Delay(250);  // Some web service call...
        return a + b;
    }
}

// This class 100% generated via reflection emit
class Proxy : IService
{
    private readonly IService _actual;

    public Proxy(IService actual) => _actual = actual;

    public Task<int> AddAsync(int a, int b)
    {
        return Awaiter.Await(_actual.AddAsync(a, b));
    }
}

static class Awaiter
{
    public static async Task<int> Await(Task<int> task)
    {
        return await task;
    }
}

class Program
{

    static async Task Main(string[] args)
    {
        var proxy = new Proxy(new Service());
        var result = await proxy.AddAsync(5, 5);

        Console.WriteLine($"Result is {result}", result);
    }
}

Upvotes: 0

Views: 636

Answers (3)

Stephen Cleary
Stephen Cleary

Reputation: 456417

does the asynchronous pattern here still hold?

No. There's nothing magical about using await on a task. Instead, consider whether the task is completed. Calling task.Result will block the calling thread until task completes. Doing await task will asynchronously wait until task completes.

So in this code, Result will not block:

static async Task<int> AwaitForResult(Task<int> task)
{
  // Task may not yet be complete here.
  await task;
  // At this point, task is complete.

  // Since task is complete, Result does not block.
  return task.Result;
}

But that is totally different than this code:

var result = AwaitForResult(addNumbersTask).Result;

// Equivalent to:
var task = AwaitForResult(addNumbersTask);
var result = task.Result;

The task returned from AwaitForResult may not be complete here, since it was never awaited. And if it's not complete, then Result will block.

Trying to emit IL code for a proxy class that needs to handle async calls, but I don't want to generate the async state machine in the IL. So I figured I could delegate the actual "await" part to a helper outside of the IL.

Have you tried Roslyn APIs? I find them much more convenient than IL emit.

If your proxy is just a pass-through, then you can just return the inner task directly:

// This class 100% generated via reflection emit
class Proxy : IService
{
  private readonly IService _actual;

  public Proxy(IService actual) => _actual = actual;

  public Task<int> AddAsync(int a, int b) => _actual.AddAsync(a, b);
}

But if you want to add much real logic, then I'd recommend using Roslyn to generate the async state machine for you.

Upvotes: 2

Servy
Servy

Reputation: 203811

The method Awaiter.Await is basically pointless. Outside of adding some overhead, the returned task will be functionally identical to the one passed in.

The only actual difference in code between simply returning the result of _actual.AddAsync(a, b) and awaiting it is that, if AddAsync throws an exception, if the method is async it will return a faulted task instead of throwing. So if that either can't happen, or if Proxy.AddAsync can safely throw itself in that situation, then simply return the value, no need to do anything. If you need to ensure that Proxy.AddAsync never throws an exception, and instead returns a faulted task if Proxy.AddAsync throws an excpetion, then you simply need to wrap the method in a try/catch and return a faulted exception in the catch block. You don't need to replicate any of the rest of the logic of the async state machine.

So assuming you want the same exception semantics as the behavior of the service's method, which I suspect you do, you can treat these methods exactly the same as you would a synchronous method, that is to say invoke the method with the appropriate parameters and return the result.

Upvotes: 0

sellotape
sellotape

Reputation: 8325

If you’re trying to avoid blocking, then no.

In your console app example it makes little difference, but if you did that somewhere where you don’t want blocking - e.g. a UI event handler - .Result will still block.

By the time you have a value in addNumbersTask, that task is started. You then immediately pass the task to AwaitForResult, which immediately begins to await it, at which point an incomplete task that returns an int is returned back to main(), and you then call .Result on it. You are now blocking that thread for most of the 250ms until that task returns its int result to .Result in main().

The way you have expressed it in the question, “awaited” does not equal “completed”.

Upvotes: 0

Related Questions