(I keep forgetting to correctly fill out the To: and Cc: fields before sending my reply... so here's the copy that should have been sent to the list 10 minutes ago...) Daan Leijen wrote:
Hi Wolfgang,
I feel like you are beating my proposal to death here, and I find it hard to react individually to all your remarks.
Sorry, maybe I shouldn't shoot all my arguments at you at once :-)
I'll try to focus on the main issue:
You are worried that the forkOS and forkIO distinction is too primitive and that it rule out sophisticated scheduling on SMP processors for example.
That is closely related to the point that I don't want implementation details (that several Haskell threads can run in one OS thread) to be part of a defined interface that has to be supported by all implementations. My main point is probably my strong phobia of "safe" calls: they are not safe to use. FFI newbies are repeatedly getting bitten by the fact that "safe" calls block everything else. There is no "natural" reason for a call to block other threads, it's a consequence of an implementation detail (lightweight threads). The intention behind threadsafe is to make this implementation detail invisible, and therefore harmless. The forkOS/forkIO proposal seems to rely on having foreign calls that block other threads (that run in the same OS thread)... or doesn't it?
Maybe, the forkOS/forkIO approach is flawed, but I think we should only rule it out when we can provide a convincing example where only the keyword approach would work, and where we can't use combinators to achieve the same effect.
That's unfair ;-) --- I could also claim the reverse and say that we stick with threadsafe/bound until we have a convincing example... Using combinators sounds good, but there are things where combinators are not automatically the best choice (we're usually not using combinators to implement lazyness, either), and we don't know yet whether this is the case here or not. So let's just get on with the discussion....
About the example: [snip] That is amazing :-) [...]
Well, of course, it's "none of your business" to know. Different Haskell runtimes could use completely different schemes, and you wouldn't even notice. In GHC, the second OS thread is spawned earlier, the first time a threadsafe call is made. When the threadsafe call is made, GHC a) makes sure that there is a second OS thread available (some overhead the first time) and b) makes sure that all non-bound [in the current implementation: all] Haskell threads are executed by the second OS thread from now on. Now if a bound callback is invoked [not yet implemented], the bound callback is executed in the thread that the wrapper was called in (the first OS thread). All other Haskell threads (if any) continue to run in the second OS thread. If forkIO is called, the new Haskell thread is just added to the list of threads to be run by the second OS thread, no new OS thread has to be spawned, and forkIO doesn't have to know about threadsafe or about bound. If a non-bound callback is invoked [available now in the CVS HEAD], the callback is executed along with the other background Haskell threads in the second OS thread.
Since the RTS can't guess whether to use new OS threads or not at forkIO, I assumed that you could mark "wrapper" functions (callbacks) with a "threadsafe" attribute. If this is not the case, I don't understand how the implementation could work in this particular example.
It doesn't need to guess, "threadsafe" is just for imports, and it works anyway. See above, and if I haven't explained it clearly, try http://www.cse.unsw.edu.au/~chak/haskell/ghc/comm/rts-libs/multi- thread.html (which I really should update, it's outdated), or ask again. Maybe we need a more exact specification of how forkOS/forkIO should behave, especially with respect to foreign calls blocking other threads. Could you elaborate on how you would expect "normal" (safe) foreign calls to behave in different situations? Cheers, Wolfgang
Hi Wolfgang,
Maybe, the forkOS/forkIO approach is flawed, but I think we should only rule it out when we can provide a convincing example where only the keyword approach would work, and where we can't use combinators to achieve the same effect.
That's unfair ;-) --- I could also claim the reverse and say that we stick with threadsafe/bound until we have a convincing example... Using combinators sounds good, but there are things where combinators are not automatically the best choice (we're usually not using combinators to implement lazyness, either), and we don't know yet whether this is the case here or not. So let's just get on with the discussion....
No, it is not unfair. We should only introduce a new keyword/syntax if we are unable to express the behaviour in plain Haskell or if it very awkward to do so. Now, I can be easily convinced that "threadsafe" is the way to go, whenever there is a compelling example where forkOS/forkIO fails. At first, I thought that one could never come up with a counter example: since forkOS/forkIO are the most primitive calls, we can always model any strategy implemented in C (and more). However, I just realised that there is more than meets the eye: you just described that "GHC moves all non-bound Haskell threads to a second OS thread". This is something that can not be described without adding another primitive (forkUnbound?) and it may lead to an example that is not expressible with forkOS/forkIO. On the other hand, it seems awkward/impossible to make sure that certain functions are always called from a specific OS thread with the "threadsafe" approach while the forkOS/forkIO primitives are always available to the programmer for explicit management of threads. In general though, I have learned over the years that is most of the time better to first find an implementation using low-level primitives in Haskell, and maybe later add special syntax, than to implement a strategy directly in C.
About the example: [snip] That is amazing :-) [...] [snip] makes sure that there is a second OS thread available (some overhead the first time) and b) makes sure that all non-bound [in the current implementation: all] Haskell threads are executed by the second OS thread from now on. [snip]
Thanks for the explanation. Just on the side, it seems that this approach always involves an OS context switch for bound callbacks. This doesn't happen with the forkOS/forkIO approach. It may be rather expensive, take a mouse motion event handler for example.
Maybe we need a more exact specification of how forkOS/forkIO should behave, especially with respect to foreign calls blocking other threads. Could you elaborate on how you would expect "normal" (safe) > foreign calls to behave in different situations?
1) "forkOS" starts a new haskell thread in a new OS thread 2) "forkIO" starts a new haskell thread in the current OS thread 3) maybe we should also add "forkUnbound" that forks a haskell thread that can automatically be moved between OS threads. 4) a foreign call blocks all haskell threads that are attached to the current OS thread until a) the call returns, or b) the call enters Haskell again. 5) on top of this, we can implement *) a library function "fork" that forks a new haskell thread that maybe runs in separate OS thread, depending on the architecture. *) a "threadSafe" combinator to make non-blocking foreign calls. notes: * "forkOS" doesn't have to use a new OS thread to run Haskell threads, just when calling a foreign function, so it would work on Hugs too for example. (as explained in a previous mail). * The naming is a bit inconveniently chosen. Better would be: forkOS => forkNativeThread forkIO => forkHaskellThread Now, "forkIO" can now be implemented in terms of those two functions, and can be used when the user doesn't care about how the thread is scheduled. Now, what I don't like about my proposal and your proposal is that the user has to be aware of OS threads when making foreign calls by wrapping it in "threadSafe" or adding "threadsafe" sometimes -- but maybe that is unavoidable. All the best, Daan.
Hi Daan & everyone else,
Now, I can be easily convinced that "threadsafe" is the way to go, whenever there is a compelling example where forkOS/forkIO fails.
I'm not arguing that there is a situation where forkOS/forkIO fail to provide sufficient functionality. In fact, I'm afraid they may provide too much functionality, leading to an overly complex solution. Also, I'm afraid that "safe" foreign imports, with their strange blocking behaviour, will be a permanent source of bugs. As I've already said, I consider lightweight threads and the ability to run multiple lightweight threads in one OS thread to be an implementation detail of many current Haskell implementations. Implementation details of the runtime system should not be exposed to the user (although they may be available as primitives that may change from compiler version to compiler version). Providing forkOS/forkIO would _require_ future implementations to provide this functionality, even if there is no longer any technical reason for it.
Now, what I don't like about my proposal and your proposal is that the user has to be aware of OS threads when making foreign calls by wrapping it in "threadSafe" or adding "threadsafe" sometimes -- but maybe that is unavoidable.
With threadsafe and bound, we could just treat "threadsafe" and "bound" as defaults. Then, the user does not have to be aware of the OS thread/lightweight issue, unless (s)he wants to optimize for performance. Only then would the user need to add "unsafe" or "unbound" (or remove "bound") in some places. (GHC 5.05 already has "threadsafe" as default; as "safe" doesn't seem to provide any advantages, it is currently not implemented in the threaded RTS).
On the other hand, it seems awkward/impossible to make sure that certain functions are always called from a specific OS thread with the "threadsafe" approach while the forkOS/forkIO primitives are always available to the programmer for explicit management of threads.
How would you make sure that a certain function is always called from a specific OS thread (other than the current one) in C? I don't see why the runtime system should provide special support for this, it looks like trying to beat C at interacting with C libraries...
Thanks for the explanation. Just on the side, it seems that this approach always involves an OS context switch for bound callbacks. This doesn't happen with the forkOS/forkIO approach. It may be rather expensive, take a mouse motion event handler for example.
(I assume you meant to say "unbound callbacks". Bound callbacks don't require a thread-switch). Unbound callbacks require a thread-switch in the approach I just described*. However (after having thought about it for a few minutes), I'm convinced that this extra thread-switch can easily be optimized away in the RTS. The resulting implementation would still be simpler than what is required to support the forkIO/forkOS primitives. ( * ... plus two extra OS thread switches that have nothing to do with this issue. They'd be necessary with either approach until someone makes certain improvements to the storage manager)
Maybe we need a more exact specification of how forkOS/forkIO should behave, especially with respect to foreign calls blocking other threads. Could you elaborate on how you would expect "normal" (safe)
foreign calls to behave in different situations?
1) "forkOS" starts a new haskell thread in a new OS thread 2) "forkIO" starts a new haskell thread in the current OS thread 3) maybe we should also add "forkUnbound" that forks a haskell thread that can automatically be moved between OS threads. 4) a foreign call blocks all haskell threads that are attached to the current OS thread until a) the call returns, or b) the call enters Haskell again.
Does that make sense for implementations that don't use the "current" OS thread for executing the Haskell code (just for foreign calls)? Such implementations would have to do extra locking that's not really necessary...
5) on top of this, we can implement *) a library function "fork" that forks a new haskell thread that maybe runs in separate OS thread, depending on the architecture.
Wouldn't that mean it would be impossible to predict which other haskell threads, if any, would be blocked by a particular call to a foreign import? Cheers, Wolfgang
participants (2)
-
Daan Leijen -
Wolfgang Thaller