为什么简单的 await Task.Delay(1) 可以实现并行执行?

11 浏览
0 Comments

为什么简单的 await Task.Delay(1) 可以实现并行执行?

我想问一个关于下面代码的简单问题:

static void Main(string[] args)
{
    MainAsync()
        //.Wait();
        .GetAwaiter().GetResult();
}
static async Task MainAsync()
{
    Console.WriteLine("Hello World!");
    Task a = Calc(18000);
    Task b = Calc(18000);
    Task c = Calc(18000);
    await a;
    await b;
    await c;
    Console.WriteLine(a.Result);
}
static async Task Calc(int a)
{
    //await Task.Delay(1);
    Console.WriteLine("Calc started");
    int result = 0;
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }
    return result;
}

这个例子以同步方式运行Calc函数。当取消注释//await Task.Delay(1);这一行时,Calc函数将以并行方式执行。

问题是:为什么添加简单的await后,Calc函数就变成了异步的?

我知道async/await配对的要求。我想知道的是当在函数开头添加简单的await Delay时,到底发生了什么。整个Calc函数被识别为在另一个线程中运行,但为什么?

编辑1:

当我在代码中添加线程检查时:

static async Task Calc(int a)
{
    await Task.Delay(1);
    Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
    int result = 0;
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }
    return result;
}

可以在控制台中看到不同的线程ID。如果删除await Delay这一行,所有运行Calc函数的线程ID始终相同。我认为这证明了await后的代码可能在不同的线程中运行。这就是代码更快的原因(在我看来)。

0
0 Comments

为什么简单的await Task.Delay(1)能够实现并行执行?

在理解async方法的工作方式之前,有几个重要的概念需要明确。首先,它们会像其他方法一样在同一个线程上同步运行。如果没有await Task.Delay(1)这一行代码,编译器会警告你该方法将完全同步。async关键字本身并不会使你的方法变为异步,它只是允许使用await关键字。

魔法发生在第一个作用于未完成的Task的await处。在这一点上,方法会“返回”。它返回一个Task,可以用来检查方法的其余部分何时完成。

因此,当你在这里有await Task.Delay(1),方法会在这一行返回,允许你的MainAsync方法移到下一行并开始调用Calc。

Calc的继续运行(await Task.Delay(1)后面的所有内容)取决于是否存在“同步上下文”。在ASP.NET(非Core)或UI应用程序中,同步上下文控制着连续运行的方式,它们将一个接一个地运行。在UI应用程序中,它将在与它启动的线程上运行。在ASP.NET中,它可能是一个不同的线程,但仍然是一个接一个地运行。因此,在任一情况下,你都不会看到任何并行性。

然而,由于这是一个控制台应用程序,它没有同步上下文,当来自Task.Delay(1)的Task完成时,连续部分会立即在任何线程池线程上发生。这意味着每个连续部分可以并行发生。

还值得注意的是:从C# 7.1开始,您可以使Main方法异步,从而消除了MainAsync方法的需要:

static async Task Main(string[] args)

这个例子和你的答案必须添加到官方文档中!

这是一个非常好的解答,但它的价值仅仅是教育性的。不要将它视为关于如何实现并行性的教训。将工作转移到后台线程的正确方法是使用Task.Run方法。将想要转移的所有内容放在Task.Run委托中,然后在适当的时候await返回的Task。在异步方法中插入await Task.Delay(1)或await Task.Yield()以并行化其余部分,最多只是一种肮脏的黑客方法。

Theodor - 将其视为黑客方法只适用于控制台应用程序,而不适用于Windows窗体应用程序,对吗?而且只有在用一个真正的异步方法(如HTTP POST调用)替换Task.Delay才是一个真正的并行解决方案。

"只有在用一个真正的异步方法替换Task.Delay才是一个真正的并行解决方案" - 所以我认为我们都同意不应该使用Task.Delay(1)来引入并行性。但是,即使在那里使用其他异步方法,也要考虑到代码的未来可维护性。如果您的应用程序依赖于并行性,那对于下一个维护代码的人来说并不明显。然而,如果您使用Task.Run,那么任何查看代码的人都知道并行性正在发生,这很重要,因为并行性可能发生各种奇怪的事情。

Gabriel - 假设我有一个循环,迭代一个包含10000行的datatable,每一行都进行一个HTTP请求,使用Task.Run在每次迭代中进行HTTP请求是安全的吗?只是担心这样会消耗所有的CPU线程并损害机器?而且我读到过Task.Run适用于CPU绑定的任务,而HTTP请求是IO绑定的任务,那么使用Task.Run是否可以?

请记住,并行与异步是不同的概念。异步允许在等待响应时运行其他代码。并行意味着同时运行两个代码片段,只有通过多线程才能实现。对于迭代多行进行每次都进行HTTP请求的情况,使其异步将允许你在等待响应时继续进行下一个请求。因此,它会立即触发所有10000个HTTP请求,并在响应后处理响应。这就是异步。只有当没有同步上下文(控制台应用程序)时,响应才会并行处理。

在ASP.NET(非Core)或桌面应用程序中,有同步上下文时,响应将一个接一个地处理。通常这已经足够好了。如果你想让响应并行处理,那么你需要使用Task.Run。但是,使用Task.Run在循环中运行10000次是一个坏主意。

0
0 Comments

为什么一个简单的`await Task.Delay(1)`可以实现并行执行?

在异步编程中,`await`关键字用于等待一个异步操作完成。然而,有时候在等待的过程中,并不会释放CPU的时间,这就导致其他任务无法获得CPU的时间片。这种情况下,并行执行并不能实现。实际上,异步和并行是两个不同的概念。异步编程是关于在执行非CPU密集型的工作时,利用CPU(线程/线程池)来执行其他任务。比如,当进行磁盘写操作时,并不需要占用线程的CPU时间,因此可以将线程释放出来,用于执行其他任务。这种情况下,异步操作可以释放线程,而不是等待非CPU密集型的任务完成。

为了更好地理解这个问题,我们可以通过一个代码示例来说明。首先,我们来看一个使用了`await Task.Delay(1)`的方法:

static async Task Calc(int a)
{
    // 模拟一个异步操作,这里将线程释放给其他任务
    await Task.Delay(1);
    Console.WriteLine("Calc started");
    int result = 0;
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }
    return result;
}

接下来,我们再来看一个真正的异步操作的示例:

static async Task Calc(int a)
{
    using (var reader = File.OpenText("Words.txt"))
    {
        // 真正的异步操作,将线程释放给其他任务
        var fileText = await reader.ReadToEndAsync();
        // 对fileText进行操作...
    }
    Console.WriteLine("Calc started");
    int result = 0;
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }
    return result;
}

可以看到,第一个示例中的`await Task.Delay(1)`只是模拟了一个异步操作,并没有真正释放线程给其他任务,因此并没有实现并行执行。而第二个示例中的异步操作是真正的异步操作,它释放了线程给其他任务,从而实现了并行执行。

为了进一步说明这个问题,我们可以通过一个简单的测试来观察执行时间。下面是一个简单的测试代码:

static void Main(string[] args)
{
    Console.WriteLine("Example1 - no Delay, expecting it to be faster, shorter times on average");
    for (int i = 0; i < 10; i++)
    {
        Example1().GetAwaiter().GetResult();
    }
    Console.WriteLine("Example2 - with Delay, expecting it to be slower, longer times on average");
    for (int i = 0; i < 10; i++)
    {
        Example2().GetAwaiter().GetResult();
    }
}
static async Task Example1()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task a = Calc(18000); await a;
    Task b = Calc(18000); await b;
    Task c = Calc(18000); await c;
    stopwatch.Stop();
    Console.WriteLine("Time elapsed: {0}", stopwatch.Elapsed);
}
static async Task Calc(int a)
{
    int result = 0;
    for (int k = 0; k < a; ++k)
    {
        for (int l = 0; l < a; ++l)
        {
            result += l;
        }
    }
    return result;
}
static async Task Example2()
{
    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    Task a = Calc(18000); await a;
    Task b = Calc(18000); await b;
    Task c = Calc(18000); await c;
    stopwatch.Stop();
    Console.WriteLine("Time elapsed: {0}", stopwatch.Elapsed);
}

通过运行这个测试代码,我们可以观察到不使用`await Task.Delay(1)`的情况下,代码执行时间更短。这是因为在使用了`await Task.Delay(1)`的情况下,代码会在不同的线程之间切换执行,导致额外的开销,从而使得执行时间变长。

总结起来,使用`await Task.Delay(1)`可以实现并行执行的原因是,它释放了线程给其他任务,充分利用了CPU资源。而不使用`await Task.Delay(1)`的情况下,代码执行是同步的,没有实现并行执行。

0
0 Comments

为什么一个简单的await Task.Delay(1)可以实现并行执行?

在没有await Task.Delay(1)的情况下,Calc()函数没有任何自己的await,所以它只会在运行到最后时返回给调用者。此时,返回的Task已经完成,所以调用端的await会立即使用结果,而不会实际调用异步机制。

问题的出现的原因是,Calc()函数没有自己的await,导致在调用端使用await时无法实现并行执行。

解决方法是在Calc()函数中添加一个简单的await Task.Delay(1),这样就能够将任务切换到其他线程上,并在下一个await点上返回给调用者。这样就可以实现并行执行,提高程序的性能。

,通过在Calc()函数中添加一个简单的await Task.Delay(1),可以解决没有并行执行的问题,提高程序的性能。

0