3

I often write code that has convenience methods which basically wrap other methods. Here's a simple example:

public class WithoutAsync
{
    public static ReadOnlyCollection<Response> GetResponses(IEnumerable<Request> fromRequests)
    {
        var ret = new List<Response>();

        foreach (Request r in fromRequests)
        {
            ret.Add(new Response());
        }

        return ret.AsReadOnly();
    }

    //convenience method
    public static Response GetResponse(Request fromRequest)
    {
        return GetResponses(new Request[] {fromRequest})[0];
    }
}

Now I want to await long-running operations but I can't quite figure out how to retrofit this methodology for use with TPL:

public class WithAsync
{
    public static async Task<ReadOnlyCollection<Response>> GetResponses(IEnumerable<Request> fromRequests)
    {
        var awaitableResponses = new List<Task<Response>>();

        foreach (Request r in fromRequests)
        {
            awaitableResponses.Add(Task.Run<Response>(async () =>
                {
                    await Task.Delay(10000); //simulate some long running async op.
                    return new Response();
                }));
        }

        return new List<Response>(await Task.WhenAll(awaitableResponses)).AsReadOnly();
    }

    //convenience method
    public static Task<Response> GetResponse(Request fromRequest)
    {
        return GetResponse(new Request[] { fromRequest });
    }
}

The convenience method above obviously won't work because it's trying to return a Task<ReadOnlyCollection<Response>> when it really needs to return a Task<Response>.

This works:

//convenience method
public static Task<Response> GetResponse(Request fromRequest)
{
    return new Task<Response>(new Func<Response>(() => GetResponse(new Request[] { fromRequest }).Result[0]));
}

but it seems really awkward, and more importantly, it blocks on .Result[0] which is potentially on a UI thread.

Is there any good way to accomplish what I'm trying to do?

rory.ap
  • 34,009
  • 10
  • 83
  • 174
  • Just guessing here: `public static async Task GetResponseAsync(Request fromRequest) { var collection = await GetResponsesAsync(new Request[] { fromRequest }); return collection[0]; });`, presuming that you renamed the `async` version of the non-convenience method accordingly. I haven't tested this -- does it still block? – Andrew Jan 30 '15 at 20:37
  • Where you have `Task.Run(async () => { await Task.Delay(10000); return new Response(); });` there's no need for `Task.Run` here. If the operation is already `async`, there's no need for it to be run in a thread pool thread. – Servy Jan 30 '15 at 20:45

2 Answers2

5

You're trying to avoid making that "convenience method" async, but there's no reason to do that.

What you want is to call the other method, wait until there are responses and then get the first and only one. You can do that by making it async and using await:

async Task<Response> GetResponseAsync(Request fromRequest)
{
    var responses = await GetResponsesAsync(new[] { fromRequest });
    return responses.Single();
}

Although a better solution in this specific case is to switch things around and have the single GetResponse actually do that work of a single request, and have the multiple GetRsponses call it instead:

async Task<ReadOnlyCollection<Response>> GetResponsesAsync(IEnumerable<Request> fromRequests)
{
    return (await Task.WhenAll(fromRequests.Select(GetResponse))).ToList().AsReadOnly();
}

async Task<Response> GetResponseAsync(Request fromRequest)
{
    await Task.Delay(10000); //simulate some long running async op.
    return new Response();
}

Notes:

  • I know it's an example, but there's probably no reason to use Task.Run instead of a simple async call.
  • The convention is to name async methods with an "Async" suffix (i.e. GetResponseAsync).
  • I've also pluralized the name of the method that returns a collection.
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • This is exactly how the framework does it. The main problem with OP's implementation is what you've described with the need to switch things around. It should be noted that once THAT is accomplished, OP can then pull out some code from the outer async method into a non-async "convenience" method as needed. A good example of what those methods could look like is Web API, with its `ExceptionLogger` and `ExceptionHandler` classes. – moarboilerplate Jan 30 '15 at 20:55
  • Great answer, thanks. Can you elaborate on not using `Task.Run`? Not sure I follow. Also, I'm aware of the "Async" suffix but am confused as to when it's appropriate. MS seems to be lazy about using it (e.g. `Task.Delay` instead of `Task.DelayAsync`). Finally, the pluralization issue was a typo on my part :) – rory.ap Jan 30 '15 at 21:07
  • 1
    @roryap `Task.Run` offloads work to the `ThreadPool`. You don't seem to need a different thread than the one you're currently using. `Task.Run` is mostly used for offloading work out of "important" threads (like the UI threads) or to parallelize CPU intensive work by utilizing multiple threads. – i3arnon Jan 30 '15 at 21:10
  • So when you say "instead of a simple `async` call" you mean an `async` lambda? Doesn't that also offload to the `ThreadPool`? – rory.ap Jan 30 '15 at 21:13
  • @roryap first of all, there's no difference between an `async` method and any type of `async` anonymous delegate (like a lambda expression). And no, `async` methods run synchronously on the calling thread until an `await` is reached, then the rest is registered as a continuation (which will run on the `ThreadPool` as long as there isn't a SynchronizationContext telling it otherwise). – i3arnon Jan 30 '15 at 21:15
  • @roryap sure.. any time. – i3arnon Jan 30 '15 at 21:17
0

I'm still sticking with I3arnon's answer because it's a well-written, informative answer, but I'd like to submit my own answer because I realized that I was almost there. Here's the async convenience method I was struggling to find:

//convenience method
public static async Task<Response> GetResponse(Request fromRequest)
{
    return (await GetResponses(new Request[] { fromRequest }))[0];
}
Community
  • 1
  • 1
rory.ap
  • 34,009
  • 10
  • 83
  • 174