13

我有以下代码:

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
<...>

也就是说,程序在读取文件时暂停。我觉得这很令人困惑,因为如果我用它正确替换readFilethreadDelay会产生控制。

这是怎么回事?GHC 不是将forkIO代码映射到不同的系统线程吗?

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

4

3 回答 3

8

杰克是对的。

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

当你遇到这样的问题时,你可以通过使用ThreadScope 来查看发生了什么。

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

img non-chunked.png

问题是我们想给另一个线程一个运行的机会。因此,我们不使用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

它按预期工作。

见图表: 见图表

于 2013-10-14T22:19:33.980 回答
5

我发展了一个理论。我相信大分配正在触发垃圾收集,但收集本身在所有线程都准备好之前无法开始。除了读取文件的线程之外的所有线程都会阻塞,直到读取完成,但不幸的是,整个读取都发生在一次调用中,因此需要一段时间。然后执行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到状态循环中。

于 2013-10-14T03:00:58.270 回答
1

我认为这readFile与其说是底层ByteString操作。有几个unsafeFFI 调用Data.ByteString.Internal

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% 肯定这是你看到延迟的原因,但这是我想到的第一件事。

于 2013-10-13T18:00:33.767 回答