IAsyncEnumerable should not implement IEnumerable
TomPeters opened this issue · 5 comments
I have a suggestion about the API based on an issue I have encountered using this library.
In reference to the following two interfaces:
public interface IAsyncEnumerable : IEnumerable
public interface IAsyncEnumerable<T> : IEnumerable<T>, IAsyncEnumerable
Currently they implement IEnumerable
and IEnumerable<T>
. I don't think they should.
With the current implementation, it is easy to pass an IAsyncEnumerable
around as an IEnumerable
. If you are in an asp.net request and are therefore using the asp.net synchronization context, you could create a deadlock if you use an AsyncEnumerable
as an IEnumerable
.
There are two obvious solutions to this problem:
- Ensure that you use ConfigureAwait(false) everywhere (and hope that you don't need
HttpContext
). - Do not use the
AsyncEnumerable
as anIEnumerable
(in order to go 'async all the way').
Both of these approaches are error prone, as there is no guarantee you have done either of them correctly, nor is there a good way of tracking down all of the appropriate places that need to be checked.
I suggest changing the IAsyncEnumerable
interfaces such that they do not implement IEnumerable
to avoid such issues.
I can see that it is very useful to be able to use an IAsyncEnumerable
as an Enumerable
in many cases, so I can see why it was done this way. But perhaps it would be better for this to be an extension method to transform an IAsyncEnumerable
into an IEnumerable
?
eg:
public static IEnumerable<T> ToEnumerable(this IAsyncEnumerable<T> asyncEnumerable)
This has two advantages:
- It makes it explicit that you want to enumerate synchronously
- It makes it easy to track down places that you are enumerating synchronously.
Point 2 here is particularly useful for the deadlock scenario: If you are find a usage of this extension method within an asp.net request, then you know there is a deadlock risk and you may want to instead enumerate asynchronously, and propagate the async all the way to your entry point.
I understand the struggle with putting obnoxious ConfigureAwait(false)
, but this is the only right solution to make ASP.NET or WPF happy. The AsyncEnumerable
internally always does Task.ConfigureAwait(false).GetAwaiter().Result
on synchronous enumeration to avoid dead-locks on SynchronizationContext
, but this will work properly only if async enumeration function (that is passed into the constructor) does ConfigureAwait(false)
on downstream async calls. I.e:
// this might hang on synchronous enumeration in ASP.NET/WPF
new AsyncEnumerable(async yield =>
{
await FooAsync();
await yield.ReturnAsync(123);
})
// this will not hang on synchronous enumeration in ASP.NET/WPF
new AsyncEnumerable(async yield =>
{
await FooAsync().ConfigureAwait(false);
await yield.ReturnAsync(123).ConfigureAwait(false);
})
On the other hand, I agree that doing synchronous calls on asynchronous methods just defies the whole purpose of proper use of the thread pool and IO port completion threads. So I'll consider the suggestion and keep this question open for now.
I understand the struggle with putting obnoxious ConfigureAwait(false), but this is the only right solution to make ASP.NET or WPF happy.
While I certainly get that putting ConfigureAwait(false)
in the AsyncEnumerable
enumerable library is a good idea, it is not the only right solution for people writing ASP.NET or WPF apps - the other solution is to use async all the way. If someone were to adopt that strategy in their own code, they might correctly omit the ConfigureAwait(false)
s that you demonstrated in your example. And in this case, accidental synchronous enumeration of an AsyncEnumerable
would necessarily deadlock :(.
The AsyncEnumerable internally always does Task.ConfigureAwait(false).GetAwaiter().Result on synchronous enumeration...
As a side note, I'm fairly sure that the ConfigureAwait(false)
in this expression is redundant and does nothing (since there is no await
to configure)
So I'll consider the suggestion and keep this question open for now.
👍
As a side note, I'm fairly sure that the ConfigureAwait(false) in this expression is redundant and does nothing (since there is no await to configure)
You are absolutely right. That affects only the task continuation: to use or not to use the SynchronizationContext
for scheduling.
@TomPeters, this is a good suggestion, because thread blocking is just harmful and defeats the purpose of this library.
Now you need to explicitly do IAsyncEnumerable.ToEnumerable()
for conversion.
The change is available in version 2.0.0.
Please see updated documentation section about the changes in version 2.
Looks good, thanks!