简体   繁体   English

linq 选择中的异步等待

[英]Async await in linq select

I need to modify an existing program and it contains following code:我需要修改一个现有的程序,它包含以下代码:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

But this seems very weird to me, first of all the use of async and await in the select.但这对我来说似乎很奇怪,首先在选择中使用asyncawait According to this answer by Stephen Cleary I should be able to drop those.根据 Stephen Cleary 的这个答案,我应该可以放弃这些。

Then the second Select which selects the result.然后第二个Select选择结果。 Doesn't this mean the task isn't async at all and is performed synchronously (so much effort for nothing), or will the task be performed asynchronously and when it's done the rest of the query is executed?这是否意味着该任务根本不是异步的而是同步执行的(付出这么多努力是白费力气的),还是该任务会异步执行并在完成后执行其余的查询?

Should I write the above code like following according to another answer by Stephen Cleary :我是否应该根据Stephen Cleary 的另一个答案编写如下代码:

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

and is it completely the same like this?是完全一样的吗?

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

While i'm working on this project I'd like to change the first code sample but I'm not too keen on changing (apparantly working) async code.当我在这个项目上工作时,我想更改第一个代码示例,但我不太热衷于更改(显然正在工作)异步代码。 Maybe I'm just worrying for nothing and all 3 code samples do exactly the same thing?也许我只是无所事事,所有 3 个代码示例都做完全相同的事情?

ProcessEventsAsync looks like this: ProcessEventsAsync 看起来像这样:

async Task<InputResult> ProcessEventAsync(InputEvent ev) {...}
var inputs = events.Select(async ev => await ProcessEventAsync(ev))
                   .Select(t => t.Result)
                   .Where(i => i != null)
                   .ToList();

But this seems very weird to me, first of all the use of async and await in the select.但这对我来说似乎很奇怪,首先在选择中使用 async 和 await 。 According to this answer by Stephen Cleary I should be able to drop those.根据 Stephen Cleary 的这个答案,我应该可以放弃这些。

The call to Select is valid.Select的调用是有效的。 These two lines are essentially identical:这两行本质上是相同的:

events.Select(async ev => await ProcessEventAsync(ev))
events.Select(ev => ProcessEventAsync(ev))

(There's a minor difference regarding how a synchronous exception would be thrown from ProcessEventAsync , but in the context of this code it doesn't matter at all.) (关于如何从ProcessEventAsync抛出同步异常存在细微差别,但在此代码的上下文中,这根本无关紧要。)

Then the second Select which selects the result.然后第二个 Select 选择结果。 Doesn't this mean the task isn't async at all and is performed synchronously (so much effort for nothing), or will the task be performed asynchronously and when it's done the rest of the query is executed?这是否意味着该任务根本不是异步的而是同步执行的(付出这么多努力是白费力气的),还是该任务会异步执行并在完成后执行其余的查询?

It means that the query is blocking.这意味着查询正在阻塞。 So it is not really asynchronous.所以它并不是真正的异步。

Breaking it down:分解它:

var inputs = events.Select(async ev => await ProcessEventAsync(ev))

will first start an asynchronous operation for each event.将首先为每个事件启动一个异步操作。 Then this line:然后这一行:

                   .Select(t => t.Result)

will wait for those operations to complete one at a time (first it waits for the first event's operation, then the next, then the next, etc).将等待这些操作一次完成一个(首先它等待第一个事件的操作,然后是下一个,然后是下一个,等等)。

This is the part I don't care for, because it blocks and also would wrap any exceptions in AggregateException .这是我不关心的部分,因为它会阻塞并且还会在AggregateException包装任何异常。

and is it completely the same like this?是完全一样的吗?

var tasks = await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev)));
var inputs = tasks.Where(result => result != null).ToList();

var inputs = (await Task.WhenAll(events.Select(ev => ProcessEventAsync(ev))))
                                       .Where(result => result != null).ToList();

Yes, those two examples are equivalent.是的,这两个例子是等价的。 They both start all asynchronous operations ( events.Select(...) ), then asynchronously wait for all the operations to complete in any order ( await Task.WhenAll(...) ), then proceed with the rest of the work ( Where... ).它们都启动所有异步操作( events.Select(...) ),然后异步等待所有操作以任何顺序完成( await Task.WhenAll(...) ),然后继续其余的工作( Where... )。

Both of these examples are different from the original code.这两个示例都与原始代码不同。 The original code is blocking and will wrap exceptions in AggregateException .原始代码是阻塞的,并将在AggregateException包装异常。

Existing code is working, but is blocking the thread.现有代码正在运行,但阻塞了线程。

.Select(async ev => await ProcessEventAsync(ev))

creates a new Task for every event, but为每个事件创建一个新任务,但是

.Select(t => t.Result)

blocks the thread waiting for each new task to end.阻塞线程等待每个新任务结束。

In the other hand your code produce the same result but keeps asynchronous.另一方面,您的代码产生相同的结果,但保持异步。

Just one comment on your first code.对你的第一个代码只有一个评论。 This line这条线

var tasks = await Task.WhenAll(events...

will produce a single Task<TResult[]> so the variable should be named in singular.将产生一个 Task<TResult[]> 所以变量应该以单数命名。

Finally your last code make the same but is more succinct.最后,您的最后一个代码相同,但更简洁。

For reference: Task.Wait / Task.WhenAll供参考: Task.Wait / Task.WhenAll

I used this code:我使用了这个代码:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource,TResult>(
    this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method)
{
  return await Task.WhenAll(source.Select(async s => await method(s)));
}

like this:像这样:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params));

Edit:编辑:

Some people have raised the issue of concurrency, like when you are accessing a database and you can't run two tasks at the same time.有些人提出了并发问题,比如当你访问一个数据库时,你不能同时运行两个任务。 So here is a more complex version that also allows for a specific concurrency level:所以这是一个更复杂的版本,它也允许特定的并发级别:

public static async Task<IEnumerable<TResult>> SelectAsync<TSource, TResult>(
    this IEnumerable<TSource> source, Func<TSource, Task<TResult>> method,
    int concurrency = int.MaxValue)
{
    var semaphore = new SemaphoreSlim(concurrency);
    try
    {
        return await Task.WhenAll(source.Select(async s =>
        {
            try
            {
                await semaphore.WaitAsync();
                return await method(s);
            }
            finally
            {
                semaphore.Release();
            }
        }));
    } finally
    {
        semaphore.Dispose();
    }
}

Without a parameter it behaves exactly as the simpler version above.如果没有参数,它的行为与上面更简单的版本完全相同。 With a parameter of 1 it will execute all tasks sequentially:参数为 1 时,它将按顺序执行所有任务:

var result = await sourceEnumerable.SelectAsync(async s=>await someFunction(s,other params),1);

Note: Executing the tasks sequentially doesn't mean the execution will stop on error!注意:按顺序执行任务并不意味着执行会在出错时停止!

Just like with a larger value for concurrency or no parameter specified, all the tasks will be executed and if any of them fail, the resulting AggregateException will contain the thrown exceptions.就像使用更大的并发值或未指定参数一样,所有任务都将被执行,如果其中任何一个任务失败,则生成的 AggregateException 将包含抛出的异常。

If you want to execute tasks one after the other and fail at the first one, try another solution, like the one suggested by xhafan ( https://stackoverflow.com/a/64363463/379279 )如果您想一个接一个地执行任务并在第一个失败,请尝试另一种解决方案,例如 xhafan 建议的解决方案( https://stackoverflow.com/a/64363463/379279

I prefer this as an extension method:我更喜欢将其作为扩展方法:

public static async Task<IEnumerable<T>> WhenAll<T>(this IEnumerable<Task<T>> tasks)
{
    return await Task.WhenAll(tasks);
}

So that it is usable with method chaining:这样它就可以与方法链一起使用:

var inputs = await events
  .Select(async ev => await ProcessEventAsync(ev))
  .WhenAll()

With current methods available in Linq it looks quite ugly:使用 Linq 中可用的当前方法,它看起来很丑陋:

var tasks = items.Select(
    async item => new
    {
        Item = item,
        IsValid = await IsValid(item)
    });
var tuples = await Task.WhenAll(tasks);
var validItems = tuples
    .Where(p => p.IsValid)
    .Select(p => p.Item)
    .ToList();

Hopefully following versions of .NET will come up with more elegant tooling to handle collections of tasks and tasks of collections.希望后续版本的 .NET 将提供更优雅的工具来处理任务集合和集合任务。

I wanted to call Select(...) but ensure it ran in sequence because running in parallel would cause some other concurrency problems, so I ended up with this.我想调用Select(...)但确保它按顺序运行,因为并行运行会导致其他一些并发问题,所以我最终得到了这个。 I cannot call .Result because it will block the UI thread.我不能调用.Result因为它会阻塞 UI 线程。

public static class TaskExtensions
{
    public static async Task<IEnumerable<TResult>> SelectInSequenceAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> asyncSelector)
    {
        var result = new List<TResult>();
        foreach (var s in source)
        {
            result.Add(await asyncSelector(s));
        }
        
        return result;
    }
}

Usage:用法:

var inputs = events.SelectInSequenceAsync(ev => ProcessEventAsync(ev))
                   .Where(i => i != null)
                   .ToList();

I am aware that Task.WhenAll is the way to go when we can run in parallel.我知道 Task.WhenAll 是我们可以并行运行的方法。

I have the same problem as @KTCheek in that I need it to execute sequentially.我和@KTCheek 有同样的问题,我需要它按顺序执行。 However I figured I would try using IAsyncEnumerable (introduced in .NET Core 3) and await foreach (introduced in C# 8).但是我想我会尝试使用 IAsyncEnumerable(在 .NET Core 3 中引入)并等待 foreach(在 C# 8 中引入)。 Here's what I have come up with:这是我想出的:

public static class IEnumerableExtensions {
    public static async IAsyncEnumerable<TResult> SelectAsync<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, Task<TResult>> selector) {
        foreach (var item in source) {
            yield return await selector(item);
        }
    }
}

public static class IAsyncEnumerableExtensions {
    public static async Task<List<TSource>> ToListAsync<TSource>(this IAsyncEnumerable<TSource> source) {
        var list = new List<TSource>();

        await foreach (var item in source) {
            list.Add(item);
        }

        return list;
    }
}

This can be consumed by saying:这可以通过说:

var inputs = await events.SelectAsync(ev => ProcessEventAsync(ev)).ToListAsync();

Update: Alternatively you can add a reference to "System.Linq.Async" and then you can say:更新:或者,您可以添加对“System.Linq.Async”的引用,然后您可以说:

var inputs = await events
    .ToAsyncEnumerable()
    .SelectAwait(async ev => await ProcessEventAsync(ev))
    .ToListAsync();

"Just because you can doesn't mean you should." “仅仅因为你可以并不意味着你应该。”

You can probably use async/await in LINQ expressions such that it will behave exactly as you want it to, but will any other developer reading your code still understand its behavior and intent?您可能可以在 LINQ 表达式中使用 async/await 以使其行为完全符合您的要求,但是任何其他阅读您代码的开发人员仍能理解其行为和意图吗?

(In particular: Should the async operations be run in parallel or are they intentionally sequential? Did the original developer even think about it?) (特别是:异步操作应该并行运行还是故意按顺序运行?原始开发人员是否考虑过?)

This is also shown clearly by the question , which seems to have been asked by a developer trying to understand someone else's code, without knowing its intent. 问题也清楚地表明了这一点,这似乎是由试图理解他人代码的开发人员提出的,但不知道其意图。 To make sure this does not happen again, it may be best to rewrite the LINQ expression as a loop statement, if possible.为确保这种情况不再发生,最好将 LINQ 表达式重写为循环语句(如果可能)。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM