AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • 主页
  • 系统&网络
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • 主页
  • 系统&网络
    • 最新
    • 热门
    • 标签
  • Ubuntu
    • 最新
    • 热门
    • 标签
  • Unix
    • 最新
    • 标签
  • DBA
    • 最新
    • 标签
  • Computer
    • 最新
    • 标签
  • Coding
    • 最新
    • 标签
主页 / coding / 问题 / 79591505
Accepted
Enlico
Enlico
Asked: 2025-04-25 05:49:56 +0800 CST2025-04-25 05:49:56 +0800 CST 2025-04-25 05:49:56 +0800 CST

在 STM 的背景下,事务日志在概念上是什么样的,以及当事务在几次重试后成功时它会如何演变?

  • 772

例如,考虑这个函数,它可以在 WM 中使用,允许在给定的显示器上将窗口从一个桌面移动到另一个桌面,

moveWindowSTM :: Display -> Window -> Desktop -> Desktop -> STM ()
moveWindowSTM disp win a b = do
  wa <- readTVar ma
  wb <- readTVar mb
  writeTVar ma (Set.delete win wa)
  writeTVar mb (Set.insert win wb)
 where
  ma = disp ! a
  mb = disp ! b

显然还有它的IO包装,

moveWindow :: Display -> Window -> Desktop -> Desktop -> IO ()
moveWindow disp win a b = atomically $ moveWindowSTM disp win a b

然后假设

  • 我们的交易在第三次尝试时成功了,
  • ma因为在第一次尝试时,它会因另一个并发事务提交而失效,该事务在我们的事务之后立即更改了内部的值wa <- readTVar ma,
  • 而在第二次尝试时,它被另一个并发事务无效,该事务开始提交并更改了我们的事务中的一个或ma两个的内容。mbwriteTVar ma (Set.delete win wa)

在这种情况下,交易日志将如何演变,以及验证在什么时候会失败?

Simon Marlow 所著《Haskell 中的并行和并发编程》的相关摘录如下(但 Simon Peyton Jones 所著的论文《美丽的并发性》中也提供了类似的信息):

STM 事务的工作原理是累积事务期间迄今为止发生的操作的日志。日志有三种readTVar用途:writeTVar

  • 通过将writeTVar操作存储在日志中而不是立即将其应用到主内存中,丢弃事务的影响很容易;我们只需丢弃日志即可。因此,中止事务的代价很小且固定。

  • 每个人都readTVar必须遍历日志来检查 是否是TVar由之前的 写入的writeTVar。因此,readTVar是日志长度上的 O(n) 操作。

  • 因为日志包含所有操作的记录readTVar,所以可以使用它来发现事务期间的完整TVars读取集,我们需要知道这些才能实现retry。

当事务结束时,STM 实现会将日志与内存内容进行比较。如果当前内存内容与 读取的值匹配readTVar,则将事务的结果提交到内存;如果不匹配,则丢弃日志并从头开始重新运行事务。此过程以原子方式进行,方法是在TVars事务执行期间锁定所有涉及的 。GHC 中的 STM 实现不使用全局锁;TVars提交期间仅锁定涉及事务的 ,因此对不相交的 集合进行操作的事务TVars可以不受干扰地继续进行。

haskell
  • 2 2 个回答
  • 83 Views

2 个回答

  • Voted
  1. Li-yao Xia
    2025-04-25T14:57:23+08:002025-04-25T14:57:23+08:00

    STM 事务运行完整,将每个事件记录到日志中,最后检查一致性以决定是否提交事务。

    因此,对于您的示例交易

    do
      wa <- readTVar ma
      wb <- readTVar mb
      writeTVar ma (Set.delete win wa)
      writeTVar mb (Set.insert win wb)
    

    每次尝试都运行readTVar一次writeTVar。这些函数会写入日志,记录每个事件是读取还是写入、受影响的 TVar 以及读取或写入的值。

    READ ma a   -- value a is read from ma (note that values are just pointers as far as STM is concerned)
    READ mb b   -- value b is read from mb
    WRITE ma c  -- value c is written to ma
    WRITE mb d  -- value d is written to mb
    

    在该事务期间,其他线程可能会提交写入相同 TVars 的事务。无论如何,当前事务仍将继续进行。

    一旦atomically完成运行事务,它会锁定其日志中出现的所有 TVars,以确保接下来的操作以原子方式发生:

    • atomically检查日志中读取的值是否与 TVars 中的当前值匹配,这意味着它们没有被覆盖(或者其他事务将相同的值写入 TVar,这很好)。

    • 如果日志与 TVar 匹配,则写入事件实际上会应用于其 TVar。这就是“提交”事务的含义。锁被释放并atomically返回。

    • 否则,日志和 TVar 的内容不匹配;日志将被丢弃,锁将被释放,并atomically从头开始重新启动。

    在您的示例中,如果有三次尝试,则意味着创建了三个类似上述的完整日志。前两次日志的唯一区别在于,当atomically回顾这些日志时,它无法假装它们是原子发生的,因此事务会重新启动。

    • 2
  2. Best Answer
    Daniel Wagner
    2025-04-26T03:18:17+08:002025-04-26T03:18:17+08:00

    我认为这样的事情应该可以作为一个相当不错的心理模型:

    data Generational a = Generational
        { generation :: Int
        , value :: a
        }
    instance Eq (Generational a) where (==) = (==) `on` generation
    
    -- choose a generation number that's never been chosen before
    -- e.g. by starting at minBound and incrementing one at each call
    freshGeneration :: IO Int
    
    newtype TVar a = TVar (IORef (Generational a))
    
    unsafeReadTVar :: MonadIO m => TVar a -> m (Generational a)
    unsafeReadTVar (TVar ref) = liftIO $ readIORef ref
    
    unsafeWriteTVar :: Int -> TVar a -> Identity a -> IO ()
    unsafeWriteTVar g (TVar ref) (Identity a) = writeIORef ref (Generational g a)
    
    lock, unlock :: TVar a -> IO ()
    
    -- like Map, but with an extra type argument
    data Map1 (key :: k -> *) (value :: k -> *)
    
    data STMLog = STMLog
        { reads :: Map1 TVar Generational
        , writes :: Map1 TVar Identity
        }
    
    emptyLog :: STMLog
    emptyLog = STMLog empty1 empty1
    
    -- throw away the generation
    readsId :: STMLog -> Map1 TVar Identity
    readsId = fmap1 (Identity . value) . reads
    
    newtype STM a = STM (StateT STMLog IO a)
        deriving (Functor, Applicative, Monad)
    
    readTVar :: TVar a -> STM a
    readTVar v = STM do
       log <- get
       case lookup1 v (writes log) <|> lookup1 v (readsId log) of
            Just (Identity a) -> pure a
            Nothing -> do
                ga <- unsafeReadTVar v
                put log { reads = insert1 v ga (reads log) }
                pure (value ga)
    
    writeTVar :: TVar a -> a -> STM ()
    writeTVar v a = STM $ modify \log ->
        log { writes = insert1 v (Identity a) (writes log) }
    
    atomically :: STM a -> IO a
    atomically (STM act) = do
        (a, log) <- runStateT act emptyLog
        let allVars = keys1 $ union1 (readsId log) (writes log)
        traverse1_ lock allVars
        reads' <- traverseWithKey1 (\v _ -> unsafeReadTVar v) (reads log)
        -- N.B. (==) only compares generations here
        -- (the real implementation probably uses some form
        -- of pointer equality instead of storing generations)
        if reads log == reads'
            then do
                g <- freshGeneration
                traverseWithKey1_ (unsafeWriteTVar g) (writes log)
                traverse1_ unlock allVars
                pure a
            else do
                traverse1_ unlock allVars
                atomically (STM act) -- try again
    

    实际情况可能更加复杂/优化/健壮。我这里根本不考虑处理异常,我假设一次性锁定一组变量很容易,我没有展示具体的实现retry,我也没有考虑是否需要任何内存隔离等等。

    • 2

相关问题

  • 是否有一个模拟“补丁”的类?

  • 状态是箭头吗?

  • 为类型同义词编写函子实例并感到困惑

  • Haskell 模数类似于 JavaScript(考虑符号并且适用于非整数)

  • Haskell 中的 scanl 如何在 Either 列表上工作 - 两种情况的比较

Sidebar

Stats

  • 问题 205573
  • 回答 270741
  • 最佳答案 135370
  • 用户 68524
  • 热门
  • 回答
  • Marko Smith

    重新格式化数字,在固定位置插入分隔符

    • 6 个回答
  • Marko Smith

    为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会?

    • 2 个回答
  • Marko Smith

    VScode 自动卸载扩展的问题(Material 主题)

    • 2 个回答
  • Marko Smith

    Vue 3:创建时出错“预期标识符但发现‘导入’”[重复]

    • 1 个回答
  • Marko Smith

    具有指定基础类型但没有枚举器的“枚举类”的用途是什么?

    • 1 个回答
  • Marko Smith

    如何修复未手动导入的模块的 MODULE_NOT_FOUND 错误?

    • 6 个回答
  • Marko Smith

    `(表达式,左值) = 右值` 在 C 或 C++ 中是有效的赋值吗?为什么有些编译器会接受/拒绝它?

    • 3 个回答
  • Marko Smith

    在 C++ 中,一个不执行任何操作的空程序需要 204KB 的堆,但在 C 中则不需要

    • 1 个回答
  • Marko Smith

    PowerBI 目前与 BigQuery 不兼容:Simba 驱动程序与 Windows 更新有关

    • 2 个回答
  • Marko Smith

    AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String”

    • 1 个回答
  • Martin Hope
    Fantastic Mr Fox msvc std::vector 实现中仅不接受可复制类型 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant 使用 chrono 查找下一个工作日 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor 构造函数的成员初始化程序可以包含另一个成员的初始化吗? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský 为什么 C++20 概念会导致循环约束错误,而老式的 SFINAE 不会? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul C++20 是否进行了更改,允许从已知绑定数组“type(&)[N]”转换为未知绑定数组“type(&)[]”? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann 为什么 {2,3,10} 和 {x,3,10} (x=2) 的顺序不同? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller 在 5.2 版中,bash 条件语句中的 [[ .. ]] 中的分号现在是可选的吗? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench 为什么双破折号 (--) 会导致此 MariaDB 子句评估为 true? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng 为什么 `dict(id=1, **{'id': 2})` 有时会引发 `KeyError: 'id'` 而不是 TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob:MobileAds.initialize() - 对于某些设备,“java.lang.Integer 无法转换为 java.lang.String” 2024-03-20 03:12:31 +0800 CST

热门标签

python javascript c++ c# java typescript sql reactjs html

Explore

  • 主页
  • 问题
    • 最新
    • 热门
  • 标签
  • 帮助

Footer

AskOverflow.Dev

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve