【问题标题】:Does Data.ByteString.readFile block all threads?Data.ByteString.readFile 是否阻塞所有线程?
【发布时间】:2013-10-13 17:07:16
【问题描述】:

我有以下代码:

module Main where
import Data.IORef
import qualified Data.ByteString as S
import Control.Monad
import Control.Concurrent

main :: IO ()
main = do
    var <- newIORef False
    forkIO $ forever $ do
        status <- readIORef var
        if status
            then putStrLn "main: file was read"
            else putStrLn "main: file not yet read"
        threadDelay 10000
    threadDelay 200000
    putStrLn ">>! going to read file"
    --threadDelay 200000    --
    str <- S.readFile "large2"
    putStrLn ">>! finished reading file"
    writeIORef var True
    threadDelay 200000  

我编译代码并像这样运行它:

$ ghc -threaded --make test.hs
$ dd if=/dev/urandom of=large bs=800000 count=1024
$ ./test +RTS -N3
<...>
main: file not yet read
main: file not yet read
main: file not yet read
main: file not yet read
>>! going to read file
>>! finished reading file
main: file was read
main: file was read
main: file was read
main: file was read
<...>

即程序在读取文件时暂停。我觉得这很混乱,因为如果我用threadDelay 替换readFile,它会正确地产生控制。

这里发生了什么? GHC 不是将forkIO'd 代码映射到不同的系统线程吗?

(我使用的是 Mac OS X 10.8.5,但人们报告了在 Ubuntu 和 Debian 上的相同行为)

【问题讨论】:

  • 惰性 IO。它实际上并不是在读取文件,而是立即打印这两行。
  • swish:这里不是这样。这是严格的 IO。它是 Data.Bytestring.readFile。运行程序时有明显的停顿。
  • 似乎 RTS 调度程序在一个绑定线程中运行这些绿色线程。只是好奇:forkOS 呢?
  • alvelcom:forkOS 创建的绑定线程并不能保证创建一个新的操作系统线程,只是为了始终在同一个操作系统线程中始终运行安全调用。您不一定要使用 forkOS 来声明专用的 OS 线程。
  • 在某些运行中,我得到了与 OP 相同的行为,但是大约 50% 的时间程序的行为符合预期。这里似乎发生了一些有趣的事情。

标签: multithreading haskell concurrency


【解决方案1】:

杰克是对的。

我认为大型分配正在触发垃圾回收,但在所有线程都准备好之前,回收本身无法开始。

遇到此类问题时,可以使用ThreadScope.查看是怎么回事

您代码中的事件日志如下所示:

问题是我们想给另一个线程一个运行的机会。 因此,我们不使用S.readFile,而是使用分块读取并累积结果(或惰性字节串)。如:

readChunky filename = withFile filename ReadMode $ \x -> do
  go x S.empty
  where
    go h acc = do
      more <- hIsEOF h
      case more of
        True  -> return acc
        False -> do
          v <- S.hGet h (4096 * 4096)
          go h $ S.append acc v

它按预期工作。

看图: .

【讨论】:

  • 嗯,只看threadscope截图,除了第二张图中的几个GC调用之外,我发现两种情况下垃圾收集大致同时执行。光看这些图片,怎么能得出关于 GC 阻塞第二个线程的理论?
【解决方案2】:

我提出了一个理论。我相信大分配正在触发垃圾收集,但收集本身在所有线程都准备好之前无法开始。除了读取文件的线程之外的所有线程都会阻塞,直到读取完成,但不幸的是,整个读取发生在一次调用中,因此需要一段时间。然后进行GC,之后一切正常。

我也有一个解决方法,但我认为它不能保证程序不会阻塞(虽然我还没有让它阻塞,但其他人报告说它仍然会阻塞在他们的机器上)。使用+RTS -N -qg 运行以下命令(如果您允许并行 GC,它有时会阻塞,但并非总是如此):

module Main where

import Data.IORef
import qualified Data.ByteString as S
import Control.Monad
import Control.Concurrent

main :: IO ()
main = do
  done <- newEmptyMVar
  forkIO $ do
    var <- newIORef False
    forkIO $ forever $ do
      status <- readIORef var
      if status
        then putStrLn "main: file was read"
        else putStrLn "main: file not yet read"
      threadDelay 10000
    threadDelay 200000
    putStrLn ">>! going to read file"
    --threadDelay 200000    --
    _str <- S.readFile "large"
    putStrLn ">>! finished reading file"
    writeIORef var True
    threadDelay 200000
    putMVar done ()
  takeMVar done

我还没有关于为什么 GC 正在等待系统调用的理论。我似乎无法通过我自己对sleep 的安全和不安全绑定以及将performGC 添加到状态循环来复制该问题。

【讨论】:

  • 如果有任何外部调用使用不安全的 ffi 而不是安全的 ffi,那会阻塞 GC
  • 我认为你是在正确的轨道上。在我尝试 2gb 文件之前无法在 Windows 上重现该错误,该程序在尝试读取文件时将立即进入 OOM。
【解决方案3】:

我不认为这是 readFile 的底层 ByteString 操作。 Data.ByteString.Internal 中有几个 unsafe FFI 调用:

foreign import ccall unsafe "string.h strlen" c_strlen
    :: CString -> IO CSize

foreign import ccall unsafe "static stdlib.h &free" c_free_finalizer
    :: FunPtr (Ptr Word8 -> IO ())

foreign import ccall unsafe "string.h memchr" c_memchr
    :: Ptr Word8 -> CInt -> CSize -> IO (Ptr Word8)

foreign import ccall unsafe "string.h memcmp" c_memcmp
    :: Ptr Word8 -> Ptr Word8 -> CSize -> IO CInt

foreign import ccall unsafe "string.h memcpy" c_memcpy
    :: Ptr Word8 -> Ptr Word8 -> CSize -> IO (Ptr Word8)

foreign import ccall unsafe "string.h memset" c_memset
    :: Ptr Word8 -> CInt -> CSize -> IO (Ptr Word8)

foreign import ccall unsafe "static fpstring.h fps_reverse" c_reverse
    :: Ptr Word8 -> Ptr Word8 -> CULong -> IO ()

foreign import ccall unsafe "static fpstring.h fps_intersperse" c_intersperse
    :: Ptr Word8 -> Ptr Word8 -> CULong -> Word8 -> IO ()

foreign import ccall unsafe "static fpstring.h fps_maximum" c_maximum
    :: Ptr Word8 -> CULong -> IO Word8

foreign import ccall unsafe "static fpstring.h fps_minimum" c_minimum
    :: Ptr Word8 -> CULong -> IO Word8

foreign import ccall unsafe "static fpstring.h fps_count" c_count
    :: Ptr Word8 -> CULong -> Word8 -> IO CULong

这些不安全调用比安全调用更快(每次调用的开销很小),但它们会阻塞 Haskell 运行时系统(包括线程),直到它们完成。

我不是 100% 肯定这是你看到延迟的原因,但这是我想到的第一件事。

【讨论】:

  • 我查看了 readFile 的实现,但对 GHC 的 IO 模块的某处失去了兴趣。我的怀疑与您的相同,即某处存在一些不安全的 FFI 绑定,但我认为它不一定是字节串库本身中的某些东西。也许这是一个从未预期会在大型输入或其他东西上使用的系统调用。
  • 我认为memcpy 或类似的东西可能是罪魁祸首。
  • readFile 在内部使用createAndTrim,它可以调用memcpy(尽管我不明白为什么在这些情况下会这样)。另一个可能的罪魁祸首是mallocPlainForeignPtrBytes / newPinnedByteArray#
  • 不安全的 FFI 调用不会阻塞整个 Haskell 运行时。它们阻止了调用的功能,这与阻止 1 个核心基本相同。
  • 抱歉,忘记包含指向 community.haskell.org/~simonmar/papers/conc-ffi.pdf 的链接,这对于理解 Haskell 中的 FFI/并发非常宝贵。
猜你喜欢
  • 1970-01-01
  • 2012-03-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多