I have just spend some time reading through all the discussions and the new "threads" document and I would like to propose the addition of a new library function.
forkOS :: IO () -> IO ThreadID
The function "forkOS" forks a new Haskell thread that runs in a new OS (or native) thread. With this, I also propose that "forkIO" always runs a Haskell thread in the same OS thread that the current Haskell thread runs in. (i.e. "forkIO": same OS thread, "forkOS": new OS thread)
I'd like to point out a concept that I think is being missed here: We never want to specify what OS thread is running a particular Haskell thread. why not? Because (a) it doesn't matter: the programmer can never tell, and (b) we want to give the implementation freedom to spread Haskell threads across multiple OS threads to make use of multiple real CPUs. But the programmer CAN tell, you scream! How can the programmer tell? Only by calling foreign functions. The point is that you want to specify which OS thread is used to invoke a foreign function, NOT which OS thread is used to execute Haskell code. The semantics that Simon & I wrote make this clear. If we keep thinking like this, then implementations like Hugs can be single-threaded internally but switch OS threads to call out to foreign functions, and implementations like GHC can be multi-threaded internally and avoid switching threads when calling out to foreign functions.
Using the new primitive, we can view the new "threadsafe" keyword as syntactic sugar:
foreign import threadsafe foo :: Int -> IO Int
===>
foo :: Int -> IO Int foo i = threadSafe (primFoo i)
foreign import "foo" primFoo :: IO Int
where
threadSafe :: IO a -> IO a threadSafe io = do result <-newEmptyMVarforkOS (do{ x <-io; putMVar result x }) getMVar result
This forces a thread switch when calling a threadsafe foreign function, which is something I think we want to avoid. I'm basing this on two assumptions: (a) switching OS threads is expensive and (b) threadsafe foreign calls are common. I could potentially be wrong on either of these, and I'm prepared to be persuaded. But if both (a) and (b) turn out to be true, then worse is worse in this case. Cheers, Simon
Hi Simon,
I'd like to point out a concept that I think is being missed here:
We never want to specify what OS thread is running a particular Haskell thread.
why not? Because (a) it doesn't matter: the programmer can never tell, and (b) we want to give the implementation freedom to spread Haskell threads across multiple OS threads to make use of multiple real CPUs.
I agree that these are valid points. However, as I said, I don't think we can do (b), ie. automatic management, in many real-world situations. The above points are mostly useful in a pure Haskell setting. In general, I think that only the programmer knows what strategy to use. In particular, we can provide a "fork" function that forks of a new Haskell thread that maybe runs in a new OS thread, or other CPU; basically implementing the above concept for programs that don't care about how the Haskell threads are distributed over OS threads: fork :: IO () -> IO ThreadID fork io = do newOS <-[complex algorithm that determines if a new OS thread is needed.] if (newOS) then forkOS io else do threadID <-[complex algorithm that determines in which existing thread we run it] forkIOIn threadID io Note that we can now implement our really sophisticated distributed algorithms in plain Haskell.
The point is that you want to specify which OS thread is used to invoke a foreign function, NOT which OS thread is used to execute Haskell code. The semantics that Simon & I wrote make this clear.
This is a good point and that is also the weakness of the "forkOS", "forkIO" approach: it is less declarative and thus leaves less freedom to the implementation. However, I hope that through functions like "fork", we can bring back declarativeness by abstraction.
If we keep thinking like this, then implementations like Hugs can be single-threaded internally but switch OS threads to call out to foreign functions, and implementations like GHC can be multi-threaded internally and avoid switching threads when calling out to foreign functions.
Ha, this is not true :-) We are saved by your observation that in the Haskell world we can't observe whether we run in a different OS thread or not. Thus a single-threaded Hugs will implement forkOS as forkIO but still attaches a different "Hugs OS thread identifier" to the Haskell thread. When a foreign call is made, it matches the Hugs OS thread identifiers and uses a different OS thread if necessary, maintaining a mapping between the Hugs OS thread identifiers and the spawned OS threads.
threadSafe :: IO a -> IO a threadSafe io = do result <-newEmptyMVar forkOS (do{ x <-io; putMVar result x }) getMVar result
This forces a thread switch when calling a threadsafe foreign function, which is something I think we want to avoid.
We can refine the implementation to avoid a thread switch when it is the only Haskell thread running in the current OS thread: threadSafeEx :: IO a -> IO a threadSafeEx io = do count <-getHaskellThreadCountInTheCurrentOSThreadif (count > 1) then threadSafe io else io
I'm basing this on two assumptions: (a) switching OS threads is expensive and (b) threadsafe foreign calls are common. I could potentially be wrong on either of these, and I'm prepared to be persuaded. But if both (a) and (b) turn out to be true, then worse is worse in this case.
I think you are righ on (a), but I also think that we can avoid it just as it can be sometimes avoided when implemented in C in the runtime. Can't say anything about (b). All the best, Daan. Now, I have an example from the wxHaskell GUI library that exposes some of the problems with multiple threads. I can't say it can be solved nicely with forkOS, so I wonder how it would work out with "threadsafe": The example is a Haskell initialization function that is called via a callback from the GUI library. The Haskell initialization function wants to do a lot processing but still stay reactive to close events for example. Since events are processed in an eventloop, new events can only come in by returning from the callback. So, the initilization functions forks of a Haskell thread (the processor) to do all the work and returns as soon as possible to the C GUI library. Now, the eventloop starts to wait for the next event in C land. The problem is that the "processor" thread won't run since we have returned to C-land and the haskell scheduler can't run. We can solve it by running the processor thread with "forkOS". I can't say it is a particularly nice solution but it is how it is done in all other major programming languages. I wonder how the "threadsafe" keyword can be used to solve this problem. Since the haskell function is called via a callback, I guess that "threadsafe" should also apply to "wrapper" functions -- that is, when the foreign world calls haskell, we use another OS thread to run the haskell code. However, I think that we are than forced to use a OS thread context switch??
Cheers, Simon
In general, I think that only the programmer knows what strategy to use.
Do programmers know? I know about my own program, but do I know about the library that I am going to use? Does it use forkOS or forkIO? What will be the consequences if it uses forkIO and I do a lengthy foreign call? Does the library writer know about my program? I fear I'll end up wrapping every call to *Haskell* libraries in your "threadSafe" combinator - just to be sure that the library and my program don't interfere. I'm very afraid of having some long debugging sessions once we have this "feature".
Now, I have an example from the wxHaskell GUI library that exposes some of the problems with multiple threads. I can't say it can be solved nicely with forkOS, so I wonder how it would work out with "threadsafe":
In both cases, threadsafe and forkOS, we would have two OS threads, and we would have OS thread context switches between them. So it will get us nowhere to count the OS thread context switches involved (incidentally, handling GUI events is an area where we can easily afford the cost of OS thread context switches). You would mark the call to the wxWindows event loop as threadsafe (actually, in the CVS version of GHC, "safe" is currently a shorthand for "threadsafe" as nobody has yet provided a logically sound and meaningful definition of the semantics that "safe" should have instead). In case that wxWindows makes the assumption that it's functions are invoked _from the same OS thread_ that it used to call your callback, you can add "bound" to the foreign export statement for your callback. Then you would use just forkIO and everything would work. (If wxWindows doesn't allow access from multiple threads, then of course you can't call wx functions from the thread you just forked. But that's the same no matter which proposal we follow.)
The problem is that the "processor" thread won't run since we have returned to C-land and the haskell scheduler can't run.
Threadsafe means that you don't need to care about things like that.
[...] but it is how it is done in all other major programming languages.
Only other languages don't require you to manually keep track of a correspondence between lightweight and heavyweight threads.
Since the haskell function is called via a callback, I guess that "threadsafe" should also apply to "wrapper" functions -- that is, when the foreign world calls haskell, we use another OS thread to run the haskell code.
Sorry, no clue what you mean... could you elaborate? "threadsafe" just applies to imports, not to exports and wrappers. "bound" applies to exports and wrappers, but not to imports. Should it be different? Cheers, Wolfgang
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. 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. I think that you are right if the programmer use forkOS and forkIO directly all the time but in general, we can provide abstractions *in haskell* to deal with scheduling. See my "fork" example in the previous mail. The bottom line is that if we have access from Haskell to the primitives, we can always implement any smart scheme that we need or want. If you implement a strategy via keywords, it is fixed. I don't promote using low-level programming but I think that "bound" and "threadsafe" should be implemented using combinators in Haskell itself, with some low-level functions like forkIO. 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. All the best, Daan. About the example:
Now, I have an example from the wxHaskell GUI library that exposes some of the problems with multiple threads. I can't say it can be solved nicely with forkOS, so I wonder how it would work out with "threadsafe":
[snip] In case that wxWindows makes the assumption that it's functions are invoked _from the same OS thread_ that it used to call your callback, you can add "bound" to the foreign export statement for your callback.
Then you would use just forkIO and everything would work.
That is amazing :-) Suppose that there is one OS thread running here -- the GUI thread. When the callback is called, I use "bound" so that the callback still runs in the GUI thread. When I use "forkIO", the forked computation could also run in the same GUI thread (I guess it is unspecified, but suppose it does). When the callback returns to C land, the eventloop will wait and since there is still just one OS thread running, the forked computation will block.... Urk, somehow the RTS has to be smart enough to spawn a new OS thread when forkIO is called. Right?
Since the haskell function is called via a callback, I guess that "threadsafe" should also apply to "wrapper" functions -- that is, when the foreign world calls haskell, we use another OS thread to run the haskell code.
Sorry, no clue what you mean... could you elaborate? "threadsafe" just applies to imports, not to ? exports and wrappers. "bound" applies to exports and wrappers, but not to imports. Should it be different?
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.
participants (3)
-
Daan Leijen -
Simon Marlow -
Wolfgang Thaller