Search code examples
multithreadingasynchronoushaskellparallel-processing

Haskell Parallel execution of pure code in async


I am trying to work out the async model of Haskell and have troubles matching the known concepts against what Haskell does. I have the following code:

module Main where

import Control.Concurrent.Async (concurrently)

main :: IO ()
main = do
  putStrLn "Hello, Haskell!"
  (a, b) <- concurrently (pure $ myFibonaci 45) (pure $ myFibonaci 42)
  print a
  putStrLn "----"
  print b

myFibonaci :: Integer -> Integer
myFibonaci 0 = 0
myFibonaci 1 = 1
myFibonaci n = myFibonaci (n - 1) + myFibonaci (n - 2)

Not sure why but logs are printed in the following order:

267914296 -- value of a
----

And only then it starts computing the value of b

267914296
----
102334155 -- value of b

I am a little puzzled since concurrently should in my understanding spawn 2 green threads which could in turn be mapped to different system threads and end up executing on different cores. Meaning that by the time I get the value of a the value of b should have theoretically (if scheduling delays are not due) be computed. I am running code with -threaded GHC options so this shouldn't be the problem. Is this a case of Haskell lazyness? I suspect the better solution would be to use Control.Parallel but what is the difference between async and Control.Parallel then? I am pretty sure Go which uses a similar model doesn't make such distinctions


Solution

  • This is due to lazyness. The IO action you are executing is pure, not myFibonaci xy, therefore at the line (a, b) <- ... both elements of the tuple are thunks which are evaluated when printing. Use @jonpurdy's solution: $!. Two things to consider:

    • Control.Parallel API, will force you to choose which "level of lazyness" you want for your parallel execution, via rseq, rdeepseq, etc..
    • async code isn't the same as parallel code. The former can run (and most likely should run) in one core doing some sort of context switching, the later must run in multiple cores. I'd recommend parallel and concurrent programming in haskell by S.Marlow who is the guy that wrote the par/conc runtime for haskell.

    below you have the fix to your code and the strategies approach

    import Control.Concurrent.Async (concurrently)
    import Data.Time.Clock (getCurrentTime)
    import Control.Exception
    import Control.Parallel.Strategies
    
    -- just an utility
    measure :: Show a => a -> IO ()
    measure a = do
      getCurrentTime >>= print
      print a
      getCurrentTime >>= print
    
    main :: IO ()
    main = do
      putStrLn "Hello, Haskell!"
      -- if you change $! by $ you'll notice measure b is much longer because
      -- computation is done on measure b but with $!, both measure are the same 
      -- since the calculation is done here
      (a, b) <- concurrently (pure $! myFibonaci 15) (pure $! myFibonaci 35)
      measure a
      putStrLn "----"
      measure b
    
      putStrLn "********"
      getCurrentTime >>= print
      -- strategies interface force you to choose the level of lazyness
      --        |- eval in IO in parallel
      --        |        |- this pure computation
      --        |        |                              |- with strategy "parallel tuples with full evaluation of each side of the tuple"
      result <- usingIO (myFibonaci 14, myFibonaci 34) (evalTuple2 rdeepseq rdeepseq)
      print result
      getCurrentTime >>= print
    
    myFibonaci :: Integer -> Integer
    myFibonaci 0 = 0
    myFibonaci 1 = 1
    myFibonaci n = myFibonaci (n - 1) + myFibonaci (n - 2)