我目前正在阅读Simon Marlow的书“ Haskell中的并行和并发编程”,但我不理解此代码:
waitAny :: [Async a] -> IO a
waitAny as = do
m <- newEmptyMVar
let forkwait a = forkIO $ do r <- try (wait a); putMVar m r
mapM_ forkwait as
wait (Async m)
在这里,我们将putMVar调用N次,但只有1次等待操作。我是否理解尝试执行putMVar会阻止N-1个线程正确吗?这是怎么回事?
...或超级简化:
test = do
m <- newEmptyMVar
forkIO $ putMVar m 1
forkIO $ putMVar m 2
a <- readMVar m
return a
为什么它可以正常工作?为什么我没有 Exception:线程在MVar操作中被无限期阻塞?
答案 0 :(得分:4)
Haskell中有关并发的一些基本规则:
当main
线程退出时,它立即杀死所有其他线程。如果要给其他线程清理的机会,则必须明确地等待其他线程。
有一组特殊的异常(辅助线程(非主线程)丢弃),因此在未被捕获时不会打印它们:
新创建的线程具有一个异常处理程序,该处理程序丢弃异常
BlockedIndefinitelyOnMVar
,BlockedIndefinitelyOnSTM
和ThreadKilled
,并将所有其他异常传递给未捕获的异常处理程序。
当线程等待MVar
且无望取得任何进展时,它将收到异常。但是由于上述问题,在此示例中这是完全不可见的。注意,由于垃圾收集器的特殊支持,这种方式只能捕获非常简单的一类死锁。无法自动检测所有死锁。
在第二个示例中,主线程(假设main = test
)在读取变量后立即退出,这没有时间让另一个线程(在putMVar
上仍然被阻塞)响应(指向1以上)。因此,首先在主线程的末尾添加一个threadDelay
,以便给另一个线程更多的时间。这还不足以带来区别,因为辅助线程被BlockedIndefinitelyOnMVar
静默杀死(第2点)。在putMVar
周围添加异常处理程序以产生显式输出。
import Control.Concurrent
import Control.Exception
main :: IO ()
main = do
m <- newEmptyMVar :: IO (MVar Int)
forkIO $ putMVar' m 1
forkIO $ putMVar' m 2
a <- readMVar m
print a
threadDelay 1000000 -- (1) Wait for other threads to clean up
putMVar' :: MVar Int -> Int -> IO ()
putMVar' r x =
catch
(putMVar r x)
(\e ->
putStrLn ("BLOCKED: " ++ show (x, e :: SomeException))) -- (2) Print something if the thread dies because of a deadlock
{- Build this file with ghc -threaded ThisFile.hs
Run it with ./ThisFile +RTS -N
-}
{- Output:
1
BLOCKED: (2,thread blocked indefinitely in an MVar operation)
-}
请注意,forkIO
级别较低,因此通常应避免使用。从头开始实现同步需要大量的精力。异步库提供了更方便的抽象。
概述并从技术上回答您的问题:
在这里,我们将putMVar调用N次,但只有1次等待操作。我是否理解尝试执行putMVar会阻止N-1个线程正确吗?这是怎么回事?
这是正确的想法。在实践中,阻塞的线程将获得异常,因为垃圾回收器可以看到MVar
不能从其他线程访问到,但是您不应该在生产中捕获并观察该异常,即使有可能,如图所示以上。确实,Control.Concurrent
的文档说明了很多:
请注意,此功能仅用于调试,不应依赖于程序的正确操作。
为什么它可以正常工作?为什么我没有
Exception: thread blocked indefinitely in an MVar operation
?
实际上会有这样的例外,但是:
main
线程退出太快,以至于实际上也无法退出;
当非main
线程被BlockedIndefinitelyOnMVar
杀死时,它们不会显示异常,您必须自己这样做。