In reactive-banana, is it safe to trigger handler actions from multiple threads?
Asked Answered
E

2

6

Is it safe to trigger the fire action in

(addHandler, fire) <- newAddHandler

from a different thread from which the reactive-banana graph was compiled?

Eponymy answered 16/9, 2015 at 14:53 Comment(0)
R
4

Yes, this is safe, but there is the caveat that @Cirdec mentioned.

For conreteness, consider the following example that creates an event network using the addHandler in a separate thread and then calls fire repeatedly in the main thread

import Control.Concurrent (myThreadId, threadDelay, forkIO)

main = do
    ...
    (addHandler, fire) <- newAddHandler

    let networkDescription :: MomentIO ()
        networkDescription = do
           e <- fromAddHandler addHandler
           ...
           reactimate $ (print =<< myThreadId) <$ e   -- reactimate

    forkIO $ do
        network <- compile networkDescription
        actuate network
    ...
    forever $ do                                      -- event loop
        threadDelay (10^6)
        fire ()

(See the documentation "Terminating the program" in Control.Concurrent for why I've put the event loop in the main thread as opposed to putting the network in the main thread.)

In this and similar situations, the following will hold:

  • The IO actions executed by the reactimate will be run in the thread that calls fire, not in the thread where the network was compiled. This is what @Cirdec already mentioned.
  • If there were a second thread also calling fire, then it could potentially interleave with other calls to fire, i.e. the program could be calling fire twice concurrently. Then,
    • Reactive-banana uses a lock to ensure that Behaviors and Events are updated consistently. You can treat them as pure functions Time -> a and lists [(Time,a)] as usual.
    • However, the IO actions from the reactimates may interleave. In other words, the pure FRP part will stay pure, but the actual IO is subject to concurrency as usual.
Ross answered 17/9, 2015 at 10:3 Comment(0)
T
3

Firing the fire handler itself is safe; it reads an IORef that is being updated atomically and runs each of the added handlers in the current thread. Whether or not that's safe will depend on what handlers have been added to the addHandler.

Using the addHandler in interpretAsHandler, fromAddHandler, or fromChanges should be safe. Nothing I know of in reactive-banana has any thread affinity, and even if it did, these are what newAddHandler was made for, so it should be safe anyway.

What you need to be careful of is the IO () actions executed by reactimate. If you need to reactimate IO actions that need to be run in a specific thread (for OpenGL output, etc), you need to only produce IO () actions that will send their data to that thread. In this complete OpenGL example for reactive-banana the IO () actions for OpenGL output, which have thread affinity, are run in the OpenGL thread. Instead ofreactimateing the Event (IO ()) executing them directly they are added to an IORef

whenIdleRef <- newIORef (return ())
let
    addWhenIdle :: IO () -> IO ()
    addWhenIdle y = atomicModifyIORef' whenIdleRef (\x -> (x >> y, ()))
    runWhenIdle :: IO ()
    runWhenIdle = atomicModifyIORef' whenIdleRef (\x -> (return (), x)) >>= id

let networkDescription  :: forall t. Frameworks t => Moment t ()
    networkDescription  = do

        reactimate $ fmap addWhenIdle (whenIdle outputs)
                     ^                ^
                     |                Event (IO ())
                     Stuff the event into an IORef

The IORef holding which IO () actions to run is read and each of all the actions are run in a context that I know is in the OpenGL thread.

idleCallback $= Just (do           -- will be executed in the OpenGL thread when it's idle
    getCurrentTime >>= raiseTime
    runWhenIdle                    -- run those `IO ()` actions in this thread
    postRedisplay Nothing)
Thrombokinase answered 16/9, 2015 at 16:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.