Haskell将屏蔽/恢复的具体操作排除在外

twh00eeo  于 2023-02-23  发布在  其他
关注(0)|答案(2)|浏览(170)

我试着理解屏蔽异常在一般情况下是如何在一个简单的示例代码上工作的,下面是库源代码中的bracket fn:

bracket before after thing =
      mask $ \restore -> do
        a <- before
        r <- restore (thing a) `onException` after a
        _ <- after a
        return r

下面是我的“括号”版本,没有mask

myBracket before after thing = do
      a <- before
      r <- thing a `onException` after a
      _ <- after a
      pure r

我用下面这样的简单代码来测试它:

tid <- forkIO $ bracket acquire release work
   treadDelay some
   throwTo tid MyException

我在不同的时刻抛出自定义异常:当执行acquireworkrelease时,对于bracketmyBracket,我得到了相同的结果。
因此,我的问题是如何修改我的示例/测试代码,以便屏蔽/恢复在异常处理中的作用是可见的/明显的?

kgsdhlau

kgsdhlau1#

如果在acquire过程中抛出异常,那么如果没有屏蔽,我们可能永远不会执行release

big = 100000000
acquire n = putStrLn "acquire start" >> evaluate (last [0..n]) >> putStrLn "acquire stop"
release _ = putStrLn "release"
work _ = putStrLn "work"

main = do
  tid <- forkIO $ myBracket (acquire (2*big)) release work
  evaluate (last [0..big])
  throwTo tid MyException
  threadDelay 1000000 -- so prints don't overlap

  tid <- forkIO $ bracket (acquire (2*big)) release work
  evaluate (last [0..big])
  throwTo tid MyException
  threadDelay 1000000 -- so the program doesn't exit until prints are done

运行时,我们可以看到release只打印了第二次,对于bracket

% ghc test && ./test
Loaded package environment from /home/dmwit/.ghc/x86_64-linux-8.10.4/environments/default
[1 of 1] Compiling Main             ( test.hs, test.o )
Linking test ...
acquire start
test: MyException
acquire start
acquire stop
release
test: MyException
z31licg0

z31licg02#

引用自"Parallel and Concurrent Programming in Haskell"
少量的操作,包括takeMVar,被指定为可中断的。2可中断的操作甚至可以在mask内部接收异步异常。
为什么要这样做?可以将掩码看作是 * 切换到轮询模式以查找异步异常 *。在掩码内部,异步异常不再是异步的,但某些操作仍然可以引发它们。换句话说,异步异常在掩码内部变为同步的。
mask-using括号中,beforeafter操作如果碰巧使用了takeMVarthreadDelayhClose这样的可中断操作,仍然可能抛出异步异常。即使在mask中也是如此!通常,可能使用户等待很长时间的IO操作往往是可中断的。
mask确保的是异步异常不会发生在IO操作之间的间隙,或者不会导致长时间等待的IO操作。这使得异步异常更易于管理。例如,您可以使用try处理它们,而不必担心try本身可能会被中断。1
在没有maskbracket版本中,在执行beforething之间,或者在执行thingafter之间,可能会突然出现异步异常。在这两种情况下,after都不会执行,并且括号中的资源将保持未释放状态。
1也就是说,捕获的任何异步异常最终都应该重新引发。
注意,根据上面的内容,如果你把threadDelay 10000000作为before传递,它可能会抛出一个异步异常,甚至在mask内部也是如此!你有责任传递before操作(即分配操作),这些操作在被中断时不会让未释放的资源挂起。
openFile这样的函数在这些情况下会做正确的事情,这一点,加上bracket被正确屏蔽的知识,意味着用户不必担心。
顺便说一下:我们说过hClose是可中断的。应该担心被中断的bracket会留下未关闭的句柄吗?不,因为hClose的合约说:
hClose是Control. Exception中所描述的意义上的可中断操作。如果hClose在刷新其缓冲器的过程中被异步异常中断,则I/O设备(例如,文件)无论如何将被关闭。
因此,即使中断,hClose也将关闭句柄。
这段代码使用uninterruptibleMask_(由于其倾向于使程序无响应,因此应非常小心地使用该函数)创建一个不可中断的threadDelay,并将其传递给bracket
掩码版本的bracket可以打印"after",无掩码版本则不能。
这是因为,如果没有mask,抛出给线程的异常将在before完成后立即出现,而没有时间安装onException处理程序来确保清理。
相比之下,对于bracket的屏蔽版本,异常直到我们调用restore回调函数时才会出现,但此时onException处理程序已经安装好了。

main :: IO ()
main = do
    tid <- forkIO $
        bracket 
        -- myBracket 
            (uninterruptibleMask_ $ threadDelay 9000000) 
            (\_ -> hPutStrLn stderr "after")
            (\_ -> threadDelay 1000000) 
    threadDelay 2000000
    -- throwTo blocks until "before" finishes because the uninterruptible mask
    throwTo tid (userError "boo") 
    threadDelay 1000000
    pure ()

相关问题