【问题标题】:Storing arbitrary function calls across threads跨线程存储任意函数调用
【发布时间】:2011-12-31 04:27:46
【问题描述】:

我正在尝试编写一个旨在重现 Qt 线程语义的库:信号可以连接到插槽,并且所有插槽都在已知线程中执行,因此绑定到同一线程的插槽彼此之间是线程安全的。

我有以下 API:

data Signal a = Signal Unique a
data Slot a = Slot Unique ThreadId (a -> IO ())

mkSignal :: IO (Signal a)
mkSlot   :: ThreadId -> (Slot a -> a -> IO ()) -> IO (Slot a)

connect :: Signal a -> Slot a -> IO ()

-- callable from any thread
emit :: Signal a -> a -> IO ()

-- runs in Slot's thread as a result of `emit`
execute :: Slot a -> a -> IO ()
execute (Slot _ _ f) arg = f arg

问题是从emitexecute。参数需要在运行时以某种方式存储,然后执行 IO 操作,但我似乎无法通过类型检查器。

我需要的东西:

  1. 类型安全:信号不应连接到期望不同类型的插槽。
  2. 类型无关:任何给定类型都可以有多个插槽(也许这可以通过 newtype 和/或 TH 放宽)。
  3. 易于使用:由于这是一个库,因此信号和槽应该易于创建。

我尝试过的事情:

  • Data.Dynamic:让整个事情变得非常脆弱,我还没有找到一种方法来对Dynamic 执行正确键入的 IO 操作。有dynApply,但很纯粹。
  • Existential types:我需要执行传递给mkSlot 的函数,而不是基于类型的任意函数。
  • Data.HList:我不够聪明,无法弄清楚。

我错过了什么?

【问题讨论】:

    标签: multithreading haskell types signals-slots


    【解决方案1】:

    首先,您确定 Slots 真的想在特定线程中执行吗?在 Haskell 中编写线程安全的代码很容易,而且 GHC 中的线程非常轻量级,因此将所有事件处理程序执行绑定到特定的 Haskell 线程并不会获得太多收益。

    另外,mkSlot 的回调不需要给定 Slot 本身:您可以使用 recursive do-notation 在其回调中绑定 slot,而无需担心与 mkSlot 打结。

    无论如何,您不需要像那些解决方案那样复杂的东西。我希望当您谈论存在类型时,您正在考虑通过 TChan(您提到在 cmets 中使用)发送类似 (a -> IO (), a) 的内容并在另一端应用它,但您希望 TChan 到接受任何 a 的这种类型的值,而不仅仅是一个特定的 a。这里的关键见解是,如果你有 (a -> IO (), a) 并且不知道 a 是什么,你唯一能做的就是将函数应用于值,给你一个 IO () - 所以我们可以直接通过频道发送!

    这是一个例子:

    import Data.Unique
    import Control.Applicative
    import Control.Monad
    import Control.Concurrent
    import Control.Concurrent.STM
    
    newtype SlotGroup = SlotGroup (IO () -> IO ())
    
    data Signal a = Signal Unique (TVar [Slot a])
    data Slot a = Slot Unique SlotGroup (a -> IO ())
    
    -- When executed, this produces a function taking an IO action and returning
    -- an IO action that writes that action to the internal TChan. The advantage
    -- of this approach is that it's impossible for clients of newSlotGroup to
    -- misuse the internals by reading the TChan or similar, and the interface is
    -- kept abstract.
    newSlotGroup :: IO SlotGroup
    newSlotGroup = do
      chan <- newTChanIO
      _ <- forkIO . forever . join . atomically . readTChan $ chan
      return $ SlotGroup (atomically . writeTChan chan)
    
    mkSignal :: IO (Signal a)
    mkSignal = Signal <$> newUnique <*> newTVarIO []
    
    mkSlot :: SlotGroup -> (a -> IO ()) -> IO (Slot a)
    mkSlot group f = Slot <$> newUnique <*> pure group <*> pure f
    
    connect :: Signal a -> Slot a -> IO ()
    connect (Signal _ v) slot = atomically $ do
      slots <- readTVar v
      writeTVar v (slot:slots)
    
    emit :: Signal a -> a -> IO ()
    emit (Signal _ v) a = atomically (readTVar v) >>= mapM_ (`execute` a)
    
    execute :: Slot a -> a -> IO ()
    execute (Slot _ (SlotGroup send) f) a = send (f a)
    

    这使用TChan 将操作发送到每个插槽所绑定的工作线程。

    请注意,我对 Qt 不是很熟悉,所以我可能错过了模型的一些微妙之处。您也可以通过以下方式断开插槽:

    disconnect :: Signal a -> Slot a -> IO ()
    disconnect (Signal _ v) (Slot u _ _) = atomically $ do
      slots <- readTVar v
      writeTVar v $ filter keep slots
      where keep (Slot u' _) = u' /= u
    

    如果这可能是一个瓶颈,你可能想要Map Unique (Slot a) 而不是[Slot a]

    因此,这里的解决方案是 (a) 认识到您有一些基本上基于可变状态的东西,并使用可变变量来构造它; (b) 意识到函数和 IO 动作就像其他所有东西一样是一流的,所以你不必做任何特殊的事情来在运行时构造它们:)

    顺便说一句,我建议保留SignalSlot 的实现抽象,不要从定义它们的模块中导出它们的构造函数;毕竟,有很多方法可以在不更改 API 的情况下解决这种方法。

    【讨论】:

    • 主要思想是在 same 线程中执行的槽彼此之间是线程安全的。它是什么类型的线程并不重要。是的,我有自己的事件循环,除了监听 TChan 并执行通过的任何内容之外什么都不做。
    • @GyörgyAndrasek:啊,我明白了:每个 插槽都在相同 线程中运行,而不是每个插槽都在自己的线程中运行。我怀疑这是否是一件好事——Haskell 具有非常好的线程支持,并且使用 STM 之类的东西,编写线程不安全的代码通常比编写线程安全的代码更难——但我会适当地修改我的答案。
    • 不完全。您有一组插槽,在其组的线程中执行。在 Qt 中,这是通过object.moveToThread(thread) 完成的,其中槽是对象的方法。效果是您可以在例如之间清晰地交流。一个 GUI 线程和一个递归目录爬虫线程,不会在整个代码库中引发线程问题。
    • 做得很好,将问题重新表述为关键思想,并勾画出一个实现等等。 :]
    • @bdonlan:大概他们想在插槽代码中引起副作用,就像事件处理程序经常做的那样; GHC 尚未实现所需的时间机器语义以允许instance MonadIO STM :) 插槽组上的锁定将起作用,但可能并不比读取TChan 的专用线程简单。 @C.A.McCann:谢谢! :)
    猜你喜欢
    • 2017-04-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-09
    • 2020-12-31
    • 1970-01-01
    相关资源
    最近更新 更多