1

We ran into a bug where we had to validate a list of objects with an async method. The writer of the code wanted to just stuff it into a Linq expression like so:

var invalidObjects = list
      .Where(x => _service.IsValidAsync(x).Result)
      .ToList();

The validating method looked something like this:

public async Task<bool> IsValidAsync(object @object) {
  var validObjects = await _cache.GetAsync<List<object>>("ValidObjectsCacheKey");

  return validObjects.Contains(@object);
}

This little solution caused the whole application to hang on the await _cache.GetAsync line. The cache is a distributed cache (redis). After changing the linq to a simple foreach and properly awaiting _service.IsValidAsync, the code ran deadlock-free and basically in an instant.

I understand on a basic level how async-await works, but I can't wrap my head around why this happened especially because the list only had one object.

Any suggestion is welcome!

EDIT: The application is running on .net Core 2.2, but the library in which the problem happened is targeting .netstandard 2.0. System.Threading.SynchronizationContext.Current returns null at the time of the deadlock

EDIT2: It turns changing the cache provider (but still accessing it via an async method) also resolves the issue, so the bug might actually be in the redis cache client: https://github.com/aspnet/Caching/blob/master/src/Microsoft.Extensions.Caching.StackExchangeRedis/RedisCache.cs

Tamás Szabó
  • 1,388
  • 11
  • 23

1 Answers1

1

Apart from the already stated issue of mixing async-await and blocking calls like .Result or .Wait()

Reference Async/Await - Best Practices in Asynchronous Programming

To summarize this second guideline, you should avoid mixing async and blocking code. Mixed async and blocking code can cause deadlocks, more-complex error handling and unexpected blocking of context threads. The exception to this guideline is the Main method for console applications, or—if you’re an advanced user—managing a partially asynchronous codebase.

Sometimes the simple approach, as you have already discovered, is to traverse the list and properly await the asynchronous function

For example

var invalidObjects = //...

foreach(var x in list){
    if(!(await _service.IsValidAsync(x)))
        invalidObjects.Add(x);
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • Yeah that's exactly what we did, and it worked. I'm more interested as why the original solution was bad. I'll read the doc you sent :) – Tamás Szabó Mar 05 '20 at 11:11
  • @TamásSzabó check included link – Nkosi Mar 05 '20 at 11:11
  • So basically the context was waiting for `_service.IsValidAsync(x).Result` to finish, which was waiting for `_cache.GetAsync` to finish, which was waiting for the context to give it resources? And we have full circle? – Tamás Szabó Mar 05 '20 at 11:15
  • In AspNet Core there is no synchronization context used. Means this deadlock won't happen. – János Pánczél Mar 05 '20 at 11:16
  • @JánosPánczél any other ideas then? – Tamás Szabó Mar 05 '20 at 11:17
  • If you don't play with SynchronizationContext (what is quiet tricky) or you maybe use vanilla Asp instead of Core, I can't see any scenario your code end up in deadlock. Edit: maybe copy the code to a console application and see if it works there. If it does, you might use vanilla Asp. – János Pánczél Mar 05 '20 at 11:26
  • @TamásSzabó does any of the code surrounding the shown snippet use await? – Nkosi Mar 05 '20 at 11:27
  • If this is really ASP.NET Core, there's no SynchronizationContext, unless one was added by some library. – Paulo Morgado Mar 05 '20 at 12:20
  • I've removed the quote mentioning SynchronizationContext to avoid any confusion – Nkosi Mar 05 '20 at 12:32
  • It definitely is dotnet core. @Nkosi The whole method which contains the snippet is async. – Tamás Szabó Mar 05 '20 at 12:45
  • @TamásSzabó can you run the exact same code from console application or unit test? Even by simply calling the controller's action in a unit test or copy the whole code into a console application. If it runs fine, somewhere, something adds SynchronizationContext into the process. – János Pánczél Mar 05 '20 at 13:26
  • @JánosPánczél Sure, I'll post the results – Tamás Szabó Mar 05 '20 at 13:41