Multithreading 跨线程存储任意函数调用
我正试图编写一个库来重现Qt的线程语义:信号可以连接到插槽,所有插槽都在一个已知线程中执行,因此绑定到同一线程的插槽彼此之间是线程安全的 我有以下API:Multithreading 跨线程存储任意函数调用,multithreading,haskell,types,signals-slots,Multithreading,Haskell,Types,Signals Slots,我正试图编写一个库来重现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 ()) ->
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
问题是从emit
到execute
。该参数需要以某种方式存储在运行时,然后执行IO操作,但我似乎无法通过类型检查器
我需要的东西:
- :使整个事情变得非常脆弱,而且我还没有找到一种方法在
上执行正确键入的IO操作。有,但它是纯的动态
- :我需要执行传递给
的函数,而不是基于类型的任意函数mkSlot
- 当前位置我不够聪明,弄不明白
我缺少什么?首先,您确定插槽真的要在特定线程中执行吗?在Haskell中编写线程安全的代码很容易,而GHC中的线程非常轻量级,因此将所有事件处理程序执行绑定到特定的Haskell线程并不会获得太多好处 另外,
mkSlot
的回调不需要指定插槽本身:您可以使用在回调中绑定插槽,而无需将打结问题添加到mkSlot
无论如何,你不需要像这些解决方案那样复杂的东西。我希望当你谈论存在类型时,你会考虑通过TChan
(你在评论中提到过)发送类似(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)
如果这可能是一个瓶颈,您可能需要类似于Map Unique(Slot a)
的东西,而不是[Slot a]
因此,这里的解决方案是:(a)认识到你有一些基本上基于可变状态的东西,并使用可变变量来构造它;(b) 要意识到函数和IO操作与其他操作一样都是一流的,所以在运行时构建它们不需要做任何特殊的工作:)
顺便说一下,我建议不要从定义它们的模块中导出它们的构造函数,从而保持
Signal
和Slot
的实现是抽象的;毕竟,有很多方法可以在不改变API的情况下解决此问题。主要思想是,在同一线程中执行的插槽彼此之间是线程安全的。不管是什么样的线。是的,我有自己的事件循环,它什么也不做,只监听TChan并执行通过的任何操作。@GyörgyAndrasek:啊,我明白了:每个插槽都在同一个线程中运行,而不是每个插槽都在自己的线程中运行。我怀疑这是否是一件好事——Haskell有很好的线程支持,而且像STM这样的东西通常比线程安全代码更难编写线程不安全的代码——但我会适当地修改我的答案。不完全正确。您有多组插槽,在其组的线程中执行。在Qt中,这是通过对象完成的。moveToThread(thread)
,其中slot是对象的方法。其效果是,您可以在GUI线程和递归目录爬虫线程之间进行干净的通信,而不会在整个代码库中抛出线程问题。这做得很好,将问题重新表述为关键思想,并勾勒出实现和所有方面。:]@bdonlan:大概他们希望在插槽代码中产生副作用,就像事件处理程序经常做的那样;GHC尚未实现所需的时间机器语义,以允许插槽组上的锁可以工作,但可能并不比读取TChan的专用线程简单@C.A.麦肯:谢谢!:)
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