【问题标题】:Is it pointless to await inside a new Thread()?在新的 Thread() 中等待是没有意义的吗?
【发布时间】:2020-10-30 13:48:36
【问题描述】:

示例代码:

    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();
        }
    }

样本输出:

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

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

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

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

【问题讨论】:

  • 你问的是ConfigureAwait(true)吗?我不确定您所说的“原始新线程”是什么意思-它是原始的还是新的?对我来说,这些有点相反的东西..
  • 是的,在所描述的情况下,这是毫无意义的。假设延续将在同一个线程上运行。您的代码等待异步操作完成,然后才能继续执行其他操作。在这种情况下,如果当前线程还想继续运行,那么当异步操作正在进行时,它会做什么?没什么,就是等。所以这和只是做Thread.Sleep一样。
  • 前段时间我有same problem。教父本人斯蒂芬·克利里(Stephen Cleary)向我暗示了Nito.AsyncEx.Context。这个库工作正常——唯一的问题是你应该删除所有ConfigureAwait-calls,因为这会破坏线程绑定。由于您不知道在被调用的方法中调用了哪些代码,您应该简单地删除所有这些代码。
  • 您的示例代码包含一个async void 方法。异步无效is something to avoid.

标签: c# multithreading asynchronous async-await


【解决方案1】:

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

永远不要说永远。但一般来说,是的,这是没有意义的。

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

是的,有办法。创建您自己的同步上下文并在该线程中运行该上下文。然后任何await 都将在该上下文中继续(当然,默认情况下……如果您调用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 上使用相对大量的资源)。

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


有用的补充阅读:
What is the difference between task and thread?
Task vs Thread differences

【讨论】:

    猜你喜欢
    • 2012-08-09
    • 2019-11-17
    • 2013-04-16
    • 2013-10-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多