6
\$\begingroup\$

LINQ Select usually works like this:

IEnumerable<Task<int>> res = new[] {1,2,3}.Select(async i => i);

I would like to have the following though:

IEnumerable<int> res = new[] {1,2,3}.Select(async i => i);

This extension method helps to get it:

static class SelectAsync
{
    public static IEnumerable<TResult> Select<T, TResult>(
        this IEnumerable<T> source, Func<T, Task<TResult>> selector)
    {
        return source
            .ToObservable()
            .Select(value => Observable.FromAsync(() => selector(value)))
            .Concat()
            .ToEnumerable();
    }
}

So the following is true:

public class AsyncSelect_Should
{
    [Test]
    public void Not_Block()
    {
        var src = new[] { 1, 2, 3 };
        var res = src.Select(async i => i);
        CollectionAssert.AreEqual(src, res);
    }
}

There should be something bad, really bad about it. Does it? :)

\$\endgroup\$
4
\$\begingroup\$

The bad part is that ToEnumerable returns an IEnumerable that blocks for each element until it is produced by the IObservable it is called on. This means that you might as well be doing:

public static IEnumerable<R> SelectAsync<T, R>(this IEnumerable<T> source, Func<T, Task<R>> selector)
{
    foreach (var item in source)
        yield return selector(item).Result;
}

and depending on what kind of app you're writing, this might deadlock, though I think your version of going through Rx might avoid that and be more functionality close to:

public static IEnumerable<R> SelectAsync<T, R>(this IEnumerable<T> source, Func<T, Task<R>> selector)
{
    foreach (var item in source)
        yield return Task.Run(() => selector(item)).Result;
}

Either way, you're blocking a Thread which is seen as a bad thing to do in the age of async/await and the TPL. It would be as bad as using Thread.Sleep with Task.Run. It is not likely to lock up your app, especially if your app is multi-threaded, but it most certainly will block a Thread on the thread pool.

I've certainly seen people get away with using Thread.Sleep with Task.Run with no real consequences, but keep in mind that it can cause issues when you scale.

As for how you might know this without being intimately familiar with Rx and the details of how its operators work? That's easy. Just know that you can never go from having an asynchronous value (Task<T>) to having a synchronous value (T) without awaiting (which requires that you stay in an async context) or blocking. Even Rx doesn't have enough magic to change that :)

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.