【问题标题】:Run multiple instances of same method asynchronously?异步运行同一方法的多个实例?
【发布时间】:2016-05-22 11:55:19
【问题描述】:

我的要求很奇怪。

我有SomeMethod(),它调用GetDataFor()

public void SomeMethod()
{
    for(int i = 0; i<100; i++) {
        var data = GetDataFor(i);
    }
}

public data GetDataFor(int i) {
    //call a remote API
    //to generate data for i
    //store to database
    return data;
}

对于每个i,最终结果将总是不同。在调用GetDataFor(i+1) 之前,无需等待GetDataFor(i) 完成。

换句话说,我需要:

  • 在成功调用i立即为每个i+1调用GetDataFor()(并行调用它们看起来不可能)
  • 等到所有 GetDataFor() 的 100 个实例完成运行
  • 离开SomeMethod()的范围

按照YK1's answer,我尝试过这样修改:

public async Task<void> SomeMethod()
{
    for(int i = 0; i < 100; i++) {
        var task = Task.Run(() => GetDataFor(i));
        var data = await task;
    }
}

它没有抛出任何错误,但我需要了解这背后的概念:

  • task 将如何区分对awaiting 的不同调用?它正在被覆盖。
  • 这样做是不是公然错误的方法?那么,如何做到正确呢?

【问题讨论】:

  • 您的代码不会并行运行它们。他们一个接一个地运行,因为你一直在等待完成。
  • @poke 明白了。我也不确定。我们如何纠正这个问题?
  • GetDataFor的主要组成部分是什么?是否受 CPU 限制?磁盘绑定?网络绑定?根据答案,您可能无论如何都不想并行运行它们。
  • 呃,Parallel.For?或 PLinq Enumerable.Range(0, 100).AsParallel().Select(i =&gt; GetDataFor(i));?
  • 这可能对你有帮助:stackoverflow.com/questions/17805887/…

标签: c# performance asynchronous async-await task


【解决方案1】:

你可以使用Parallel.For:

public void SomeMethod()
{
    Parallel.For(0, 100, i =>
    {
        var data = GetDataFor(i);
        //Do something
    });
}

public data GetDataFor(int i)
{
    //generate data for i
    return data;
}

编辑:

并行循环的语法与您已经知道的forforeach 循环非常相似,但并行循环在具有可用内核的计算机上运行得更快。另一个区别是,与顺序循环不同,并行循环没有定义执行顺序。步骤通常同时并行进行。有时,两个步骤的执行顺序与循环顺序相反。唯一的保证是所有循环的迭代都将在循环结束时运行。

对于并行循环,您的代码不需要指定并行度。相反,运行时环境在尽可能多的内核上同时执行循环的步骤。无论有多少内核可用,循环都能正常工作。如果只有一个核心,则性能接近(可能在几个百分点之内)顺序等效。如果有多个核心,性能会提高;在许多情况下,性能会随着内核数量的增加而提高。

你可以看到更详细的解释here

【讨论】:

  • 很好地呼吁使用并行性,OP 确实同时提到了。
  • 这只会根据您拥有的核心数量并行运行。如果你有 1 个核心,那么什么都不会改善。
  • @student - 不,它只是不会并行运行。它会像正常的 for 循环一样运行,所以没有优势。它基本上取决于核心的数量。因此,如果您有 2 个内核,它将在 1 上运行一次对 GetDataFor 的调用,并在另一个上运行下一次调用(并行运行)。然后它需要等到其中一个调用完成后再开始另一个。如果 GetDataFor 是一个运行时间很长的进程并且您只有 2 个内核,那么您可能只会看到接近 50% 的改进。
  • @student: 如果GetDataFor(i)GetDataFor(i+1) 完全独立并且不修改任何共享数据,则不存在线程安全问题。
  • @student:正如@YK1 所说,线程安全完全依赖于GetDataFor(i) 的实现。 Parallel.For 只是执行一个 for 循环,其中迭代可以并行运行。我编辑了我的答案以根据核心数量添加 Parallel.For 行为。
【解决方案2】:

有几种不同的方法。

首先,您可以让它保持同步,然后并行执行它们(在不同的线程上)。 Parallel LINQ 优于Parallel如果你想在调用方法中收集所有结果继续:

public data[] SomeMethod()
{
  return Enumerable.Range(0, 100)
      .AsParallel().AsOrdered()
      .Select(GetDataFor).ToArray();
}

其次,你可以让它异步。要使某些东西真正异步,您需要从最低级别开始(在这种情况下,“调用远程 API”和“存储到数据库”)并使其异步首先。然后你可以让GetDataFor异步:

public async Task<data> GetDataForAsync(int i)
{
  await .. //call a remote API asynchronously
  await .. //store to database asynchronously
  return data;
}

那么你也可以使SomeMethod异步:

public Task<data[]> SomeMethodAsync()
{
  return Task.WhenAll(
      Enumerable.Range(0, 100).Select(GetDataForAsync)
  );
}

使代码异步需要更多工作 - 更多代码必须更改 - 但在可扩展性和资源使用方面更好。

【讨论】:

    【解决方案3】:

    当使用asyncawait时,你实际上是在说“在等待这个任务完成的同时,请去做一些不依赖这个任务的独立工作”。由于您不关心等待 GetDataFor 完成,因此您并不想使用 async await

    This previous question 的请求似乎与您的请求非常相似。考虑到这一点,我认为您应该能够执行以下操作:

    public void SomeMethod()
    {
        Task.Run(() => GetDataFor(i));
    }
    

    基本上,这假设您无需等待 GetDataFor 完成即可执行任何其他操作,这实际上是“一劳永逸”。

    对于 Parallel.For,只要您拥有 1 个以上的内核,您就可能会看到性能有所提升。如果没有,您可能会看到性能略有下降(更多开销)。 Here's an article 这有助于解释它是如何工作的。

    更新

    根据您的评论,我会建议如下:

    var TList = new List<Task>();
    
    for (var i = 0; i < 100; i++)
    {
        TList.Add(Task.Run(() => GetDataFor(i)));
    }
    
    await Task.WhenAll(TList);     
    

    Here's a useful question 强调了为什么您可能希望使用 WhenAll 而不是 WaitAll。

    您可能希望对任务的完成状态进行一些检查,以查看哪些失败(如果有)。示例见here

    【讨论】:

    • 我需要等到所有GetDataFor() 运行完毕。
    • 另外,它会让事情变得沉重吗?我的视觉工作室无法处理 F5,我必须杀死它。
    【解决方案4】:

    我会改为将每个任务添加到一个集合中,然后在循环之后等待整个集合。

    在这样的循环内部等待会产生大量的延续和超出预期的开销,包括等待每个调用完成,然后再继续我相信的循环。

    改为查看 awaiting Task.WaitAll

    如果每个任务的值对处理很重要,则查看等待Task.WhenAll,然后将每个任务的结果读入您的返回集合。

    【讨论】:

    • @student,GetDataFor 本身是否返回数据?
    • 是的,它返回data
    • 我添加了处理该场景的 WhenAll 用例,但 @Arturo-Menchaca 可能会为您提供更好的案例,具体取决于您是否希望代码真正并排运行或只是非阻塞运行异步。
    【解决方案5】:

    代码实际上没有意义。

    任务如何区分不同的等待调用?它越来越 重写。

    它不会被覆盖。因为……

    for(int i = 0; i < 100; i++) {
        var task = Task.Run(() => GetDataFor(i));
        var data = await task;
    }
    

    这是在继续循环之前等待每个请求完成。 await 等待结束。

    这意味着整个任务是无关紧要的——这里没有并行发生。您可以通过在没有任务的情况下执行此操作来减少一些小开销。

    我怀疑 OP 想要实现一些他根本没有做到的事情,并且他没有花费足够的时间进行调试以意识到他再次单线程了整个循环。

    【讨论】:

    • 您对这个问题(“如何并行运行”)的回答是……?
    【解决方案6】:

    虽然您的原始代码会覆盖这些值,但您似乎正在尝试合并并行操作的结果。如果是这样,请考虑使用Task.ContinueWith 来处理返回值。您的代码将如下所示:

    public void SomeMethod()
        List<Task> tasks = new List<Task>();
        for (var i = 0; i < 100; i++)
        {
            tasks.Add(Task.Run(() => GetDataFor(i)).ContinueWith((antecedent) => {
                // Process the results here.
            }));
        }
        Task.WaitAll(tasks.ToArray());
    }
    

    【讨论】:

    • 这与使用 await 关键字没有什么不同。
    • @OvanCrone 这不是真的。所有任务都将异步运行。 OP 说 GetDataFor 被允许是异步的。 Task.WaitAll 阻止 SomeMethod 返回,直到一切都完成,如果这就是你的意思,但 OP 没有说包含方法需要是异步的。
    • @OvanCrone 如果您有时间扩展您的评论,我将不胜感激,尤其是在否决票之后 :-) 我已经在一个简单的应用程序中运行了我的代码,以验证所有 GetDataFor 任务和所有ContinueWith 任务异步并行运行;在产生另一个任务之前,无需等待一个任务完成。如果我遗漏了什么,我愿意接受建设性的批评!
    • 我错过了任务。添加!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-02
    • 1970-01-01
    • 2019-03-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多