使用 thread.sleep() 时,异步编程如何与线程一起工作?

How does Async programming work with Threads when using thread.sleep()?

Presumptions/Prelude:

  1. 在前面的问题中,我们注意到 thread.sleep 块线程参见:When to use Task.Delay, when to use Thread.Sleep?
  2. 我们还注意到控制台应用程序具有三个线程:主线程、GC 线程和终结器线程 IIRC。所有其他线程都是调试器线程。
  3. 我们知道 async 不会启动新线程,而是在同步上下文中运行,“仅当方法处于活动状态时才在线程上使用时间”。 https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

设置:
在示例控制台应用程序中,我们可以看到兄弟代码和父代码都不受调用 thread.sleep 的影响,至少在调用 await 之前是这样(如果进一步调用则未知)。

var sw = new Stopwatch();
sw.Start();
Console.WriteLine($"{sw.Elapsed}");
var asyncTests = new AsyncTests();

var go1 = asyncTests.WriteWithSleep();
var go2 = asyncTests.WriteWithoutSleep();

await go1;
await go2;
sw.Stop();
Console.WriteLine($"{sw.Elapsed}");
        
Stopwatch sw1 = new Stopwatch();
public async Task WriteWithSleep()
{
    sw1.Start();
    await Task.Delay(1000);
    Console.WriteLine("Delayed 1 seconds");
    Console.WriteLine($"{sw1.Elapsed}");
    Thread.Sleep(9000);
    Console.WriteLine("Delayed 10 seconds");
    Console.WriteLine($"{sw1.Elapsed}");
    sw1.Stop();
}
public async Task WriteWithoutSleep()
{
    await Task.Delay(3000);
    Console.WriteLine("Delayed 3 second.");
    Console.WriteLine($"{sw1.Elapsed}");
    await Task.Delay(6000);
    Console.WriteLine("Delayed 9 seconds.");
    Console.WriteLine($"{sw1.Elapsed}");
}

问题: 如果线程在 thread.sleep 期间被阻止执行,它如何继续处理父进程和兄弟进程?有人回答说它是后台线程,但我没有看到多线程后台线程的证据。我错过了什么?

Task.Delay 方法基本上是这样实现的(简化¹):

public static Task Delay(int millisecondsDelay)
{
    var tcs = new TaskCompletionSource();
    _ = new Timer(_ => tcs.SetResult(), null, millisecondsDelay, -1);
    return tcs.Task;
}

TaskSystem.Threading.Timer 组件的回调中完成,根据 the documentation this callback is invoked on a ThreadPool 线程:

The method does not execute on the thread that created the timer; it executes on a ThreadPool thread supplied by the system.

因此,当您等待 Task.Delay 方法返回的任务时,await 之后的继续在 ThreadPool 上运行。 ThreadPool 通常有多个线程可根据需要立即可用,因此如果您一次创建 2 个任务,那么引入并发性和并行性并不困难,就像您在示例中所做的那样。默认情况下,控制台应用程序的主线程没有配备 SynchronizationContext,因此没有适当的机制来防止观察到的并发。

¹ 仅供演示。 Timer 引用未存储在任何地方,因此它可能在调用回调之前被垃圾收集,导致 Task 永远不会完成。

I see no evidence of multithreading background threads. What am I missing?

可能您找错了地方,或者使用了错误的工具。有一个方便的 属性 可能对您有用,形式为 Thread.CurrentThread.ManagedThreadId。根据the docs,

A thread's ManagedThreadId property value serves to uniquely identify that thread within its process.

The value of the ManagedThreadId property does not vary over time

这意味着同一线程 上的所有代码运行 将始终看到相同的ManagedThreadId 值。如果您在代码中加入一些额外的 WriteLine,您将能够看到您的任务在其生命周期中可能 运行 在几个不同的线程上。一些异步应用程序甚至完全有可能在同一线程上执行所有任务 运行,尽管在正常情况下您可能不会在代码中看到这种行为。

这是我机器的一些示例输出,不能保证与您的机器相同,也不一定在同一应用程序的连续 运行 上具有相同的输出。

00:00:00.0000030
 * WriteWithSleep on thread 1 before await
 * WriteWithoutSleep on thread 1 before first await
 * WriteWithSleep on thread 4 after await
Delayed 1 seconds
00:00:01.0203244
 * WriteWithoutSleep on thread 5 after first await
Delayed 3 second.
00:00:03.0310891
 * WriteWithoutSleep on thread 6 after second await
Delayed 9 seconds.
00:00:09.0609263
Delayed 10 seconds
00:00:10.0257838
00:00:10.0898976

线程上的 运行 任务由 TaskScheduler 处理。您可以编写一个强制代码为单线程的代码,但这通常不是一件有用的事情。默认调度程序使用线程池,因此任务可以 运行 在多个不同的线程上。

我不接受我自己的回答,我会接受别人的回答,因为他们帮助我解决了这个问题。首先,在我的问题中,我使用的是 async Main。很难在 Theodor 和 Rook 的答案之间做出选择。但是,Rook 的回答为我提供了帮助我钓鱼的一件事:Thread.CurrentThread.ManagedThreadId

这些是我的 运行ning 代码的结果:

1 00:00:00.0000767
Not Delayed.
1 00:00:00.2988809
Delayed 1 second.
4 00:00:01.3392148
Delayed 3 second.
5 00:00:03.3716776
Delayed 9 seconds.
5 00:00:09.3838139
Delayed 10 seconds
4 00:00:10.3411050
4 00:00:10.5313519

我注意到这里有 3 个线程,初始线程 (1) 提供第一个调用方法和 WriteWithSleep() 的一部分,直到 Task.Delay 被初始化并稍后等待。在 Task.Delay 被带回线程 1 时,线程 4 上的所有内容都是 运行,而不是线程 1 的主线程和 WriteWithSleep 的其余部分。
WriteWithoutSleep 使用它自己的 Thread(5).

所以我的错误是认为只有 3 个线程。我相信这个问题的答案:.

但是,该问题可能不是异步的,或者可能没有考虑线程池中的这些额外工作线程。

感谢大家帮助解答这个问题。