简体   繁体   English

不使用 Task.Run() 安排工作时如何处理并发?

[英]How is concurrency handled when not using Task.Run() to schedule the work?

If we fill a list of Tasks that need to do both CPU-bound and I/O bound work, by simply passing their method declaration to that list (Not by creating a new task and manually scheduling it by using Task.Start ), how exactly are these tasks handled?如果我们通过简单地将它们的方法声明传递给该列表(而不是通过创建新任务并使用Task.Start手动调度它)来填充需要同时执行 CPU 绑定和 I/O 绑定工作的任务列表,如何这些任务到底处理好了吗?

I know that they are not done in parallel, but concurrently.我知道它们不是并行完成的,而是同时完成的。

Does that mean that a single thread will move along them, and that single thread might not be the same thread in the thread pool, or the same thread that initially started waiting for them all to complete/added them to the list?这是否意味着单个线程将沿着它们移动,并且该单个线程可能不是线程池中的同一个线程,或者最初开始等待它们全部完成/将它们添加到列表中的同一个线程?

EDIT: My question is about how exactly these items are handled in the list concurrently - is the calling thread moving through them, or something else is going on?编辑:我的问题是如何在列表中同时处理这些项目 - 调用线程是通过它们移动,还是正在发生其他事情?

Code for those that need code:需要代码的人的代码:

public async Task SomeFancyMethod(int i)
{
    doCPUBoundWork(i);
    await doIOBoundWork(i);
}


//Main thread

List<Task> someFancyTaskList = new List<Task>();
for (int i = 0; i< 10; i++)
    someFancyTaskList.Add(SomeFancyMethod(i));
// Do various other things here --
// how are the items handled in the meantime?
await Task.WhenAll(someFancyTaskList);

Thank you.谢谢你。

Asynchronous methods always start running synchronously.异步方法总是同步开始运行。 The magic happens at the first await .魔法发生在第一个await When the await keyword sees an incomplete Task , it returns its own incomplete Task .await关键字看到一个不完整Task时,它会返回自己的不完整Task If it sees a complete Task , execution continues synchronously.如果它看到一个完整Task ,则同步继续执行。

So at this line:所以在这一行:

someFancyTaskList.Add(SomeFancyMethod(i));

You're calling SomeFancyMethod(i) , which will:您正在调用SomeFancyMethod(i) ,它将:

  1. Run doCPUBoundWork(i) synchronously.同步运行doCPUBoundWork(i)
  2. Run doIOBoundWork(i) .运行doIOBoundWork(i)
  3. If doIOBoundWork(i) returns an incomplete Task , then the await in SomeFancyMethod will return its own incomplete Task .如果doIOBoundWork(i)返回一个不完整的Task ,那么SomeFancyMethod中的await将返回它自己的不完整Task

Only then will the returned Task be added to your list and your loop will continue.只有这样,返回的Task才会添加到您的列表中,并且您的循环将继续。 So the CPU-bound work is happening sequentially (one after the other).因此,受 CPU 限制的工作是按顺序进行的(一个接一个)。

There is some more reading about this here: Control flow in async programs (C#)这里有更多关于这个的阅读: 异步程序中的控制流(C#)

As each I/O operation completes, the continuations of those tasks are scheduled.随着每个 I/O 操作的完成,这些任务的继续进行安排。 How those are done depends on the type of application - particularly, if there is a context that it needs to return to (desktop and ASP.NET do unless you specify ConfigureAwait(false) , ASP.NET Core doesn't ).这些是如何完成的取决于应用程序的类型 - 特别是,如果有一个上下文需要返回(桌面和 ASP.NET 做,除非你指定ConfigureAwait(false)ASP.NET 核心没有)。 So they might run sequentially on the same thread, or in parallel on ThreadPool threads.所以它们可能在同一个线程上按顺序运行,或者在 ThreadPool 线程上并行运行。

If you want to immediately move the CPU-bound work to another thread to run that in parallel, you can use Task.Run :如果您想立即将 CPU 绑定的工作转移到另一个线程以并行运行,您可以使用Task.Run

someFancyTaskList.Add(Task.Run(() => SomeFancyMethod(i)));

If this is in a desktop application, then this would be wise, since you want to keep CPU-heavy work off of the UI thread.如果这是在桌面应用程序中,那么这将是明智的,因为您希望将 CPU 繁重的工作保持在 UI 线程之外。 However, then you've lost your context in SomeFancyMethod , which may or may not matter to you.但是,您在SomeFancyMethod中丢失了上下文,这对您来说可能很重要,也可能无关紧要。 In a desktop app, you can always marshall calls back to the UI thread fairly easily.在桌面应用程序中,您总是可以相当轻松地编组回调到 UI 线程。

I assume you don't mean passing their method declaration , but just invoking the method, like so:我假设您不是要传递他们的方法声明,而只是调用该方法,如下所示:

var tasks = new Task[] { MethodAsync("foo"), 
                         MethodAsync("bar") };

And we'll compare that to using Task.Run :我们将把它与使用Task.Run进行比较:

var tasks = new Task[] { Task.Run(() => MethodAsync("foo")), 
                         Task.Run(() => MethodAsync("bar")) };

First, let's get the quick answer out of the way.首先,让我们快速回答一下。 The first variant will have lower or equal parallelism to the second variant.第一个变体将具有比第二个变体更低或相等的并行度。 Parts of MethodAsync will run the caller thread in the first case, but not in the second case. MethodAsync的一部分将在第一种情况下运行调用者线程,但在第二种情况下不会。 How much this actually affects the parallelism depends entirely on the implementation of MethodAsync .这实际上影响并行性的程度完全取决于MethodAsync的实现。

To get a bit deeper, we need to understand how async methods work.为了更深入一点,我们需要了解异步方法是如何工作的。 We have a method like:我们有一个类似的方法:

async Task MethodAsync(string argument)
{
  DoSomePreparationWork();
  await WaitForIO();
  await DoSomeOtherWork();
}

What happens when you call such a method?当你调用这样的方法时会发生什么? There is no magic .没有魔法 The method is a method like any other, just rewritten as a state machine (similar to how yield return works).该方法与任何其他方法一样,只是重写为 state 机器(类似于yield return的工作方式)。 It will run as any other method until it encounters the first await .它将像任何其他方法一样运行,直到遇到第一个await At that point, it may or may not return a Task object.此时,它可能会或可能不会返回Task object。 You may or may not await that Task object in the caller code.您可能会或可能不会在调用方代码中等待该Task object。 Ideally, your code should not depend on the difference.理想情况下,您的代码不应依赖于差异。 Just like yield return , await on a (non-completed.) task returns control to the caller of the method, Essentially: the contract is:就像yield return一样, await (未完成)任务将控制权返回给方法的调用者,本质上:合约是:

  • If you have CPU work to do, use my thread.如果您有 CPU 工作要做,请使用我的线程。
  • If whatever you do would mean the thread isn't going to use the CPU, return a promise of the result (a Task object) to the caller.如果您所做的任何事情都意味着线程不会使用 CPU,则将结果( Task对象)的 promise 返回给调用者。

It allows you to maximize the ratio of what CPU work each thread is doing.它允许您最大化每个线程正在执行的 CPU 工作的比率。 If the asynchronous operation doesn't need the CPU, it will let the caller do something else.如果异步操作不需要 CPU,它会让调用者做其他事情。 It doesn't inherently allow for parallelism, but it gives you the tools to do any kind of asynchronous operation, including parallel operations.本质上不允许并行性,但它为您提供了执行任何类型的异步操作(包括并行操作)的工具。 One of the operations you can do is Task.Run , which is just another asynchronous method that returns a task, but which returns to the caller immediately.您可以执行的操作之一是Task.Run ,它只是另一个返回任务的异步方法,但会立即返回给调用者。

So, the difference between:因此,两者之间的区别:

MethodAsync("foo");
MethodAsync("bar");

and

Task.Run(() => MethodAsync("foo"));
Task.Run(() => MethodAsync("bar"));

is that the former will return (and continue to execute the next MethodAsync ) after it reaches the first await on a non-completed task , while the latter will always return immediately.是前者在到达未完成任务的第一个await后将返回(并继续执行下一个MethodAsync ),而后者将始终立即返回。

You should usually decide based on your actual requirements:您通常应该根据您的实际需求来决定:

  • Do you need to use the CPU efficiently and minimize context switching etc., or do you expect the async method to have negligible CPU work to do?您是否需要有效地使用 CPU 并最小化上下文切换等,或者您是否希望异步方法的 CPU 工作可以忽略不计? Invoke the method directly.直接调用方法。
  • Do you want to encourage parallelism or do you expect the async method to do interesting amounts of CPU work?您想鼓励并行性还是希望异步方法能够完成有趣的 CPU 工作量? Use Task.Run .使用Task.Run

Here is your code rewritten without async/await, with old-school continuations instead.这是您在没有 async/await 的情况下重写的代码,而是使用老式的延续。 Hopefully it will make it easier to understand what's going on.希望这将使人们更容易理解正在发生的事情。

public Task CompoundMethodAsync(int i)
{
    doCPUBoundWork(i);
    return doIOBoundWorkAsync(i).ContinueWith(_ =>
    {
        doMoreCPUBoundWork(i);
    });
}

// Main thread
var tasks = new List<Task>();
for (int i = 0; i < 10; i++)
{
    Task task = CompoundMethodAsync(i);
    tasks.Add(task);
}
// The doCPUBoundWork has already ran synchronously 10 times at this point

// Do various things while the compound tasks are progressing concurrently

Task.WhenAll(tasks).ContinueWith(_ =>
{
    // The doIOBoundWorkAsync/doMoreCPUBoundWork have completed 10 times at this point
    // Do various things after all compound tasks have been completed
});

// No code should exist here. Move everything inside the continuation above.

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

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