RE: Pre-emptive or co-operative concurrency (was: Concurrency)

On 28 March 2006 15:26, Malcolm Wallace wrote:
"Simon Marlow"
wrote: Ok, I'll try to nail down what we should require in terms of fairness (which is the same as pre-emption).
The terms are not entirely synonymous.
Pre-emption means that (1) threads have priority levels, and (2) that a higher priority thread can steal the processor from a currently-running lower priority thread, and (3) it can do so "immediately" it needs to, without waiting for some "safe" synchronisation point.
By those criteria, none of the current Haskell implementations are pre-emptive, because no API assigns priorities to threads. So let's not use the term pre-emption; instead let's agree to talk only about fairness, since that is the real topic here.
Ok, I won't argue this point. It depends which definition you read - I'm using the term preemption to mean that threads can be preempted by the execution engine. I won't use the term any more to avoid confusion.
As I see it, all current (and likely future) implementations of concurrency in Haskell use co-operative scheduling. The only differences are in the granularity and positioning of the 'yield' calls.
* For Hugs, yield is inserted at certain I/O actions. * For ghc, yield is inserted after some count of allocations. * For yhc, yield is inserted after some count of bytecode instructions.
That list is a useful implementation reference, but I think it misses the point. It's much more useful to reserve the term "cooperative" for when the burden is on the programmer to insert context-switch points, as is the case in Hugs. This is a significant difference from the programmer's point of view, whereas the difference in imlementation between GHC and YHC is to a very close approximation invisible. Let's stick to fairness. These are the requirements I think the standard should include: No runnable process will be indefinitely delayed. No thread can be blocked indefinitely on an MVar unless another thread holds the MVar indefinitely. Suitably annotated foreign calls run concurrently with other Haskell threads. That does preclude a cooperative implementation, and for good reasons. Manuel made the point that if a cooperative implementation is admitted by the standard, that makes it much harder to write portable concurrent code. Code written using GHC wouldn't neceessarily run properly on a cooperative implementation, despite the fact that both implementations would be conforming. I think Haskell' should avoid this pitfall. Cheers, Simon

On Wed, Mar 29, 2006 at 11:44:12AM +0100, Simon Marlow wrote:
It's much more useful to reserve the term "cooperative" for when the burden is on the programmer to insert context-switch points, as is the case in Hugs. This is a significant difference from the programmer's point of view, whereas the difference in imlementation between GHC and YHC is to a very close approximation invisible.
The dividing line isn't "cooperative", though. Even if an implementation did a context switch after each IO action (i.e. in >>=), it would fail your first fairness requirement, as an IO action looping in pure code will starve other threads (as does one that doesn't do allocation in GHC).

On Wed, Mar 29, 2006 at 11:44:12AM +0100, Simon Marlow wrote:
Let's stick to fairness. These are the requirements I think the standard should include:
No runnable process will be indefinitely delayed.
No thread can be blocked indefinitely on an MVar unless another thread holds the MVar indefinitely.
Suitably annotated foreign calls run concurrently with other Haskell threads.
That does preclude a cooperative implementation, and for good reasons. Manuel made the point that if a cooperative implementation is admitted by the standard, that makes it much harder to write portable concurrent code. Code written using GHC wouldn't neceessarily run properly on a cooperative implementation, despite the fact that both implementations would be conforming. I think Haskell' should avoid this pitfall.
I don't think we should preclude cooperative implementations. inserting extra yields is harmless, omitting them when needed is bad. That and relying on the implementation for preemption is just asking for trouble in general. neither pthreads or java make such strong fairness guarentees. I would prefer a much weaker 'progress guarentee' * if any haskell thread is runable, at least one will be running. * a succession of yields will cycle through all runable threads in a finite number of steps. * these rules will still apply reguardless of whether suitably annotated foreign calls are running. it is much easier to program to and understand. I mean, if an implementation allocates 1 out of every thousand cycles to thread a as opposed to b, then it is still 'fair' by the above rules, but you would certainly want to take that into account when writing your program. Programs written as if they were running on a cooperative system will be more portable and more likely to work out of the box on alternate implementations than they were tested on. I am not sure what the MVar guarentee means, if it is blocked on an MVar, then it becomes runnable when the MVar is filled, so the runnable rule seems to take care of it. John -- John Meacham - ⑆repetae.net⑆john⑈

On Wednesday 29 March 2006 13:35, John Meacham wrote:
I am not sure what the MVar guarentee means, if it is blocked on an MVar, then it becomes runnable when the MVar is filled, so the runnable rule seems to take care of it.
Unfortunately not. Suppose threads A, B, and C compete for taking the same MVar. Now, without the MVar related fairness condition, threads A and B could be scheduled so that both alternately get the MVar but thread C still can't make any progess since it never gets the MVar. Note that this problem is related to so called priority inversion in priority scheduled systems. Priority inversion happens when a high priority thread waits for a resource held by a low priority process, but the latter can't make progress because it gets crowded out by a medium priority thread. In this case the medium priority thread effectively runs with a priority higher than the high priority thread, contrary to what the programmer has specified. The standard solution is to adopt a priority inheritance protocol, that is, temporarily raise the priority of any process that has claimed a resource to the maximum priority of all threads waiting for this resource. Ben -- There are three kinds of programmers: those who make off by one errors, and those who don't.
participants (4)
-
Benjamin Franksen
-
John Meacham
-
Ross Paterson
-
Simon Marlow