Difference between revisions of "Talk:Parallelism vs. Concurrency"

From HaskellWiki
Jump to navigation Jump to search
m (Terminology changed)
m (More changes to examples)
 
(2 intermediate revisions by the same user not shown)
Line 1: Line 1:
 
=== Parallelism vs concurrency: what's the difference? ===
 
=== Parallelism vs concurrency: what's the difference? ===
   
'''''Visible'' side effects:'''
+
<b><i>Visible</i> side effects:</b>
   
 
* Have a look at this <del>ugly eysore</del> "prototype definition" of <code>par</code>:
 
* Have a look at this <del>ugly eysore</del> "prototype definition" of <code>par</code>:
Line 21: Line 21:
   
 
Assuming:
 
Assuming:
* <code>x</code> is ''well-defined'' (it contains no <code>unsafe...</code> calls),
+
* <code>x</code> is <i>well-defined</i> (it contains no <code>unsafe...</code> calls),
* <code>x</code> is ''well-behaved'' (not throwing exceptions or causing errors);
+
* <code>x</code> is <i>well-behaved</i> (not throwing exceptions or causing errors);
   
 
then:
 
then:
Line 36: Line 36:
 
|<haskell>
 
|<haskell>
 
forkIO :: IO () -> IO ThreadId
 
forkIO :: IO () -> IO ThreadId
forkIO act = do t <- unsafeInterleaveIO act
+
forkIO act = do v <- newEmptyMVar
case attachThreadId t of
+
let thr = do i <- myThreadId
Nothing -> itsThreadId t
+
putMVar v i
Just i -> par t (return i)
+
act
  +
z <- unsafeInterleaveIO thr
  +
par z (takeMVar v)
 
</haskell>
 
</haskell>
   
Line 45: Line 47:
   
 
<haskell>
 
<haskell>
attachThreadId :: a -> IO (Maybe ThreadId)
+
myThreadId :: IO ThreadId
itsThreadId :: a -> IO ThreadId
+
newEmptyMVar :: IO (MVar a)
  +
putMVar :: MVar a -> a -> IO ()
  +
takeMVar :: MVar a -> IO a
  +
par :: a -> b -> b
 
</haskell>
 
</haskell>
   
  +
Assuming <code>par</code>, <code>newEmptyMVar</code>, <code>putMVar</code>
Assuming:
 
* <code>par</code>, <code>attachThreadId</code> and <code>itsThreadId</code> are primitive,
+
and <code>takeMVar</code> are primitive,
* <code>attachThreadId</code> would return <code>Nothing</code> if it's argument already has been assigned a <code>ThreadId</code>;
 
   
 
then:
 
then:
# A new <code>ThreadId</code> is assigned to the new (and suspended) value <code>t</code>;
+
# An unused <code>MVar</code> is obtained: <code>v</code>;
# evaluating <code>par t i</code> causes <code>t</code> to be added to the work-queue by the implementation;
+
# the parameter <code>act</code> is used to build <code>thr</code> which will store its <code>ThreadId</code> in <code>v</code>;
# the <code>ThreadId</code> for <code>t</code> is then returned;
+
# <code>z</code>, the future result of <code>thr</code>, is then retrieved lazily by using <code>unsafeInterleaveIO</code>;
  +
# <code>par</code> then presents <code>z</code> for parallel evaluation;
# Some time later, the implementation discovers that a <code>ThreadId</code> has been attached to <code>t</code> and uses the <code>ThreadId</code> to immediately start a new thread for evaluating <code>t</code>;
 
  +
# </code>putMVar</code> waits while <code>v</code> is empty;
 
  +
# </code>putMVar</code> then returns the contents of <code>v</code>.
  +
 
This is parallelism, but having visible side effects - it behaves like elementary concurrency.
 
This is parallelism, but having visible side effects - it behaves like elementary concurrency.
 
|}
 
|}
Line 66: Line 72:
 
* While the use of <code>unsafeInterleaveIO</code> may annoy some, it being one of the earlier Haskell extensions means it's widely available.
 
* While the use of <code>unsafeInterleaveIO</code> may annoy some, it being one of the earlier Haskell extensions means it's widely available.
   
For now, using a primitive <code>par</code> (and others) to define <code>forkIO</code> looks like the simplest option...but if using <code>unsafeInterleaveIO</code> ''really does'' annoy you, how about [[IO, partible-style|this]]:
+
For now, using a primitive <code>par</code> (and others) to define <code>forkIO</code> looks like the simplest option...but if using <code>unsafeInterleaveIO</code> <i>really does</i> annoy you, how about [[IO in action|this]]:
   
 
:<haskell>
 
:<haskell>
 
forkIO :: (OI -> ()) -> OI -> ThreadId
 
forkIO :: (OI -> ()) -> OI -> ThreadId
forkIO act u = let !(u1:u2:u3:_) = parts u in
+
forkIO act u = let !(u1:u2:u3:u4:u5:_) = parts u in
let t = act u1 in
+
let !v = newEmptyMVar u1
case attachThreadId t u2 of
+
let z = let !i = myThreadId u2 in
Nothing -> itsThreadId t u3
+
let !_ = putMVar v i u3 in
Just i -> par t i
+
let !_ = act u4 in
  +
()
  +
in par z (takeMVar v u5)
 
</haskell>
 
</haskell>
   

Latest revision as of 20:14, 2 January 2024

Parallelism vs concurrency: what's the difference?

Visible side effects:

  • Have a look at this ugly eysore "prototype definition" of par:
par     :: a -> b -> b
par x y =  case
             unsafeLocalState (forkIO (evaluate x >> return ()))
           of
             !_ -> y

where:

      evaluate :: a -> IO a
      forkIO   :: IO () -> IO ThreadId

Assuming:

  • x is well-defined (it contains no unsafe... calls),
  • x is well-behaved (not throwing exceptions or causing errors);

then:

  1. forkIO attaches a ThreadId to its argument, adds it to the work-queue and returns the identifier;
  2. par then returns y;
  3. Some time later, forkIO's argument is called, causing evaluate to start evaluating x.

If y is still being evaluated when the evaluation of x commences, then we have concurrency, but with no visible side-effects - it behaves like elementary parallelism.

  • Now have a look at this nearly-as-ugly prototype definition for spawnIO forkIO:
forkIO     :: IO () -> IO ThreadId
forkIO act =  do v <- newEmptyMVar
                 let thr = do i <- myThreadId
                              putMVar v i
                              act
                 z <- unsafeInterleaveIO thr
                 par z (takeMVar v)

where:

      myThreadId   :: IO ThreadId
      newEmptyMVar :: IO (MVar a)
      putMVar      :: MVar a -> a -> IO ()
      takeMVar     :: MVar a -> IO a
      par          :: a -> b -> b

Assuming par, newEmptyMVar, putMVar and takeMVar are primitive,

then:

  1. An unused MVar is obtained: v;
  2. the parameter act is used to build thr which will store its ThreadId in v;
  3. z, the future result of thr, is then retrieved lazily by using unsafeInterleaveIO;
  4. par then presents z for parallel evaluation;
  5. putMVar waits while v is empty;
  6. putMVar then returns the contents of v.

This is parallelism, but having visible side effects - it behaves like elementary concurrency.

Can either prototype definition potentially go mainstream?

  • As shown by it's type signature, par is meant to have no visible effects: avoiding the use of unsafeLocalState means making it primitive;
  • While the use of unsafeInterleaveIO may annoy some, it being one of the earlier Haskell extensions means it's widely available.

For now, using a primitive par (and others) to define forkIO looks like the simplest option...but if using unsafeInterleaveIO really does annoy you, how about this:

forkIO       :: (OI -> ()) -> OI -> ThreadId
forkIO act u =  let !(u1:u2:u3:u4:u5:_) = parts u in
                let !v = newEmptyMVar u1
                let z  = let !i = myThreadId u2 in
                         let !_ = putMVar v i u3 in
                         let !_ = act u4 in
                         ()
                in  par z (takeMVar v u5)

-- Atravers Tue Apr 20 06:04:10 UTC 2021