在控制台应用程序中,为啥在等待的异步任务中使用同步阻塞代码 (Thread.Sleep(..)) 的行为类似于多线程? [复制]

Posted

技术标签:

【中文标题】在控制台应用程序中,为啥在等待的异步任务中使用同步阻塞代码 (Thread.Sleep(..)) 的行为类似于多线程? [复制]【英文标题】:In console app, why does synchronous blocking code (Thread.Sleep(..)) when used in an awaited async task behave like multi threading? [duplicate]在控制台应用程序中,为什么在等待的异步任务中使用同步阻塞代码 (Thread.Sleep(..)) 的行为类似于多线程? [复制] 【发布时间】:2021-10-05 23:55:51 【问题描述】:

代码:

using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace SampleAsyncConsoleProgram

    class Program
    
        public static void ConsolePrint(string line)
        
            Console.WriteLine(DateTime.Now.ToString("HH:mm:ss.fff") + " ["
                + Thread.CurrentThread.ManagedThreadId.ToString() + "] > " + line);
        

        static readonly IEnumerable<string> s_urlList = new string[]
        
            "website1",
            "website2",
            "website3",
            "website4"
        ;

        static Task Main() => DownloadWebsites();

        static async Task DownloadWebsites()
        
            ConsolePrint("Main program: Program started..");

            var stopwatch = Stopwatch.StartNew();

            ConsolePrint("Main program: Adding tasks to list..");
            
            IEnumerable<Task> downloadTasksQuery =
                from url in s_urlList
                select ProcessUrlAsync(url);

            List<Task> downloadTasks = downloadTasksQuery.ToList();

            ConsolePrint("Main program: Added tasks to list.."); 

            while (downloadTasks.Any())
            
                ConsolePrint("Main program: Checking if a task is completed.... followed by await...");
                Task finishedTask = await Task.WhenAny(downloadTasks);
                ConsolePrint("Main program: A task was completed..");
                downloadTasks.Remove(finishedTask);
                await finishedTask;
            

            stopwatch.Stop();

            ConsolePrint($"Main program: Program Completed Elapsed time: stopwatch.Elapsed\n Current time is " + DateTime.Now);
        

        static async Task ProcessUrlAsync(string url)
        
            ConsolePrint("Task: Starts downloading " + url);
            await Task.Delay(5000); //represents async call to fetch url
            
            ConsolePrint("Task: Sleeping for 10 sec.." + url);
            Thread.Sleep(10000); //represents some long running blocking synchronous work and keeping thread busy...
            ConsolePrint("Task: Wake up.." + url);
            ConsolePrint("Task: Done Task.." + url);
        
    

输出:

13:39:24.567 [1] > Main program: Program started..
13:39:36.255 [1] > Main program: Adding tasks to list..
13:39:37.177 [1] > Task: Starts downloading website1
13:39:43.241 [1] > Task: Starts downloading website2
13:39:43.242 [1] > Task: Starts downloading website3
13:39:43.242 [1] > Task: Starts downloading website4
13:39:43.243 [1] > Main program: Added tasks to list..
13:39:43.243 [1] > Main program: Checking if a task is completed.... followed by await...
13:39:48.811 [5] > Task: Sleeping for 10 sec..website1
13:39:48.810 [7] > Task: Sleeping for 10 sec..website2
13:39:48.810 [4] > Task: Sleeping for 10 sec..website4
13:39:48.810 [6] > Task: Sleeping for 10 sec..website3
13:39:58.823 [4] > Task: Wake up..website4
13:39:58.826 [5] > Task: Wake up..website1
13:39:58.823 [6] > Task: Wake up..website3
13:39:58.826 [7] > Task: Wake up..website2
13:39:58.828 [5] > Task: Done Task..website1
13:39:58.828 [6] > Task: Done Task..website3
13:39:58.829 [7] > Task: Done Task..website2
13:39:58.828 [4] > Task: Done Task..website4
13:39:58.922 [7] > Main program: A task was completed..
13:39:58.923 [7] > Main program: Checking if a task is completed.... followed by await...
13:39:58.923 [7] > Main program: A task was completed..
13:39:58.923 [7] > Main program: Checking if a task is completed.... followed by await...
13:39:58.924 [7] > Main program: A task was completed..
13:39:58.924 [7] > Main program: Checking if a task is completed.... followed by await...
13:39:58.924 [7] > Main program: A task was completed..
13:39:58.985 [7] > Main program: Program Completed Elapsed time: 00:00:22.6686293
 Current time is 30/07/2021 13:39:58

我期待 - 在第 65 行(在代码中),Thread.Sleep(10000); - 每个任务应该独立阻塞 10 秒(因为我使用了 Thread.Sleep(10000),这是一个同步和阻塞代码)。

但是从上面的输出来看,Thread.Sleep(阻塞操作)就像多线程一样发生。

我了解 - 在 Windows 应用程序的情况下,没有 Task.Run() 的 async+await 使用相同的线程(UI 线程)。而那个Thread.Sleep是同步阻塞操作。

    那么为什么每个Task 没有阻塞 10 秒?什么方法可以让每个 Task 阻塞 10 秒?

    我被告知此行为与同步上下文有关。因此它从线程池中提取线程(与 windows 窗体应用程序不同)。那我想问的是——这是否意味着它会执行多线程(多线程并行运行)?

【问题讨论】:

改变他们的顺序,你会看到不同。 "如果没有其他同等优先级的线程可以运行,当前线程的执行不会被暂停。" docs.microsoft.com/en-us/dotnet/api/… “我明白了——没有Task.Run()的async+await不使用多线程——所以上面的代码只使用了1个线程”你的理解有点离开。它不会使用线程来等待 IO 绑定操作,但会运行延续。 嘿,请不要一直更改您的代码。人们正在寻找答案。如果我为您已删除的代码发布答案会怎样? 您获得相同线程名称的原因是该线程在此处被捕获:Thread thr = Thread.CurrentThread;,随后您访问了该线程的名称而不是实际运行的线程。 【参考方案1】:

await Task.Delay(5000); 在模拟 IO 绑定操作方面做得非常好。尽管 IO 绑定操作不使用线程来等待完成(参见 Stephen Cleary 的 there is no thread 文章,docs 也可以提供一些启示),但延续将在线程池上运行。因此downloadTasksQuery.ToList() 将并行启动所有await Task.Delay,然后(取决于任务和线程池的数量以及SynchronizationContext)设置部分或全部可以在单独的线程上继续。

那么为什么每个任务都没有阻塞 10 秒呢?让每个任务阻塞 10 秒的方法是什么?

它会阻塞,但在你的情况下它会阻塞一个单独的线程。

我被告知此行为与同步上下文有关。

是的,这种行为可能会受到同步上下文的影响。例如,在桌面应用程序中,未标记 ConfigureAwait(false) 的延续将在单个 UI 线程上运行,并且由于您没有为 await Task.Delay(5000) 配置 ConfigureAwait(false),因此您实际上最终会导致 UI 无响应。

【讨论】:

或全部这永远不会使用多个线程。要启动一个新线程,OP 需要使用 Task.Run,任何 CPU 绑定代码都将根据该线程的可用性进行排序。 @Liam:延续将在不同的线程上运行 @Liam:为什么不呢?我有一个 8 核 CPU,所以如果 Windows 愿意,它可以并行运行 8 个 @Liam:恕我直言,这次你错了。我在 WinDbg 中确认了它。有 4 个线程在运行,抱歉。 @Liam:如果我将 Thread.Sleep 替换为保持 CPU 忙碌的东西,它将 100% 使用 16 个内核:i.stack.imgur.com/FPvnp.png【参考方案2】:

此答案基于您的旧版本代码。它总是打印相同的线程名称的原因是线程被捕获,因此thr.Name 不是当前线程的名称,而是捕获的线程。

如果你内联那个变量并使用托管线程ID,你可以看到sleep里面有多个线程:

static async Task ProcessUrlAsync(string url)

    Console.Write(Thread.CurrentThread.ManagedThreadId + " starts downloading " + url + " at " + DateTime.Now + "\n\n");
    await Task.Delay(5000); //represents async call to fetch url
    Console.Write(Thread.CurrentThread.ManagedThreadId + " sleeping for 5 sec.. " + url + " at " + DateTime.Now + "\n\n");
    Thread.Sleep(5000); //represents some long running blocking syncronous work and keeping thread busy...
    Console.Write(Thread.CurrentThread.ManagedThreadId + " sleeping done for.. " + url + " at " + DateTime.Now + "\n\n");
    Console.Write(Thread.CurrentThread.ManagedThreadId + " - Done " + url + " at " + DateTime.Now + "\n\n");

部分输出:

5 sleeping for 5 sec.. website3 at 30.07.2021 14:54:09
7 sleeping for 5 sec.. website1 at 30.07.2021 14:54:09
4 sleeping for 5 sec.. website4 at 30.07.2021 14:54:09
6 sleeping for 5 sec.. website2 at 30.07.2021 14:54:09

您还可以使用 VS 并行堆栈视图查看 4 个线程在同一方法中。

在WinDbg中,你可以看到已经分配了4个操作系统线程:

0:000> ~*e!clrstack
[...]
OS Thread Id: 0x455c (11)
Child SP       IP Call Site
0665f444 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]
OS Thread Id: 0x35b4 (12)
Child SP       IP Call Site
0681f1f4 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]
OS Thread Id: 0x17e4 (13)
Child SP       IP Call Site
0695f3e4 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]
OS Thread Id: 0x475c (14)
Child SP       IP Call Site
06a9f124 02d515a6 SampleAsyncConsoleProgram.Program+d__3.MoveNext()
[...]

【讨论】:

以上是关于在控制台应用程序中,为啥在等待的异步任务中使用同步阻塞代码 (Thread.Sleep(..)) 的行为类似于多线程? [复制]的主要内容,如果未能解决你的问题,请参考以下文章

为啥我的循环中的异步委托没有等待

Spring Boot中使用@Async实现异步调用,加速任务的执行!

Spring Boot中使用@Async实现异步调用,加速任务的执行!

同步异步阻塞和非阻塞

python网络编程04 异步与同步编程事件驱动

运行多个异步任务并等待它们全部完成