FFI, safe vs unsafe
Wolfgang Thaller
wolfgang.thaller at gmx.net
Mon Apr 3 14:00:33 EDT 2006
Sorry for the length of this. There are three sections: the first is
about how I don't like for "nonconcurrent" to be the default, the
second is about bound threads and the third is about implementing
concurrent reentrant on top of state threads.
> no, state-threads, a la NSPR, state-threads.sf.net, or any other of a
> bunch of implementations.
Ah. I was thinking of old-style GHC or hugs only, where there is one
C stack and only the Haskell state is per-haskell-thread. My bad.
So now that I know of an implementation method where they don't cause
the same problems they used to cause in GHC, I am no longer opposed
to the existance of nonconcurrent reentrant imports.
To me, "nonconcurrent" is still nothing but a hint to the
implementation for improving performance; if an implementation
doesn't support concurrent reentrancy at all, that is a limitation of
the implementation.
I think that this is a real problem for libraries; library writers
will have to choose whether they preclude their library from being
used in multithreaded programs or whether they want to sacrifice
portability (unless they spend the time messing around with cpp or
something like it).
Some foreign calls are known never to take much time; those can be
annotated as nonconcurrent. For calls that might take nontrivial
amounts of time, the question whether they should be concurrent or
not *cannot be decided locally*; it depends on what other code is
running in the same program.
Maybe the default should be "as concurrent as the implementation
supports", with an optional "nonconcurrent" annotation for
performance, and an optional "concurrent" annotation to ensure an
error/warning when the implementation does not support it. Of course,
implementations would be free to provide a flag *as a non-standard
extension* that changes the behaviour of unannotated calls.
==== Bound Threads ====
In GHC, there is a small additional cost for each switch to and from
a bound thread, but no additional cost for actual foreign call-outs.
For jhc, I think you could implement a similar system where there are
multiple OS threads, one of which runs multiple state threads; this
would have you end up in pretty much the same situation as GHC, with
the added bonus of being able to implement foreign import
nonconcurrent reentrant for greater performance.
If you don't want to spend the time to implement that, then you could
go with a possibly simpler implementation involving inter-thread
messages for every foreign call from a bound thread, which would of
course be slow (that's the method I'd have recommended to hugs).
If the per-call cost is an issue, we could have an annotation that
can be used whenever the programmer knows that a foreign function
does not access thread-local storage. This annotation, the act of
calling a foreign import from a forkIO'ed (=non-bound) thread, and
the act of calling a foreign import from a Haskell implementation
that does not support bound threads, all place this proof obligation
on the programmer. Therefore I'd want it to be an explicit
annotation, not the default.
> "if an implementation supports haskell code running on multiple OS
> threads, it must support the bound threads proposal. if it does not,
> then all 'nonconcurrent' foreign calls must be made on the one true OS
> thread"
*) "Haskell code running on multiple OS threads" is irrelevant. Only
the FFI allows you to observe which OS thread you are running in.
This should be worded in terms of what kind of concurrent FFI calls
are supported, or whether call-in from arbitrary OS threads is
supported.
*) Note though that this makes it *impossible* to make a concurrent
call to one of Apple's GUI libraries (both Carbon and Cocoa insist on
being called from the OS thread that runs the C main function). So
good-bye to calculating things in the background while a GUI is
waiting for user input.
We could also say that a modified form of the bound threads proposal
is actually mandatory; the implementation you have in mind would
support it with the following exceptions:
a) Foreign calls from forkIO'ed threads can read and write (a.k.a.
interfere with) the thread local state of the "main" OS thread;
people are not supposed to call functions that use thread local state
from forkIO'ed threads anyway.
b) Concurrent foreign imports might not see the appropriate thread
local state.
c) Call-ins from OS threads other than the main thread are not
allowed, therefore there is no forkOS and no runInBoundThread. (Or,
alternatively, call-ins from other OS threads create unbound threads
instead).
==== On the implementability of "concurrent reentrant" ====
>> It might not be absolutely easy to implement "concurrent reentrant",
>> but it's no harder than concurrent non-reentrant calls.
>
> it is much much harder. you have to deal with your haskell run-time
> being called into from an _alternate OS thread_ meaning you have to
> deal
> with the os threading primitives and locking and mutexi and in general
> pay a lot of the cost you would for a fully OS threaded
> implementation.
I don't follow your claim. The generated code for a foreign export
will have to
a) check a thread-local flag/the current thread id to see whether we
are being called from a non-concurrent reentrant import or from
"elsewhere". Checking a piece of thread-local state is FAST.
b) If we are "elsewhere", send an interthread message to the runtime
thread. The runtime thread will need to periodically check whether an
interthread message has arrived, and if there is no work, block
waiting for it. The fast path of checking whether something has been
posted to the message queue is fast indeed - you just have to check a
global flag. So no locking and mutexes -- sorry, I don't buy
"mutexi" ;-) -- in your regular code.
What is so hard or so inefficient about this?
Remember, for concurrent non-reentrant, you will have to deal with
inter-OS-thread messaging, too.
About how fast thread-local state really is:
__thread attribute on Linux: ~ 2 memory load instructions.
__declspec(thread) in MSVC++ on Windows: about the same.
pthread_getspecific on Mac OS X/x86 and Mac OS X/G5: ~10 instructions
pthread_getspecific on Linux and TlsGetValue on Windows: ~10-20
instructions
pthread_getspecific on Mac OS X/G4: a system call :-(.
Also, to just check whether you can use the fast-path call-in, you
could optimise things by just checking whether the stack pointer is
in the expected range for the runtime OS thread (fast case), or not
(slow case).
All in all, I can't see a good excuse to not implement foreign import
concurrent reentrant when you've already implemented concurrent
nonreentrant.
Cheers,
Wolfgang
More information about the Haskell-prime
mailing list