【问题标题】:Does await create another thread internally before shifting to the same thread as the caller (for UI application)await 在转移到与调用者相同的线程之前是否在内部创建另一个线程(对于 UI 应用程序)
【发布时间】:2017-01-22 22:30:46
【问题描述】:

我对 async/await 的了解是,当任务完成时,继续在调用 await 时在相同的上下文中运行,在我的情况下,这将是 UI 线程。 但我的问题是,它是否会在 IO 完成之后和移动到同一个 UI 线程之前创建一个新线程(内部)。

我正在分享一段代码。如果我点击这个按钮一次,它显示在执行await之前可用线程为1023,但之后可用线程下降到1022。虽然当我检查线程id时,它与UI线程相同。

    private async void button1_ClickAsync(object sender, EventArgs e)
    {
        int x, y;
        ThreadPool.GetAvailableThreads(out x, out y);
        textBox1.Text = x.ToString()+"..."+y.ToString();
        await Task.Delay(5000);
        ThreadPool.GetAvailableThreads(out x, out y);
        textBox1.Text = x.ToString() + "..." + y.ToString();
    }

但有趣的是,当我下次单击此按钮时,可用线程数仍为 1023(等待之前和之后)。

【问题讨论】:

    标签: c# multithreading asynchronous async-await


    【解决方案1】:

    但我的问题是,它是否会在 IO 完成之后并在移动到同一个 UI 线程之前创建一个新线程(内部)。

    其他线程可能会暂时使用,但您不必担心。

    特别是,.NET 上的 I/O 通常通过作为线程池一部分的 I/O 完成端口。 I/O 线程会根据需要自动添加和删除。通常,I/O 在准备好返回您的代码之前还有一些额外的工作要做(例如,解析 HTTP 响应标头),所以 很多 BCL I/O 代码将实际上使用 I/O 线程只是为了将工作排队到线程池中。因此,I/O 代码经常(简单地)使用线程池工作线程。

    此外,在这个特定的示例中,我相信还有一个单独的计时器线程,用于合并系统计时器。当然,这是一个实现细节,可能会发生变化。

    因此,总而言之,可能会创建/销毁/临时使用其他线程,但您不必担心。它们都由 BCL 或 .NET 运行时以非常有效的方式管理,在重用线程(最小化流失)和最小化资源使用(尤其是内存)之间取得平衡。

    【讨论】:

    • 感谢斯蒂芬的回答!还有一件事。如果等待(而不是等待)这个“异步方法”,我假设 .net 仍然通过 I/O 完成端口,并且其中一个 I/O 临时线程正在传输到调用方法等待的线程。
    • @Pragmatic:是的,它仍然会走同样的路。它会必须这样做,因为被调用的方法不知道你将来是去await 还是Wait 它。但是,阻塞异步代码是一种反模式,应该尽可能避免。
    • 谢谢斯蒂芬,这真的很有帮助。有时我们必须遵循这种反模式。例如,当我们要执行多个 IO 操作时,我们调用多个 Async API,然后对其进行 WaitAll() (再次,如果调用方法不能异步)。这比创建显式任务然后调用 API 的同步版本要好得多。
    • @Pragmatic:我只是建议你重新评估“调用方法不能异步”。我经常听到这种说法,但只有 2-3% 的情况是正确。通常情况下是“我不想这样做,因为它很危险......”
    • 再次感谢您的回复。我们正在构建一个从队列(Azure 服务总线)读取消息的后台作业。我们有一个监听消息到达的事件处理程序。一旦它到达,我们就执行这段代码。我们需要等到时间事件处理程序被执行,而等待事件处理程序是不可能的。
    【解决方案2】:

    我猜你的意思是它下降到 1022。

    总的来说,我认为这取决于正在进行的异步调用。磁盘和网络调用将在来自 I/O 完成线程池的线程上返回。 Task.Delay 似乎在常规工作线程上返回。

    您可以将行更改为 await Task.Delay(5000).ConfigureAwait(false); ,在其后设置断点并检查“线程”窗口以直接查看。

    无论您从哪里调用它,它都会在工作线程上完成。 await 不会将调用上下文传递给实现异步操作的函数;它只是增加了完成后返回 UI 线程的额外步骤。

    我不会在此处详细了解跟踪确切的数字; CLR 有自己的算法来管理线程池大小,并且这些算法会随着版本的不同而变化。我不会强调在那里使用不同的线程:在普通应用程序中,它会简单地重新使用池中的现有线程,并且操作会非常快。

    【讨论】:

    • 抱歉打错字了。是的,线程数下降到 1022。我做了这个更正。
    • 这不是强调计数。我只是想了解它是如何工作的。让我再举一个用例。当进行多个异步调用然后使用 Task.Whenall() 等待时,内部创建了多少线程。据我了解,所有此类调用的单独线程(在 IO 完成后)。这可能会在短时间内发生。然后所有这些线程都将转换为 UI 线程。
    • 每个任务都将返回一个线程池线程。通常这些不需要创建,只需借用即可。至于Task.WhenAll,我做了一些测试,我认为它会在最后一个完成的任务的线程上返回。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-07-11
    • 1970-01-01
    • 1970-01-01
    • 2013-12-13
    相关资源
    最近更新 更多