在C#中运行异步foreach循环,使用async和await。

6
我很难理解c# async await的基本概念。
基本上,我有一个对象列表需要处理,处理包括遍历其属性并连接字符串,然后创建一个新对象(在这种情况下称为trellocard),最终添加一个trellocards列表。
迭代需要相当长的时间,所以我想异步地处理多个对象。
我尝试了多种方法,但基本上我想做的是这样的。 (在下面的示例中,我已删除处理,并只放置system.threading.thread.sleep(200)。我等待这不是异步方法,我可以使用tasks.delay,但重点是我的处理没有任何异步方法,我想运行整个方法与多个实例。
    private async Task<List<TrelloCard>> ProcessJobs(IQueryable<IGrouping<CardGrouping, Job>> jobs)
    {
        List<TrelloCard> cards = new List<TrelloCard>();

        foreach (var job in jobs.ToList())
        {
           card = await ProcessCards(job, cards);  // I would like to run multiple instances of the processing
           cards.add(card); //Once each instance is finshed it adds it to the list
        }


  private async Task<TrelloCard> ProcessCards(Job job)
    {
        System.Threading.Thread.Sleep(2000);  //Just for examples sake

        return new TrelloCard();
    }
3个回答

8
我很难理解c#异步等待的基本概念。
简单来说,异步等待是.Net并发的一部分,可以用于进行多个IO调用,在进程中不浪费计算操作所需的线程。就像对数据库、Web服务、网络调用、文件IO等的调用,这些都不需要当前进程线程。
在您目前的情况下,使用情况为:
1.遍历其属性并连接字符串,然后创建一个新对象
2.最终添加trellocards列表
这似乎是一个计算绑定的操作,除非你正在进行IO,否则对我来说似乎你正在遍历内存中的对象,对于这种情况,更好的选择是:
  1. Parallel.ForEach,用于并行化内存处理,但需要注意竞态条件,因为同一块内存可能会被多个线程访问,特别是在写操作期间容易损坏,因此在当前代码中至少使用线程安全集合,如来自System.Collections.Concurrent命名空间的ConcurrentBag,或者适合用例的任何其他集合,而不是List<TrelloCard>,或者您可以考虑使用线程安全列表

此外,请注意,如果您的方法默认情况下不是Async,则可以计划将它们包装在Task.Run中,以进行await,尽管这将需要一个线程池线程,但可以使用Async-Await来调用

Parallel.Foreach 代码适用于您的用例(我正在进行直接替换,因为您的代码似乎存在问题,由于 ProcessCards 函数只接受 Job 对象,但是您也传递了集合 Cards,这会导致编译错误):

 private List<TrelloCard> ProcessJobs(IQueryable<IGrouping<CardGrouping, Job>> jobs)
    {
        ConcurrentBag<TrelloCard> cards = new ConcurrentBag<TrelloCard>();

        Parallel.ForEach(jobs.ToList(), (job) => 
        {
           card = ProcessCards(job);  // I would like to run multiple instances of the processing
           cards.Add(card); //Once each instance is finshed it adds it to the list
        });

           return cards.ToList();
   }

  private TrelloCard ProcessCards(Job job)
    {
       return new TrelloCard();
    }

谢谢!太神奇了,我在网上四处寻找答案,而你的前三行代码让我豁然开朗。现在这一切对我来说都更加清晰了。 - Michael
欢迎,对于 Web 场景来说,它们就像 Ajax 调用一样,但有一些微妙的差别,这些差别是特定于 .Net 的。 - Mrinal Kamboj
在上述情况下,如果我们需要同时显示一些繁忙指示器,我们该如何做呢?在我的情况下,繁忙指示器会卡住或出现闪烁效果。有没有人有解决方案或任何提示? - Suchith
@Suchith是可行的,只是像繁忙指示器这样的Ui控件只能在Ui线程上执行,而不是在线程池线程上执行,请查看此链接:(https://www.codeproject.com/Articles/567356/Async-Await-in-WPF-to-invoke-WCF-with-Busy-Indicat) - Mrinal Kamboj

7

如果你想让它们并行运行,你可以为每个操作生成一个新的任务,然后使用Task.WhenAll等待所有任务完成。

private async Task<List<TrelloCard>> ProcessJobs(IQueryable<IGrouping<CardGrouping, Job>> jobs)
{
    List<Task<TrelloCard>> tasks = new List<Task<TrelloCard>>();

    foreach (var job in jobs)
    {
        tasks.Add(ProcessCards(job));
    }

    var results = await Task.WhenAll(tasks);

    return results.ToList();
}


private Task<TrelloCard> ProcessCards(Job job)
{
    return Task.Run(() =>
    {
        System.Threading.Thread.Sleep(2000);  //Just for examples sake

        return new TrelloCard();
    });
}

在这里,你可能想要避免使用jobs.ToList(),因为它将不再是一个远程操作,并且所有的数据将加载到进程内存中,因此计算密集型操作比IO密集型操作更合适。实际上,这会破坏异步等待的目的。 - Mrinal Kamboj
@MrinalKamboj 是的,使用 async/await 编写此代码(计算密集型)的唯一原因是如果它是 WPF/WinForms 并且会阻塞用户界面。在 ASP.NET 应用程序中,计算密集型工作不应使用 async/await。 - Alex Wiese
即使在WPF中,您也可以在Threadpool线程上启动调用,不需要使用Async-Await,除非您计划更新Ui控件,这始终需要在Ui线程上下文中完成,否则会引发异常(使用Async-Await很容易实现,因为没有线程)。理想情况下,不应该使用Async-Await来处理计算密集型工作的应用程序逻辑,它仅设计用于IO调用。 - Mrinal Kamboj
@MrinalKamboj “等待计算密集型操作总是值得的”。没错,你刚刚证明了我的观点 - 因此需要使用 await 关键字。 - Alex Wiese
让我们在聊天中继续这个讨论 - Alex Wiese
显示剩余4条评论

1
"

jobs.ToList() 只是浪费内存。它已经是 IEnumerable,所以可以在 foreach 中使用。

ProcessCards 无法编译。你需要像下面这样做:

"
    private Task<TrelloCard> ProcessCards(Job job)
    {
        return Task.Run(() =>
        {
            System.Threading.Thread.Sleep(2000);  //Just for examples sake

            return new TrelloCard();
        });
    }

现在你想要 ProcessJobs
  • create a ProcessCards task for each job
  • wait for all tasks to finish
  • return a sequence of TrelloCard

    private async Task<List<TrelloCard>> ProcessJobs(IQueryable<IGrouping<CardGrouping, Job>> jobs)
    {
        return await Task.WhenAll(jobs.Select(ProcessCards));
    }
    

+1,正如我之前在评论中指出的那样,对于Async-Await的使用,重要的是不要做jobs.ToList(),因为一旦它被带入本地内存,那么它就可以成为简单的并行处理而不是异步处理。 - Mrinal Kamboj

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接