在线程中调用'yield'有哪些很好的用例?



许多支持多线程的语言提供了一个操作,允许一个线程向另一个线程提供上下文切换。例如Haskell的yield

然而,文档并没有说明实际的用例是什么。何时使用这些yield函数合适,何时不合适?

最近我看到了一个这样的用例,再次提高Warp的性能,当网络服务器发送消息时,在尝试再次接收数据之前调用yield是值得的,因为它需要客户端花费一些时间来处理答案并发出另一个请求。

我希望看到调用yield带来一些好处的其他示例或指南。

我主要对Haskell感兴趣,但我不介意学习其他语言或一般概念。

注意:这与生成器或协程无关,例如Python或Ruby中的yield

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必须执行的阻塞轮询的数量。

GHC只在特定的安全点挂起线程(特别是在分配内存时)。引用《The Glasgow Haskell Compiler》作者Simon Marlow和Simon Peyton-Jones:

上下文切换只在线程处于安全点时发生,此时需要保存的额外状态很少。因为我们使用了精确的GC,所以线程的堆栈可以根据需要移动、扩展或缩小。相比之下,操作系统线程的每个上下文切换都必须保存整个处理器状态,并且堆栈是不可移动的,因此必须为每个线程预先保留一大块地址空间。

[…]

话虽如此,但实现确实存在一个用户偶尔会遇到的问题,特别是在运行基准测试时。我们在上面提到,轻量级线程仅通过在"安全点"进行上下文切换来获得一些效率,这些"安全点"是代码中编译器指定为安全的点,在这些点上,虚拟机的内部状态(堆栈、堆、寄存器等)处于整洁状态,并且可以进行垃圾收集。在GHC中,安全点是在分配内存的任何时候,这在几乎所有的Haskell程序中都有规律地发生,以至于程序在执行几十条指令时都不会碰到安全点。然而,在高度优化的代码中,有可能找到运行多次迭代而不分配内存的循环。这在基准测试中经常发生(例如,像factorial和Fibonacci这样的函数)。这种情况在实际代码中发生的频率较低,但确实会发生。缺乏安全点会阻止调度器运行,这可能会产生有害的影响。解决这个问题是可能的,但不能不影响这些循环的性能,而且人们通常关心在他们的内部循环中节省每个循环。这可能只是我们不得不接受的妥协。

因此,有紧循环的程序可能没有这样的点,也不会切换线程。然后需要yield来让其他线程运行。请看这个问题和这个答案