simonmar / async

Run IO operations asynchronously and wait for their results
BSD 3-Clause "New" or "Revised" License
322 stars 65 forks source link

`concurrently` doesn't compose well with `forever` #113

Open infinity0 opened 4 years ago

infinity0 commented 4 years ago

Suppose I want to compose a bunch of reads in the way select(2) does, then it's very tempting to write something like forever $ race (forever $ read a) (forever $ read b).

However looking at the implementation this won't work correctly, since new threads are created and killed on every call to race. So when one action "wins", the other actions are killed, but they might have consumed their streams in the meantime.

It's possible to create an implementation that does work properly, although it has to compose with forever inside the action - something like:

foreverInterleave :: [IO r] -> IO (IO r, IO (), IO())

where running (_1, _2, _3) <- foreverConcurrently reads would itself give further actions to be run, that behave as follows:

  1. an action that gets any of the results, whichever arrives first, in order
  2. an action that waits for any ongoing actions to finish, as well as all their results to be consumed ("graceful shutdown") by the caller calling (1)
  3. an action that kills the ongoing threads, potentially dropping any unconsumed values, but can return immediately ("ungraceful shutdown")

The implementation ideally would avoid running the original actions, unless (1) has indicated a concrete demand for them.

Does that seem like something sensible for this library? I will have to write code that does this anyway in one of my projects, I can contribute it here for others too. Note that the existance of this function here might help other people avoid bugs in their code, since the incorrect-but-obvious version forever $ race (forever $ read a) (forever $ read b) at a first glance seems perfectly acceptable.