0

Assuming I have an async builder method and I need to convert a collection of db models into a collection of UI models, I wrote the code below (simplified for example).

After reading this medium article, the code was refactoring to bubble ASYNC all the way up but I'd still like to know

  1. will collection.GetConsumingEnumerable() block, leading to deadlocks, similar to .Wait or .Result
  2. what is the preferred way to stream items in an async method
    public IEnumerable<GenericDevice> GetGenericDevices(SearchRequestModel srModel)
    {
        var collection = new BlockingCollection<GenericDevice>(new ConcurrentBag<GenericDevice>());
        Task.Run(() => QueueGenericDevices(collection, srModel));
    
        foreach (var genericDevice in collection.GetConsumingEnumerable())
        {
               yield return genericDevice;
        }
    }
    
    private async Task QueueGenericDevices(BlockingCollection<GenericDevice> collection, SearchRequestModel srModel)
    {
        var efDevices = _dbDeviceRepo.GetEfDevices(srModel);
    
        var tasks = efDevices
               .Select(async efDevice =>
               {
                     var genericDevice = await BuildGenericDevice(efDevice, srModel);
                     collection.Add(genericDevice);
               });
    
        await Task.WhenAll(tasks);
        collection.CompleteAdding();
    }
Brantley Blanchard
  • 1,208
  • 3
  • 14
  • 23
  • If you target .NET Core 3+, then it's possible to use async enumerators. – Evk Nov 13 '20 at 15:39
  • 1
    Related: [Is there anything like asynchronous BlockingCollection?](https://stackoverflow.com/questions/21225361/is-there-anything-like-asynchronous-blockingcollectiont) – Theodor Zoulias Nov 13 '20 at 16:12

1 Answers1

1

will collection.GetConsumingEnumerable() block, leading to deadlocks, similar to .Wait or .Result

No. There are two parts to the classic deadlock:

  1. A context that only allows one thread at a time (usually a UI SynchronizationContext or ASP.NET pre-Core SynchronizationContext).
  2. Blocking on code that uses await (which uses that context in order to complete the async method).

GetConsumingEnumerable is a blocking call, but it's not blocking on asynchronous code, so there's no danger of the classic deadlock there. More generally, using a queue like this inserts a "barrier" of sorts between two pieces of code.

what is the preferred way to stream items in an async method

Modern code should use IAsyncEnumerable<T>.

However, if you're not on that platform yet, you can use a queue as a "barrier". You wouldn't want to use BlockingCollection<T>, though, because it is designed for synchronous producers and synchronous consumers. For asynchronous producers/consumers, I'd recommend Channels.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Thanks Stephen, you've got a lot of great information on your blog. When you get some time, I'd love to see a blog with your three examples: async enumerable, queue, and channels. – Brantley Blanchard Nov 30 '20 at 14:21
  • On the blocking collection note, I did indeed run into an issue of completing early. I had to add `await Task.WhenAll(tasks);` to block until all adds were complete. Is that the issue you are referring to? – Brantley Blanchard Nov 30 '20 at 14:26
  • @BrantleyBlanchard: No; I mean that `GetGenericDevices` is now a synchronous method that is (presumably) called on the UI thread. So a `foreach` over its enumerable will just block the UI. You can use Channels instead, where the consumption pattern includes `await`ing for more items to be available - that way, the UI can remain responsive. – Stephen Cleary Nov 30 '20 at 14:41