C# 不使用Task.Run()计划工作时如何处理并发性?

C# 不使用Task.Run()计划工作时如何处理并发性?,c#,.net,asynchronous,.net-core,async-await,C#,.net,Asynchronous,.net Core,Async Await,如果我们填写一个需要同时执行CPU和I/O绑定工作的任务列表,只需将它们的方法声明传递给该列表,而不是创建一个新任务并使用task.Start手动调度它,那么这些任务是如何处理的 我知道这些工作不是并行进行的,而是同时进行的 这是否意味着单个线程将沿着它们移动,并且单个线程可能不是线程池中的同一个线程,或者不是最初开始等待它们全部完成/添加到列表中的同一个线程 编辑:我的问题是关于如何在列表中同时处理这些项目-调用线程是否在它们之间移动,或者发生了其他事情 需要代码的人的代码: public a

如果我们填写一个需要同时执行CPU和I/O绑定工作的任务列表,只需将它们的方法声明传递给该列表,而不是创建一个新任务并使用task.Start手动调度它,那么这些任务是如何处理的

我知道这些工作不是并行进行的,而是同时进行的

这是否意味着单个线程将沿着它们移动,并且单个线程可能不是线程池中的同一个线程,或者不是最初开始等待它们全部完成/添加到列表中的同一个线程

编辑:我的问题是关于如何在列表中同时处理这些项目-调用线程是否在它们之间移动,或者发生了其他事情

需要代码的人的代码:

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);

谢谢。

我想您的意思不是传递他们的方法声明,而是调用方法,如下所示:

var tasks = new Task[] { MethodAsync("foo"), 
                         MethodAsync("bar") };
我们将其与使用任务进行比较。运行:

首先,让我们把快速的答案放在一边。第一个变量与第二个变量的并行度较低或相等。MethodAsync的某些部分将在第一种情况下运行调用方线程,但在第二种情况下不运行。这对并行性的实际影响程度完全取决于MethodAsync的实现

为了更深入一点,我们需要了解异步方法是如何工作的。我们有这样一种方法:

async Task MethodAsync(string argument)
{
  DoSomePreparationWork();
  await WaitForIO();
  await DoSomeOtherWork();
}
调用这样的方法时会发生什么?没有魔法。该方法与其他方法一样,只是重写为一个状态机,类似于收益率回报的工作方式。它将像任何其他方法一样运行,直到遇到第一个等待。此时,它可能返回任务对象,也可能不返回。您可以等待调用方代码中的任务对象,也可以不等待。理想情况下,代码不应依赖于差异。就像收益回报一样,等待一个未完成!任务将控制权返回给方法的调用方。基本上,合同是:

如果您有CPU工作要做,请使用我的线程。 如果您所做的任何事情都意味着线程将不使用CPU,那么将结果的承诺返回给调用者一个任务对象。 它允许您最大化每个线程正在执行的CPU工作的比率。如果异步操作不需要CPU,它将让调用方执行其他操作。它本身不允许并行,但它为您提供了执行任何类型的异步操作的工具,包括并行操作。可以执行的操作之一是Task.Run,它只是返回任务的另一个异步方法,但会立即返回给调用方

那么,两者之间的区别是:

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

前者将在到达未完成任务的第一个等待后返回并继续执行下一个MethodAsync,而后者将始终立即返回

您通常应根据您的实际需求来决定:

您是否需要高效地使用CPU并最小化上下文切换等,还是希望异步方法的CPU工作可以忽略不计?直接调用该方法。 您是希望鼓励并行性,还是希望异步方法能够完成大量有趣的CPU工作?使用Task.Run。
我假设您不是要传递他们的方法声明,而是要调用该方法,如下所示:

var tasks = new Task[] { MethodAsync("foo"), 
                         MethodAsync("bar") };
我们将其与使用任务进行比较。运行:

首先,让我们把快速的答案放在一边。第一个变量与第二个变量的并行度较低或相等。MethodAsync的某些部分将在第一种情况下运行调用方线程,但在第二种情况下不运行。这对并行性的实际影响程度完全取决于MethodAsync的实现

为了更深入一点,我们需要了解异步方法是如何工作的。我们有这样一种方法:

async Task MethodAsync(string argument)
{
  DoSomePreparationWork();
  await WaitForIO();
  await DoSomeOtherWork();
}
调用这样的方法时会发生什么?没有魔法。该方法与其他方法一样,只是重写为一个状态机,类似于收益率回报的工作方式。它将像任何其他方法一样运行,直到遇到第一个等待。此时,它可能返回任务对象,也可能不返回。您可以等待调用方代码中的任务对象,也可以不等待。理想情况下,代码不应依赖于差异。就像收益回报一样,等待一个未完成!任务将控制权返回给方法的调用方。基本上,合同是:

如果您有CPU工作要做,请使用我的线程。 如果您所做的任何事情都意味着线程将不使用CPU,那么将结果的承诺返回给调用者一个任务对象。 它允许您最大化每个线程正在执行的CPU工作的比率。如果异步操作不需要CPU,它将让调用方执行其他操作。它本身不允许并行,但它为您提供了执行任何类型的异步操作的工具,包括并行操作。可以执行的操作之一是Task.Run,它只是返回任务的另一个异步方法,但返回给调用方imm 爱迪生

那么,两者之间的区别是:

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

前者将在到达未完成任务的第一个等待后返回并继续执行下一个MethodAsync,而后者将始终立即返回

您通常应根据您的实际需求来决定:

您是否需要高效地使用CPU并最小化上下文切换等,还是希望异步方法的CPU工作可以忽略不计?直接调用该方法。 您是希望鼓励并行性,还是希望异步方法能够完成大量有趣的CPU工作?使用Task.Run。
异步方法总是同步开始运行。魔术发生在第一次等待。当wait关键字看到未完成的任务时,它将返回自己的未完成任务。如果它看到一个完整的任务,执行将同步继续

因此,在这一行:

someFancyTaskList.Add(SomeFancyMethod(i));
您正在调用某个Fancymethodi,它将:

同步运行doCPUBoundWorki。 运行doIOBoundWorki。 如果doIOBoundWorki返回一个未完成的任务,那么SomeFancyMethod中的wait将返回它自己的未完成任务。 只有这样,返回的任务才会添加到列表中,循环才会继续。因此,CPU限制的工作是一个接一个依次进行的

这里有更多关于这方面的阅读:

随着每个I/O操作的完成,这些任务的继续将被安排。这些操作的完成方式取决于应用程序的类型,特别是如果存在需要返回到桌面的上下文和ASP.NET do,除非您指定ConfigureWaitFalse。因此,它们可以在同一线程上顺序运行,也可以在线程池线程上并行运行

如果要立即将CPU绑定的工作移动到另一个线程以并行运行,可以使用Task。运行:


如果这是在桌面应用程序中,那么这将是明智的,因为您希望将CPU繁重的工作从UI线程中移除。然而,你已经失去了你的上下文在某些幻想的方法,这可能或不重要,你。在桌面应用程序中,您可以非常轻松地将调用返回到UI线程。

异步方法总是同步运行。魔术发生在第一次等待。当wait关键字看到未完成的任务时,它将返回自己的未完成任务。如果它看到一个完整的任务,执行将同步继续

因此,在这一行:

someFancyTaskList.Add(SomeFancyMethod(i));
您正在调用某个Fancymethodi,它将:

同步运行doCPUBoundWorki。 运行doIOBoundWorki。 如果doIOBoundWorki返回一个未完成的任务,那么SomeFancyMethod中的wait将返回它自己的未完成任务。 只有这样,返回的任务才会添加到列表中,循环才会继续。因此,CPU限制的工作是一个接一个依次进行的

这里有更多关于这方面的阅读:

随着每个I/O操作的完成,这些任务的继续将被安排。这些操作的完成方式取决于应用程序的类型,特别是如果存在需要返回到桌面的上下文和ASP.NET do,除非您指定ConfigureWaitFalse。因此,它们可以在同一线程上顺序运行,也可以在线程池线程上并行运行

如果要立即将CPU绑定的工作移动到另一个线程以并行运行,可以使用Task。运行:


如果这是在桌面应用程序中,那么这将是明智的,因为您希望将CPU繁重的工作从UI线程中移除。然而,你已经失去了你的上下文在某些幻想的方法,这可能或不重要,你。在桌面应用程序中,您总是可以相当轻松地调用UI线程。

以下是您的代码在不使用async/await的情况下重写,而使用的是老式的延续。希望这能让你更容易理解发生了什么

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.

这里是你的代码重写没有异步/等待,与旧式的延续。希望这能让你更容易理解发生了什么

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.

问题还不清楚。没有手动安排任务。实际上,任务本质上是它们的方法声明。它们不是线程,它们代表了将来将要完成的事情。某些东西可能是委托或IO任务。它们的运行方式取决于用于执行它们的任务计划程序-它们可以在池中的线程上运行,也可以在IO线程池中的线程上运行,如果它们表示异步IO操作,则可以使用IO完成端口。如果您使用task.run,则可以创建一个将调度到默认计划程序的任务。如果使用Task.Factory.StartNew,您可以执行自定义计划程序,但您仍然在计划任务。手动调度意味着创建一个尚未调度的任务。配置它的选项,并通过传入Task.Start方法手动启动它。使用更少的单词和更多的C。仍然不清楚您在做什么。假设事情确实是它们看起来的样子,SomeFancyMethod在doCPUBoundWork完全完成之前不会返回调用方。然后它执行wait并返回exe

cution to someFancyTaskList.AddSomeFancyMethodi现在可以进入下一个i。以这种方式填写整个列表,然后启动的任务在其各自的事件发生时被唤醒,一次一个,然后它们依次等待。@PanagiotisKanavos为什么会死锁?它使用的是wait Task.WhenAll,而不是Task.WaitAll。主线程没有阻塞。问题不清楚。没有手动安排任务。实际上,任务本质上是它们的方法声明。它们不是线程,它们代表了将来将要完成的事情。某些东西可能是委托或IO任务。它们的运行方式取决于用于执行它们的任务计划程序-它们可以在池中的线程上运行,也可以在IO线程池中的线程上运行,如果它们表示异步IO操作,则可以使用IO完成端口。如果您使用task.run,则可以创建一个将调度到默认计划程序的任务。如果使用Task.Factory.StartNew,您可以执行自定义计划程序,但您仍然在计划任务。手动调度意味着创建一个尚未调度的任务。配置它的选项,并通过传入Task.Start方法手动启动它。使用更少的单词和更多的C。仍然不清楚您在做什么。假设事情确实是它们看起来的样子,SomeFancyMethod在doCPUBoundWork完全完成之前不会返回调用方。然后它执行wait,并将执行返回给someFancyTaskList.AddSomeFancyMethodi,现在可以继续执行下一个i。以这种方式填写整个列表,然后启动的任务在其各自的事件发生时被唤醒,一次一个,然后它们依次等待。@PanagiotisKanavos为什么会死锁?它使用的是wait Task.WhenAll,而不是Task.WaitAll。主线程没有被阻塞。当然,Task.Run也有成本。如果某个FancyMethod确实需要同步上下文,那么您已经破坏了您的应用程序:D@Luaan符合事实的我会在我的回答中提到这一点。谢谢你,露西,我感谢你在我之前的所有问题上对我的帮助!为了查看我是否正确理解了您->在我的示例中,我们填充了一个未完成任务的列表,因为在每次真正的等待之后,控制权都会返回给调用者,这将有效地将未完成的任务添加到列表中。要实现的棘手概念是I/O操作何时完成。如果I/O方法或我们没有使用ConfigureWait,那么延续将由一个线程拾取,并添加一个syncctx,允许我们操作应用程序。接下来会发生什么?如果其他9个I/O操作在同一个实例中完成,会发生什么?我的想法是:多个线程将为获得syncttx而斗争,但是线程在使用它之前就已经停止了。在我们的例子中,该线程是否会简单地返回一个已完成的任务,因为在等待之后,我们在方法中没有更多的代码,从而有效地避免了死锁?还是我误会了什么大事?我还想知道线程究竟如何返回已完成的任务。这可能会有所帮助:。对于UI应用程序,只有一个UI线程。上下文将始终将您带回该线程。对于ASP.NET,可以在不同的线程上重建上下文—不一定,但可以。在ASP.NET Core中,根本没有上下文。当然,Task.Run也有成本-如果某个FancyMethod确实需要同步上下文,那么您已经破坏了您的应用程序:D@Luaan符合事实的我会在我的回答中提到这一点。谢谢你,露西,我感谢你在我之前的所有问题上对我的帮助!为了查看我是否正确理解了您->在我的示例中,我们填充了一个未完成任务的列表,因为在每次真正的等待之后,控制权都会返回给调用者,这将有效地将未完成的任务添加到列表中。要实现的棘手概念是I/O操作何时完成。如果I/O方法或我们没有使用ConfigureWait,那么延续将由一个线程拾取,并添加一个syncctx,允许我们操作应用程序。接下来会发生什么?如果其他9个I/O操作在同一个实例中完成,会发生什么?我的想法是:多个线程将为获得syncttx而斗争,但是线程在使用它之前就已经停止了。在我们的例子中,该线程是否会简单地返回一个已完成的任务,因为在等待之后,我们在方法中没有更多的代码,从而有效地避免了死锁?还是我误会了什么大事?我还想知道线程究竟如何返回已完成的任务。这可能会有所帮助:。对于UI应用程序,只有一个UI线程。上下文将始终将您带回该线程。对于ASP.NET,可以在不同的线程上重建上下文—不一定,但可以。在ASP.NETCore中,根本没有上下文。我有点困惑-当我们使用Task.Run时,它会立即返回给调用方,这是什么意思?我的理解是这个任务。Run为另一个线程安排工作。当线程完成工作时,会发生什么?它刚刚回来
这是对这项工作的承诺,如果有任何任务或已完成的任务,无论线程正在等待它。如果我们像您的示例中那样编写一个同步lambda,它如何立即返回,特别是当计算工作量可能需要几分钟时?@SupportMonica spirtbob Task.Run立即返回-这并不意味着承诺已经完成。它只是意味着Task.Run的调用方可以同时做其他事情。Run在返回之前不执行MethodAsync-它计划运行MethodAsync,并立即返回。它不等待任务开始,它不等待任务到达第一个等待,它不等待任务完成。这并没有什么神奇之处——它只是传递方法调用的结果和委托之间的区别。@SupportBob暂时忽略任务,考虑一下普通方法。如果您有字符串DoSomething;,您可以将其称为DoSomething,也可以为其创建一个委托=>DoSomething。第一个执行DoSomething,但第二个不执行。同样,DoSomethingAsync执行部分方法,但=>DoSomethingAsync不执行。如果您执行了Task.RunDoSomethingAsync(由于明显的原因而不允许),则无论Task.Run的内部实现如何,您都会丢失延迟执行。@supportbob一旦等待任务,您就会强制所有内容在逻辑上同步执行;您并不真正关心纯异步方法和CPU工作方法之间的区别。只有当你想同时启动多个任务时,你才真正关心。wait是恢复逻辑同步操作的功能,假装您的方法是一个简单的同步方法。@bob,但这只是讨论一般模式。具体来说,Task.Run按照您所说的方式运行,它需要使用委托来允许这种情况发生。这就是为什么Task.Run很有用的原因-允许您将固有的同步代码与您想要异步执行的部分分离至少一段时间。我有点困惑-当我们使用Task.Run时,它会立即返回给调用者是什么意思?我的理解是这个任务。Run为另一个线程安排工作。当线程完成工作时,会发生什么?它只是将对该工作的承诺(如果有)或已完成的任务返回给等待它的线程。如果我们像您的示例中那样编写一个同步lambda,它如何立即返回,特别是当计算工作量可能需要几分钟时?@SupportMonica spirtbob Task.Run立即返回-这并不意味着承诺已经完成。它只是意味着Task.Run的调用方可以同时做其他事情。Run在返回之前不执行MethodAsync-它计划运行MethodAsync,并立即返回。它不等待任务开始,它不等待任务到达第一个等待,它不等待任务完成。这并没有什么神奇之处——它只是传递方法调用的结果和委托之间的区别。@SupportBob暂时忽略任务,考虑一下普通方法。如果您有字符串DoSomething;,您可以将其称为DoSomething,也可以为其创建一个委托=>DoSomething。第一个执行DoSomething,但第二个不执行。同样,DoSomethingAsync执行部分方法,但=>DoSomethingAsync不执行。如果您执行了Task.RunDoSomethingAsync(由于明显的原因而不允许),则无论Task.Run的内部实现如何,您都会丢失延迟执行。@supportbob一旦等待任务,您就会强制所有内容在逻辑上同步执行;您并不真正关心纯异步方法和CPU工作方法之间的区别。只有当你想同时启动多个任务时,你才真正关心。wait是恢复逻辑同步操作的功能,假装您的方法是一个简单的同步方法。@bob,但这只是讨论一般模式。具体来说,Task.Run按照您所说的方式运行,它需要使用委托来允许这种情况发生。这就是为什么Task.Run很有用的原因-允许您将固有的同步代码与您想要异步执行的部分分离至少一段时间。这样编写,doIOBoundWorkAsync的继续将在哪些线程上继续?我知道,要在使用ContinueWith时模拟等待/异步行为,必须执行以下操作:ContinueWithcontinuationAction、SynchronizationContext.Current!=无效的TaskScheduler.FromCurrentSynchronizationContext:TaskScheduler.Current正确。默认情况下,ContinueWith将继续调度到TaskScheduler.Current,这通常是TaskScheduler.default,它将继续调度到线程池。还应注意,await不使用task continuati
奥斯。不过,它大致显示了它在逻辑上的功能。这样写,doIOBoundWorkAsync的继续将在哪些线程上继续?我知道,要在使用ContinueWith时模拟等待/异步行为,必须执行以下操作:ContinueWithcontinuationAction、SynchronizationContext.Current!=无效的TaskScheduler.FromCurrentSynchronizationContext:TaskScheduler.Current正确。默认情况下,ContinueWith将继续调度到TaskScheduler.Current,这通常是TaskScheduler.default,它将继续调度到线程池。还应注意,Wait不使用任务继续。不过,它确实粗略地展示了它的逻辑功能。