我不熟悉那个特定的 API,但在我看来,您可能只想在 MVar 中存储令牌和一个指示其刷新次数的计数器。一个线程负责最初用令牌填充MVar。每个需要令牌的线程都调用readMVar 来获取它。
当线程发现令牌已过期时,它会调用tryTakeMVar 来控制令牌。如果失败了,那么其他线程已经控制了,这个线程返回到readMVar。如果成功,它会检查计数器是否符合预期。如果不是,其他线程已经刷新了令牌,它只是把它放回去。如果是,那么它会刷新令牌,递增计数器,然后将它们放入 MVar 中,然后继续前进。对于锁定协议,您需要像往常一样小心异常安全;有一些 MVar 函数可以帮助解决这个问题。
正如我所描述的,该方案要求一个线程负责初始化。如果您只想在第一次需要时获取令牌,则必须进行一个小调整:在MVar 中存储一个Maybe,初始化为Nothing。
以下代码假设函数acquireToken 和refreshToken 分别初始获取令牌并刷新现有令牌。显然,如果这些操作实际上以相同的方式完成,您可以相应地进行调整。如果刷新令牌涉及大量计算,则使用下面的restore;我们不想让线程在执行此操作时无法杀死。
newtype TokBox = TB (MVar (Maybe (Word, AccessToken)))
newTokBox :: IO TokBox
newTokBox = TB <$> newMVar Nothing
-- | Get a (possibly expired) token and an action to use if that
-- token is expired. The result
-- should only be used once.
getToken :: TokBox -> IO (AccessToken, IO ())
getToken tb@(TB mv) = do
contents <- readMVar mv
case contents of
Nothing -> refresh Nothing tb
Just (_, t) -> pure (t, refresh contents tb)
-- Refresh the access token, expecting the MVar to have particular contents.
refresh :: Maybe (Word, AccessToken) -> TokBox -> IO ()
refresh old (TB mv) =
mask $ \restore ->
tryTakeMVar mv >>= \case
-- Another thread is refreshing
Nothing -> pure ()
Just cont
-- Another thread refreshed; we restore the MVar
| not $ sameContents cont old
= putMVar mv cont
| otherwise
= (restore $ case cont of
Nothing -> do
tok <- acquireToken
putMVar mv (Just (0, tok))
Just (count, tok) -> do
tok' <- refreshToken tok
putMVar mv (Just (count + 1, tok')))
`onException`
putMVar cont
sameContents :: Maybe (Word, a) -> Maybe (Word, b) -> Bool
sameContents Nothing Nothing = True
sameContents (Just (m, _)) (Just (n, _)) = m == n
sameContents _ _ = False