Skip to content

Does effectful have a policy on thread safety? #292

@tomjaguarpaw

Description

@tomjaguarpaw

I was wondering if effectful has a policy on the thread safety of code written using it. Currently the provided functionality seems to support

make it easier to write thread-safe code than thread-unsafe code

(which seems perfectly reasonable). I wonder if this is actually the intended policy. In particular, it's quite easy to write code that doesn't look obviously thread-unsafe, but actually is. For example, something like the below.

The reason this isn't obviously thread unsafe is that the use of Effectful.State.Dynamic in useStateConcurrently and its interpretation as an IORef in evalState can be very far apart in a codebase. That is, although it's obvious at the definition site of useStateConcurrently that State Int must not be handled by a thread-unsafe handler, and although it's obvious at the definition site of evalState that it must not be used to handle an effect that will be used concurrently, if those two definitions are far apart then we have to keep track of their incompatibility manually, rather than having the type system guide us.

In particular, it is possible to interpret any dynamic effect, even one that might be used concurrently, in a non-thread safe way.

As far as I can tell, effectful's policy is essentially "Use either Effectful.State.Local or Effecful.State.Dynamic whenever a state effect might be used concurrently", and effectful's "copy on fork" makes this very convenient in practice, hence "make it easier to write thread-safe code than thread-unsafe code". But it's still easy to get it wrong. One could imagine a more restrictive type system that doesn't allow this kind of sharing between threads, thus my question: is there a particular policy?

#!/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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions