在控制台应用程序中,为啥在等待的异步任务中使用同步阻塞代码 (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
)设置部分或全部可以在单独的线程上继续。
它会阻塞,但在你的情况下它会阻塞一个单独的线程。
我被告知此行为与同步上下文有关。是的,这种行为可能会受到同步上下文的影响。例如,在桌面应用程序中,未标记 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实现异步调用,加速任务的执行!