StephenCleary / AsyncEx

A helper library for async/await.
MIT License
3.49k stars 358 forks source link

The ApmAsyncFactory interop hangs if a path in async method returns synchronously. #281

Open fiseni opened 10 months ago

fiseni commented 10 months ago

Hi,

I observed a strange behavior in ApmAsyncFactory. If the task contains a path that returns synchronously, then the execution hangs. It's easy to reproduce, so I'll just provide an example. I'll use asmx web service to demonstrate the issue.

public class DemoService : System.Web.Services.WebService
{

    [WebMethod]
    public IAsyncResult BeginRun(int option, AsyncCallback callback, object state)
    {
        return ApmAsyncFactory.ToBegin<string>(GetResponseAsync(option), callback, state);
    }
    [WebMethod]
    public string EndRun(IAsyncResult result)
    {
        return ApmAsyncFactory.ToEnd<string>(result);
    }

    private async Task<string> GetResponseAsync(int option)
    {
        if (option == 1)
        {
            await Task.Delay(1000);
            return "Works";
        }
        else if (option == 2)
        {
            // I tried awaiting a completed task too, just to test it out
            await Task.CompletedTask;
            return "Hangs";
        }
        else
        {
            return "Hangs";
        }
    }
}

To be sure that this is not a generic problem, I tried the solution provided in this article, and it works without issues.

public class DemoService : System.Web.Services.WebService
{

    [WebMethod]
    public IAsyncResult BeginRun(int option, AsyncCallback callback, object state)
    {
        return GetResponseAsync(option).AsApm(callback, state);
    }
    [WebMethod]
    public string EndRun(IAsyncResult result)
    {
        return ((Task<string>)result).Result;
    }

    private async Task<string> GetResponseAsync(int option)
    {
        if (option == 1)
        {
            await Task.Delay(1000);
            return "Works";
        }
        else if (option == 2)
        {

            await Task.CompletedTask;
            return "Works";
        }
        else
        {
            return "Works";
        }
    }
}
fiseni commented 10 months ago

It seems there is a race condition. If there is an asynchronous execution, the ToBegin returns before the callback is invoked. On the other hand, if the task is already completed, the callback is invoked before the ToBegin manages to return a result.

image

Just to confirm this, I tried adding a small delay before the callback is invoked, and it works properly then.

EDIT: There is no even race condition here. If the task is already completed, CompleteAsync will run synchronously, and a callback always will be invoked before ToBegin returns;