在新的 Thread() 中等待是没有意义的吗?

muu*_*lla 1 c# multithreading asynchronous async-await

示例代码:

    class Program
    {
        static void sleepFunc()
        {
            int before = Thread.CurrentThread.ManagedThreadId;
            Thread.Sleep(5000);
            int after = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine($"{before} -> sleep -> {after}");
        }

        static async void delayFunc()
        {
            int before = Thread.CurrentThread.ManagedThreadId;
            await Task.Delay(5000);
            int after = Thread.CurrentThread.ManagedThreadId;
            Console.WriteLine($"{before} -> delay -> {after}");
        }
        static void Main(string[] args)
        {
            List<Thread> threads = new List<Thread>();
            for(int i = 0; i < 10; i++)
            {
                var thread = new Thread(sleepFunc);
                thread.Start();
                threads.Add(thread);
            }
            Thread.Sleep(1000); // just to separate the result sections
            for (int i = 0; i < 10; i++)
            {
                var thread = new Thread(delayFunc);
                thread.Start();
                threads.Add(thread);
            }
            Console.ReadLine();
        }
    }
Run Code Online (Sandbox Code Playgroud)

示例输出:

3 -> sleep -> 3
7 -> sleep -> 7
4 -> sleep -> 4
5 -> sleep -> 5
6 -> sleep -> 6
8 -> sleep -> 8
9 -> sleep -> 9
10 -> sleep -> 10
11 -> sleep -> 11
12 -> sleep -> 12
21 -> delay -> 25
18 -> delay -> 37
15 -> delay -> 36
16 -> delay -> 32
19 -> delay -> 24
20 -> delay -> 27
13 -> delay -> 32
17 -> delay -> 27
22 -> delay -> 25
14 -> delay -> 26
Run Code Online (Sandbox Code Playgroud)

Thread.Sleep() 的延续运行在同一个显式创建的线程上,但 Task.Delay() 的延续运行在不同的(线程池)线程上。

由于延续总是在线程池上运行,如果在 func 内的任何地方都有等待,使用新的 Thread(func) 是否只是毫无意义/浪费/反模式?有没有办法强制任务在原来的非线程池新线程上继续?

我问是因为我读到有人建议在线程而不是任务中放置一个长时间运行的循环(例如用于套接字通信),但似乎如果他们调用 ReadAsync 那么它最终会成为线程池上的任务无论如何,新线程毫无意义?

Pet*_*iho 5

由于延续总是在线程池上运行,如果在 func 内的任何地方都有等待,使用新的 Thread(func) 是否只是毫无意义/浪费/反模式?

永远不要把话说绝了。但总的来说,是的,这毫无意义。

有没有办法强制任务在原来的非线程池新线程上继续?

是的,有办法。创建您自己的同步上下文并在该线程中运行该上下文。然后 anyawait将在该上下文中继续(当然,默认情况下……如果您调用ConfigureAwait(false),则不允许这样做)。

创建自己的同步上下文(应该)相对较少。要么你在一个自然有自己的上下文中运行代码,要么你真的不关心哪个线程处理代码。

显式线程和async/await并不能很好地结合在一起。该await语句是一种以线性、近似同步的方式组合异步代码的方法。通常,如果您创建了一个显式线程,那是因为您打算在该线程中执行所有工作。在这种情况下甚至没有理由调用异步方法。

相反,如果您使用异步方法,则根据定义,这些方法以一种独立于您可能使用的任何线程的方式异步执行。许多异步操作甚至根本不使用线程!如果您await在逻辑中使用,那么根据定义,您的代码会短暂运行一段时间,然后在等待异步完成时暂停。在这种情况下没有理由使用显式线程;线程池是一种理想的方式,可以在您不等待其他事情时在这些短暂的时间间隔内执行您自己的代码。

我问是因为我读到有人建议在线程而不是任务中放置一个长时间运行的循环(例如用于套接字通信),但似乎如果他们调用 ReadAsync 那么它最终会成为线程池上的任务无论如何,新线程毫无意义?

I/O 是一个典型的例子,其中使用await是一种理想的技术。一个套接字涉及(从 CPU 的角度来看)长时间等待某事发生,被非常非常短的时间中断,处理出现的任何数据。它一直被认为专整个线程从套接字阅读是个坏主意的情况。早在 .NET 之前,仍然存在异步 I/O 模型,允许线程池处理这些间歇性完成的 I/O 操作。请参阅“I/O 完成端口”。事实上,.NET 套接字 API 是建立在该 API 之上的。

如果连接很少,则可以使用“每个连接一个线程”模型,但它的扩展性非常差。使用 BSD 套接字 API 中的“选择”模型的较旧替代方案做得好一点,但效率很低。使用 IOCP 允许 Windows 有效地管理处理大量套接字的几个线程,而不会将太多 CPU 时间用于 I/O,也不会太多线程(每个线程在 Windows 上使用相对大量的资源)。

不仅在您描述的场景中显式线程毫无意义,还有其他非常好的理由不这样做。


有用的补充阅读:
任务和线程有什么区别?
任务与线程的区别