C#中的SpinWait用于长时间等待

10

这段代码几乎不消耗CPU(i5系列)

    public void SpinWait() {

        for (int i = 0; i < 10000; i++)
        {
            Task.Factory.StartNew(() =>
            {
                var sw = new SpinWait();
                while (true)
                {
                    sw.SpinOnce();
                }
            });
        }
    }

在我的代码中,与SemaphoreSlim相比,当旋转真正被证明是必要的情况下(5 Mops),性能差异达到3倍或更多。然而,我担心长时间等待时使用它。标准建议是实现两阶段等待操作。我可以检查 NextSpinWillYield 属性,并引入计数器+重置以增加默认旋转迭代次数而不产生收益,然后再回到信号量。

但是,仅使用 SpinOnce 进行长期等待有什么缺点?我已经查看了其 implementation,并且在需要时它会正确地产生收益。它使用 Thread.SpinWait ,在现代CPU上使用PAUSE指令,并根据Intel非常高效。

一项我在监控任务管理器时发现的问题是,由于默认的线程池算法(当所有任务都忙碌时,每秒添加一个线程),线程数逐渐增加。这可以通过使用 ThreadPool.SetMaxThreads 来解决,然后线程数就固定了,CPU 使用率仍然接近零。
如果长时间等待的任务数量有限,那么使用 SpinWait.SpinOnce 进行长期等待会有哪些其他陷阱?它是否取决于 CPU 家族、操作系统、.NET 版本?
(只是为了澄清:我仍然会实现两阶段等待,我只是好奇为什么不一直使用 SpinOnce?)
1个回答

13

嗯,缺点就是你看到的那个,你的代码占用了一个线程,但并没有完成任何任务。这会阻止其他代码运行,并迫使线程池管理器采取措施。调整ThreadPool.SetMaxThreads()只是在可能存在严重问题时的一种权宜之计,只有在真正需要抓住回家的飞机时才使用。

只有在非常确定自旋比线程上下文切换更有效率时才应该尝试自旋。这意味着您必须确信线程可以在不到10,000个CPU周期内继续运行。这只有5微秒左右,比大多数程序员认为的“长期”要短得多。

使用能够触发线程上下文切换的同步对象。或者使用lock关键字。

这不仅会让处理器腾出来以便其他等待的线程可以完成其工作,从而完成更多的工作,而且还会向操作系统线程调度程序提供优秀的提示。被标记为信号的同步对象将提高线程的优先级,因此极有可能下次就获取处理器。


谢谢!顺便问一下,如果我在 semaphore.WaitAsync(-1, token) 上等待,线程唤醒需要多少微秒?如果我使用自旋等待并且 SpinOneSleep(1) 后让出,那么在最坏的情况下,如果信号恰好在调用 Sleep(1) 后到达,我会引入至少 15 毫秒的延迟。如果我改用信号量,那么线程在 semaphore.Release() 后唤醒的最坏延迟是多少? - V.B.
这在很大程度上取决于机器上正在发生的其他事情。当然,您还必须与其他进程拥有的线程和内核线程竞争。它需要微秒级别的时间,而不是毫秒级别的时间。只需使用计时器测量即可,确保对许多测量值取中位数,因为偶尔出现的最坏情况可能会非常长。请记住,您无法对此进行任何操作,因此仅仅是信息性的。永远不要使用Sleep()。 - Hans Passant
所以,信号量释放后醒来与.NET无关,使用C/C++甚至汇编语言也无济于事?(不打算使用它们,只是为了理解这是硬件/操作系统设计) - V.B.
1
正确的线程调度和上下文切换是纯粹的操作系统职责。 - Hans Passant

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接