Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concurrency primitimes and thread safety #34

Open
tomjaguarpaw opened this issue Jan 7, 2025 · 4 comments
Open

Concurrency primitimes and thread safety #34

tomjaguarpaw opened this issue Jan 7, 2025 · 4 comments

Comments

@tomjaguarpaw
Copy link
Owner

We don't have any Bluefin-specific concurrency primitives and just rely on MonadUnliftIO to give us access to IO currency primitives. This seems dangerous, firstly in light of #29, but also because we could actually use Bluefin's type system to forbid thread-unsafe access to resources.

By way of comparison, effectful seems fairly lax. See haskell-effectful/effectful#292.

#!/usr/bin/env cabal
{- cabal:
  build-depends: base, effectful==2.5.1.0, async
-}
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE LambdaCase #-}

import Control.Concurrent
import Control.Concurrent.Async
import Data.IORef
import Effectful
import Effectful.Dispatch.Dynamic
import Effectful.State.Dynamic

evalState ::
  (IOE :> es) =>
  s ->
  Eff (State s : es) a ->
  Eff es a
evalState s0 m = do
  v <- liftIO (newIORef s0)
  reinterpret id (ioState v) m

ioState ::
  (IOE :> es) =>
  IORef s ->
  LocalEnv localEs es ->
  State s (Eff localEs) a ->
  Eff es a
ioState v env = \case
  Get -> liftIO (readIORef v)
  Put s -> liftIO (writeIORef v s)
  State f -> liftIO $ do
    s <- readIORef v
    let (r, s') = f s
    writeIORef v s
    pure r
  StateM _ -> error "Dunno"

useStateConcurrently ::
  (State Int :> es, IOE :> es) => Eff es ()
useStateConcurrently = do
    withEffToIO (ConcUnlift Persistent Unlimited) $
      \effToIO -> do
        concurrently
          ( effToIO $ do
              liftIO (threadDelay 500)
              s <- get @Int
              put (s + 1)
          )
          ( effToIO $ do
              s <- get @Int
              liftIO (threadDelay 1000)
              put (s * 2)
          )

    (liftIO . print) =<< get @Int

-- We "want" the result to be either
--
-- - 12 (== (5 + 1) * 2), or
--
-- - 11 (== (5 * 2) + 1)
--
-- but we get
--
-- % cabal run test-effectful-thread-unsafe.hs
-- 10
main :: IO ()
main = runEff $ do
  evalState @_ @Int 5 $ do
    useStateConcurrently
@jeukshi
Copy link

jeukshi commented Jan 10, 2025

Another problem with the effectful API is that threads can outlive effectful operations. I'm cheating here with IOE, but long computation would suffice.

greatEscape :: IO ()
greatEscape = do
    hSetBuffering stdout LineBuffering
    res <- runEff . runConcurrent $ do
        _ <- async $ do
            liftIO $ forever do
                print "still alive"
                threadDelay 2_000
        return "eff done"
    print res
    threadDelay 10_000

This is mentioned in the documentation, but I don't think it's necessary as a primitive, it can be done with IOE anyway, if needed. I'd rather go with structured concurrency from the start.

Speaking of which, I did some experiments with Ki. The Naive version just exposes that this is IORefs all the way down, which is yikes.

This is my current best idea, with explicit copy/share through a typeclass. I did not think this through and did not use it for anything, but maybe it can serve as an inspiration.

@tomjaguarpaw
Copy link
Owner Author

Thanks! I have some ideas forthcoming about structured concurrency in Bluefin which give thread safety by construction, so watch this space. I have some concerns about the ki API, but think I have worked out how to make it type safe with Bluefin.

@jeukshi
Copy link

jeukshi commented Jan 10, 2025

Oh, for the record, this doesn't with my wrapper.

useFile = runEff \io -> do
    runScope io \scope -> do
        _ <- BIO.withFile io "/dev/null" WriteMode \h -> do
            _ <- forkWithNewEff scope do
                BIO.hPutStr h "foo"
                -- • Ambiguous type variable ‘forkEs0’ arising from a use of ‘BIO.hPutStr’
               --    prevents the constraint ‘(e2 :> forkEs0)’ from being solved.
            pure ()
        pure ()

But to be fair, nothing works.

Anyway looking forward to what is cooking!

@tomjaguarpaw
Copy link
Owner Author

You can have a look at branch ki for a rough sketch of what I have so far. I think what's exported from Bluefin.Ki is solid, but it needs some documentation and some examples. (There is a long example but it's not really clear what's going on.)

The thread safety story is this:

  • Bluefin.Ki is thread safe
  • You can only directly use resources within your own Scope
  • You can access resources from higher Scopes using exclusively
  • exclusively locks everything on the path between you and the Scope you're accessing
  • This is a very blunt hammer, but thread safe and deadlock free by construction
  • For "lock-free" communication between threads use STM
  • Still to come: a way of cloning those resources for which it's fine to pass copies of them down to lower scopes (for example an "HTTP file downloader" -- there's no problem with running multiple of them at a time) and a way of unsafely cloning resources, as an escape hatch

If you're feeling adventurous you might want to check it out. I envisage writing up a better explanation over the coming weeks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants