Native Threads in the RTS

Alastair Reid alastair@reid-consulting-uk.ltd.uk
29 Nov 2002 08:34:15 +0000


[Note: I'm consistently using 'foreign thread' instead of 'native
thread'.  The Haskell-spec necessarily treats Haskell as the centre of
the universe.  So what a Linux kernel hacker might think of as a
'native thread' is really quite foreign to Haskell.  Feel free to
ignore this little experiment with the language.]

> *) Exactly one Haskell thread associated with the native thread is
> executing. All other associated Haskell threads are blocked. No
> foreign code is being executed by the native thread.  

This isn't quite right - or, at least, needs clarified.

Consider Haskell functions a,b,c,d and C functions A,B,C,D and a call
pattern

  a -> A -> b -> B -> c -> C -> d -> D

That is, a calls A, calls b, calls B, calls ...

Suppose we want A,B,C,D executed by the same foreign thread.

Each of a,b,c,d are executed by different Haskell threads (because a
new Haskell thread is spawned for each call into Haskell) so we have
multiple Haskell threads associated with a single foreign thread.  Now,
when D is called, which of these threads is 'executing' and which are
'blocked'?

I think the quoted text assumes that a,b,c,d are blocked during the
call to D.

The GHC implementation might well perform a context switch on making
the calls into C so we could, perhaps, say that a Haskell thread is
'blocked' while making an ffi call.  But, for normal function calls
(i.e., all in Haskell or all in C, no ffi stuff), we don't say that
the caller is 'blocked' until the callee returns.  I think it's better
to reserve the word is 'blocked' for its normal usage and avoid
possibly over-specifying what implementations must do to implement the
spec.

Possible rewording:

  Definitions:

    Let $f$ be a foreign thread.

    $uses(f)$ is the number of foreign calls by Haskell threads bound to $f$.
    $bindees(f)$ is the number of Haskell threads bound to $f$.

  Invariant:

    $0 <= bindees(f) - uses(f) <= 1$

  Proof:

    (Should be possible by) structural induction over relevant IO
    operations (forkIO, forkNativeIO, etc.)


This needs careful interpretation if we want to be able to bind
finalizers to foreign threads.  In particular, if a finalizer is bound
to a foreign thread, we don't increment 'bindees(f)' until the
finalizer starts and we don't start the finalizer unless either:

  bindees(f) - uses(f) == 0   

or, maybe even,

  bindees(f) == 0   



Or, we can adopt a much weaker semantics than Wolfgang intended and have:

    $0 <= bindees(f) - uses(f)$

This would allow several currently running, active Haskell threads to
all be bound to the same foreign thread.  When any of these threads
makes a foreign call, the other threads could all keep running and
they would only block if they too tried to make foreign calls.  From
an implementation point of view, this requires:

1) That foreign threads are _not_ used to execute Haskell code.

2) That we maintain a lock on foreign threads so that only one
   Haskell thread tries to use it at a time.

I see two potential problems with this (but would like to hear which,
if either, dominates your thoughts):

1) If foreign threads cannot be used to execute Haskell code, foreign
   calls require (OS-level) context switches which are expensive.

2) Adding an implicit lock to foreign calls might surprise programmers.


--
Alastair