Haskell:为什么 usleep + 线程编译选项比 threadDelay 更精确?

Gor*_*ord 5 haskell usleep raspberry-pi

我在 Raspberry Pi 上用 Haskell 编写了一个测试程序,该程序在连接到 GPIO 引脚的蜂鸣器上播放令人愉悦的曲调。

这是我使用的进口:

import qualified Control.Concurrent as C
import qualified Control.Monad as M
import System.IO
import qualified System.Posix.Unistd as P
Run Code Online (Sandbox Code Playgroud)

以下是通过写入 /sys/class/gpio/gpio16/value 文件来切换引脚的函数:

changePin2 :: Handle -> String -> Int -> IO ()
changePin2 handle onOff delay = do
  pos <- hGetPosn handle
  hPutStr handle (onOff ++ "\n")
  hFlush handle
  hSetPosn pos
  P.usleep delay
  --C.threadDelay delay

blinkOn2 :: Handle -> Int -> IO ()
blinkOn2 handle delay = do
  changePin2 handle "1" delay
  changePin2 handle "0" delay
Run Code Online (Sandbox Code Playgroud)

最后,这是一个播放一个音符并在下一个音符之前暂停的示例:

  mapM_ (blinkOn2 h) (replicate 26 1908)
  P.usleep 50000
  -- C.threadDelay 50000
Run Code Online (Sandbox Code Playgroud)

当我第一次尝试时,我使用了 threadDelay,听起来很糟糕。它的音调很低,表明延迟比预期的要长,并且所有音符听起来或多或少相同。使用 usleep 函数可以显着改善情况。最后,在使用 ghc 编译时添加 -threaded 选项使声音更加清晰。

ghc -threaded buzzer1t.hs
Run Code Online (Sandbox Code Playgroud)

我不明白为什么其中任何一个都会改进它,如果有人知道这将会有很大帮助。

谷歌搜索似乎表明 usleep 和朋友是操作系统级别的延迟,而 threadDelay 仅与 Haskell 程序本身的线程有关。threadDelay 似乎也是更推荐的一种,并且被认为是更好的实践,尽管在这种情况下 usleep 显然更优越。

Car*_*arl 5

我认为文档是一个好的开始:

GHC 注意:threadDelay 是更好的选择。如果没有 -threaded 选项,usleep 将阻塞所有其他用户线程。即使使用 -threaded 选项,usleep 本身也需要一个完整的操作系统线程。threadDelay 没有这些缺点。

进一步扩展一下:GHC 运行时在系统线程上复用用户线程。默认运行时仅使用单个操作系统线程,无论有多少用户线程。大多数对外部代码的阻塞调用都是这样编写的,即它们在外部代码中时取消调度当前的 Haskell 用户线程,从而允许与 Haskell 代码同时执行。例如,这意味着即使是具有单个操作系统线程的默认运行时也可以处理同时执行 IO 的多个用户线程。

在这个世界上,实际上阻塞操作系统线程被认为是一种有点敌意的活动。 threadDelay只是将当前线程标记为不可运行,直到指定的时间到期。这对运行时系统更加友好,因为它释放了底层操作系统线程。

当您使用线程运行时时,您会获得多个操作系统线程来执行用户线程,但抓住一个而不释放它仍然有些敌意。除其他外,它可以防止垃圾收集器运行(它会等待,直到可以在已知的安全点暂停所有用户线程,因此它不会损坏同时使用的内存),并且操作系统线程比用户线程占用更多内存如果您添加额外的内容来弥补丢失的并发性。

所以对于大多数软件来说,threadDelay是一个更好的公民。但它也有缺点。线程不一定立即恢复。它可以在给定时间安排,但这并不意味着它实际运行。这仍然取决于其他线程的屈服。这几乎肯定是您遇到麻烦的原因 - 等待从可运行到实际运行的额外延迟。 usleep专门针对出现问题的情况。似乎是在需要时使用它的一个很好的理由。