5

I've been struggling with async / await for a week now. Some background: the code below is part of an MVC4 website project. The website has a large number of API calls happening. The goal is to get those API calls happening in parallel instead of synchronous to improve site responsiveness. Right now all the API calls block each other. So if one page requires 4 calls...longer load time.

I've built out individual methods for both the synchronous and async versions of all the API calls. The problem I have is the await never responds. I think it's related to this question. However, I'm really not sure how to solve it. I've tried the ConfigureAwait(false), and that hasn't helped me.

Here's the code:

The initial call in the controller it looks like so:

BaseData bdata = API.GetBaseData().Result;

I'd love to use await here, but that's not an option without an AsyncController, which we can't use due to needing request / response access. The other methods are in the API class:

internal static async Task<BaseData> GetBaseData() {
    var catTask = GetParentCategoriesAsync();
    var yearTask = GetYearsAsync();

    await Task.WhenAll(new Task[] { catTask, yearTask });

    var bdata = new BaseData {
        years = await yearTask,
        cats = await catTask
    };
    return bdata;
}

internal static async Task<List<APICategory>> GetParentCategoriesAsync() {
    try {
        WebClient wc = new WebClient();
        wc.Proxy = null;

        string url = getAPIPath();
        url += "GetFullParentCategories";
        url += "?dataType=JSON";

        Uri targeturi = new Uri(url);
        List<APICategory> cats = new List<APICategory>();

        var cat_json = await wc.DownloadStringTaskAsync(targeturi);
        cats = JsonConvert.DeserializeObject<List<APICategory>>(cat_json);

        return cats;
    } catch (Exception) {
        return new List<APICategory>();
    }
}

internal static async Task<List<double>> GetYearsAsync() {
    WebClient wc = new WebClient();
    wc.Proxy = null;
    Uri targeturi = new Uri(getAPIPath() + "getyear?dataType=JSON");
    var year_json = await wc.DownloadStringTaskAsync(targeturi);
    List<double> years = JsonConvert.DeserializeObject<List<double>>(year_json);
    return years;
}

When these methods are called, I can put breakpoints in GetYearsAsync() and GetParentCategoriesAsync(). Everything fires until the await wc.DownloadStringTaskAsync(targeturi) command. That's where stops.

I've added ConfigureAwait(continueOnCapturedContext: false) to all the tasks, but that hasn't helped. I'm assuming the problem is that the threads are not in the same context. However, I'm not certain. I am certain, however, that I'm doing something wrong. I'm just not sure what. It's either that or I'm just trying to do something that can't be done with .NET MVC4. Any thoughts would be supremely appreciated.

Community
  • 1
  • 1
janiukjf
  • 63
  • 1
  • 6
  • 5
    If you can't "async all the way up" then there's not really much point of using async methods at all. If you're going to be blocking on the task at the top level you're still keeping a thread pool thread busy for the entire duration of your operation. You may as well just use non-async methods throughout. – Servy Jun 07 '13 at 17:11
  • If you're performing long-running operations in the background, you need a service, something that lives beyond the HTTP request. – Robert Harvey Jun 07 '13 at 17:11
  • 3
    "which we can't use due to needing request / response access" - not sure what you mean by this? `async` code can access the request and response just fine. – Stephen Cleary Jun 07 '13 at 17:12
  • 3
    `Task.Result` when asynchrony is involved will almost always deadlock. – SLaks Jun 07 '13 at 17:15
  • As Servy pointed out, there's no benefit to `async` if you're just going to block a thread by calling `Result`. That said, `ConfigureAwait` *should* work if you are `await`ing the result of `ConfigureAwait` *instead* of the task directly, and if you have [targetFramework or UseTaskFriendlySynchronizationContext](http://blogs.msdn.com/b/webdev/archive/2012/11/19/all-about-httpruntime-targetframework.aspx) set appropriately. – Stephen Cleary Jun 07 '13 at 17:15
  • You don't need `AsyncController`. – SLaks Jun 07 '13 at 17:15
  • I don't know that I agree with the statement that some blocking methods make async a waste of time. Just because some methods are blocking doesn't mean that async won't still improve response time. I've used this type of methodology in other languages, and it's been fantastic. That said, When I make the controller method async, HttpContext.Current becomes null to any method called inside that controller method. So, if I need cookie access, for example, there's no way to get the cookie data then. That's a problem for this particular app. – janiukjf Jun 07 '13 at 17:32
  • @janiukjf The performance improvement from using `async` comes from not blocking a thread. So, if you use `async` and block a thread anyway, you're not going to get any performance improvement. – svick Jun 07 '13 at 17:42
  • @svick The main thread is blocked until the await returns, yes, but the await is waiting for two simultaneous API method calls. If those were done synchronously, it will take longer. In some cases in this app, a page load will require 5 API calls. If 5 API calls are happening async, it's going to be faster then if each individual API call was syncronously done...even if the whole await blocks the rest. Either way, I think I need to restructure a bunch of things to make this work the way I want it to work. I ultimately want async all the way. The HttpContext.Current issue is the real problem. – janiukjf Jun 07 '13 at 17:52
  • @janiukjf Oh, I didn't actually look at your code, so I didn't see that's what you're doing. In that case, you could keep your code synchronous, but parallelize it. This means you will block two threads instead of one, but keeps the advantages of concurrency without the complications of `async`. – svick Jun 07 '13 at 18:12
  • @svick That's essentially the goal. I've been doing async / parallel programming in golang for a while now, which has totally spoiled me. .NET has been kicking my ass when it comes to parallel tasks. I've had trouble figuring out the best approach here. Any pointers to examples that might be fruitful? – janiukjf Jun 07 '13 at 19:06

2 Answers2

2

The problem is actually due to WebClient, which will always sync back to the request context (which is blocked due to the Result call).

You can use HttpClient instead, combined with ConfigureAwait(false):

internal static async Task<BaseData> GetBaseDataAsync() {
  var catTask = GetParentCategoriesAsync();
  var yearTask = GetYearsAsync();

  await Task.WhenAll(catTask, yearTask).ConfigureAwait(false);

  var bdata = new BaseData {
    years = await yearTask,
    cats = await catTask
  };
  return bdata;
}

internal static async Task<List<APICategory>> GetParentCategoriesAsync() {
  try {
    var client = new HttpClient();

    string url = getAPIPath();
    url += "GetFullParentCategories";
    url += "?dataType=JSON";

    Uri targeturi = new Uri(url);
    List<APICategory> cats = new List<APICategory>();

    var cat_json = await client.GetStringAsync(targeturi).ConfigureAwait(false);
    cats = JsonConvert.DeserializeObject<List<APICategory>>(cat_json);

    return cats;
  } catch (Exception) {
    return new List<APICategory>();
  }
}

internal static async Task<List<double>> GetYearsAsync() {
  var client = new HttpClient();
  Uri targeturi = new Uri(getAPIPath() + "getyear?dataType=JSON");
  var year_json = await client.GetStringAsync(targeturi).ConfigureAwait(false);
  List<double> years = JsonConvert.DeserializeObject<List<double>>(year_json);
  return years;
}

That should enable you to call it as such:

BaseData bdata = API.GetBaseDataAsync().Result;

However, I strongly recommend that you call it as such:

BaseData bdata = await API.GetBaseDataAsync();

You'll find that the code both before and after the await can access the request and response context just fine.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
0

I ended up following a combination of Servy's and Stephen Cleary's advice. Based on Stephen's response, I realized I could access the request/response as long as I did it before I made any asynchronous calls in the controller.

If I stored the HttpContext in a variable, I could pass that context around to any model/service method that needed access to it. This allowed me to just go async all the way up like Servy suggested. After that, I had no issues with anything I wanted with the async patterns.

4444
  • 3,541
  • 10
  • 32
  • 43
janiukjf
  • 63
  • 1
  • 6
  • 2
    Passing `HttpContext` in a variable to other threads that shouldn't have access to it is not recommended, to say the least. Just use `async` and `await` (without `ConfigureAwait`) and you will be able to access `HttpContext.Current` just fine both before *and after* the `await`. – Stephen Cleary Aug 07 '13 at 14:01
  • The big problem that I was running into initially with just plain old async / await was that HttpContext.Current was empty in helper methods that needed to get and set cookie data. With just async and await, any method call made would throw null reference errors when trying to access HttpContext.Current. By storing HttpContext.Current in a variable, I can pass it around much like I've seen modeled in other languages. Without storing it as a variable, the cookie logic wouldn't work at all. I guess there has to be something I'm still missing then. – janiukjf Aug 07 '13 at 17:58
  • That is a different problem then. I recommend posting a minimal repro with the question of why `HttpContext.Current` is `null`. – Stephen Cleary Aug 07 '13 at 18:03