许多支持多线程的语言都提供了一个允许线程向其他线程提供上下文切换的操作。例如Haskell的yield
。
但是,文档没有说明实际用例是什么。 何时适合使用这些 yield 函数,何时不适用?
最近我在Improving the performance of Warp again中看到过一个这样的用例,结果发现当网络服务器发送消息时,在尝试再次接收数据之前值得调用yield
,因为它需要客户端有时间处理答案并发出另一个请求。
我希望在调用yield
带来一些好处时看到其他示例或指南。
我主要对Haskell感兴趣,但我不介意学习其他语言或概念。
注意:这与生成器或协同程序无关,例如Python或Ruby中的yield
。
答案 0 :(得分:8)
GHC的IO经理使用yield
来提高绩效。用法可以在github找到,但我也会将其粘贴到此处。
step :: EventManager -> IO State
step mgr@EventManager{..} = do
waitForIO
state <- readIORef emState
state `seq` return state
where
waitForIO = do
n1 <- I.poll emBackend Nothing (onFdEvent mgr)
when (n1 <= 0) $ do
yield
n2 <- I.poll emBackend Nothing (onFdEvent mgr)
when (n2 <= 0) $ do
_ <- I.poll emBackend (Just Forever) (onFdEvent mgr)
return ()
有用的评论解释了yield
的用法:
如果[第一次非阻塞]轮询未能找到事件,我们屈服,将轮询循环线程置于 Haskell运行队列的结束。当它回来时,我们再做一次 非阻止民意调查,如果我们幸运并有准备好的事件。如果这也没有返回任何事件,那么我们会进行阻止调查。
因此yield
用于最小化EventManager
必须执行的阻止轮询的数量。
答案 1 :(得分:4)
GHC仅在特定安全点挂起线程(特别是在分配内存时)。引用The Glasgow Haskell Compiler by Simon Marlow and Simon Peyton-Jones:
仅当线程处于安全点时才会发生上下文切换,其中需要保存很少的附加状态。因为我们使用准确的GC,所以线程的堆栈可以根据需要移动和扩展或缩小。将这些与OS线程进行对比,其中每个上下文切换必须保存整个处理器状态,并且堆栈是不可移动的,因此必须为每个线程预先保留大块地址空间。
[...]
话虽如此,实现确实存在用户偶尔遇到的一个问题,尤其是在运行基准测试时。我们在上面提到轻量级线程只通过“安全点”的上下文切换来获得它们的一些效率,编译器指定为安全的代码点,虚拟机的内部状态(堆栈,堆,寄存器等)。 )处于整洁状态,可以进行垃圾收集。在GHC中,一个安全点就是每当分配内存时,几乎所有Haskell程序都会定期发生,以至于程序永远不会执行超过几十条指令而不会达到安全点。但是,在高度优化的代码中,可以找到在不分配内存的情况下运行多次迭代的循环。这往往在基准测试中经常发生(例如,像阶乘和斐波纳契这样的函数)。它在实际代码中发生的次数较少,尽管确实发生了。缺少安全点会阻止调度程序运行,这可能会产生不利影响。有可能解决这个问题,但不会影响这些循环的性能,并且人们常常关心在内循环中保存每个循环。这可能只是我们必须要妥协的妥协。
因此,具有紧密循环的程序没有这样的点并且永远不会切换线程。然后yield
是必要的,让其他线程运行。请参阅this question和this answer。