Haskell中是否有一个标准的数据结构,它可以像IORef一样可变,但是如果需要,也可以像MVar一样被“锁定”?这是我要实现的目标:


有多个线程在基于OAuth的API上调用,并且所有线程都需要AccessToken
但是,AccessToken可以到期,并且其中一个线程将是第一个知道的线程(因为它将获得401响应)。我们将此线程称为T1
T1将立即重试refreshToken函数,然后重试原始API调用。在这一点上,代码需要确保两件事:


尝试读取AccessToken时,所有新线程都会被阻止-直到刷新它,并在此共享数据结构中提供一个新的AccessToken
401之后不久可能已收到T1的所有其他线程,在调用refreshToken函数时被阻止。



我已经使用了IORef以可变方式存储AccessToken。但是,我不确定是否应该使用单独的MVar保护对refreshToken函数的并发访问。是否已经有内置的数据结构?

最佳答案

我不熟悉该特定的API,但听起来像您可能只想存储令牌,并在MVar中存储一个计数器,该计数器指示刷新了多少次。一个线程负责最初用令牌填充MVar。每个需要令牌的线程都调用readMVar来获取它。

当线程发现令牌已过期时,它将调用tryTakeMVar来控制令牌。如果失败,则说明其他线程已控制,然后该线程返回readMVar。如果成功,它将检查计数器是否符合预期。如果不是,则其他线程已经刷新了令牌并将其放回去。如果是,则刷新令牌,递增计数器,然后将其放在MVar中,然后再继续操作。您将需要谨慎对待锁定协议的异常安全性;有一些MVar功能可以帮助您解决此问题。

如我所描述的,该方案要求一个线程负责初始化。如果您只想在第一次需要令牌时就获取令牌,则必须进行一些小的调整:将Maybe存储在MVar中,并初始化为Nothing

以下代码假定函数acquireTokenrefreshToken分别最初获取令牌并刷新现有令牌。显然,如果这些操作实际上是以相同的方式完成的,则可以进行相应的调整。以下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

10-06 02:43