Malcolm Wallace wrote [snip]
When sequential control returns to the Foogle world, that is when the Haskell GC is guaranteed to be complete. [snip] I don't think I recognise this notion of "sequential control". It seems to be assuming there is only one "real" OS thread in existence, and it is impossible for both Foogle and Haskell to have "sequential control" over this thread at any given moment. When they do not have sequential control, they are not allowed to perform garbage collection. This model no doubt fits NHC well enough, but it seems to be assuming an awful lot about Foogle or about the system you are running on. To take a concrete example, suppose Haskell == NHC and Foogle = GHC, then it seems to me the model breaks down because I imagine GHC is quite happy to do GCs and invoke finalizers in the middle of exported functions. Even worse, GHC at least is now capable thanks to Sigbjorn of running in several OS threads at once (on Windows) so it may be doing GCs at any time whatever.
I appreciate it's not Malcolm Wallace's fault that NHC can't implement a form of dynamic wrapper which can be used at all times. However since I do not think this problem is going to go away, would it not be better to put it in the specification but admit that NHC can't do it? Otherwise the FFI specification will be very good (as it is) if you want to call languages like C, but will be inadequate if you want to communicate with other languages which also know about things like concurrency and garbage-collection, such as Haskell or even Java.
I appreciate it's not Malcolm Wallace's fault that NHC can't implement a form of dynamic wrapper which can be used at all times. However since I do not think this problem is going to go away, would it not be better to put it in the specification but admit that NHC can't do it? Otherwise the FFI specification will be very good (as it is) if you want to call languages like C, but will be inadequate if you want to communicate with other languages which also know about things like concurrency and garbage-collection, such as Haskell or even Java.
I think the two of you are getting caught up in implementation details (which always seem fixable) instead of looking at fundamentals (which tend not to be). The issue is that allowing finalizers to be Haskell code or to call Haskell code forces us to add preemptive concurrency to every Haskell implementation. Hugs has cooperative concurrency but that isn't enough, it has to be preemptive. To see why, suppose Hugs is busy calculating sum [1..1000000] and a garbage collection finds it has to run a Haskell function, it can't run it straight away because that would preempt the original execution. Likewise, it can't run it immediately that the GC ends - that's still preemption. Hugs is probably busy evaluating a + or (:) or the like when the GC happens, what if we wait until that finishes and then run the finalizers? Maybe. The world is in a cleaner state at this point but it's still tricky. And, again, you've just added most of the machinery required for implementing preemptive concurrency. Hugs does allow context switches in the IO monad. Could we allow it there? Maybe, we could. It wouldn't do you much good though because Haskell can spend arbitrarily long running pure code so your finalizers might not get run for a long time or maybe never. So what's the objection to adding preemptive concurrency? 1) The goal was to be able to call foreign code and have it call us. It seems like there's something wrong if doing that forces you to add concurrency. 2) Preemptive concurrency is one of those things that you can't just paint on later - you really have to rewrite the whole thing from scratch because it's just too hard to find all the pieces that make the assumption that they are single-threaded. 3) Adding preemptive concurrency guarantees a steady source of hard to repeat bugs - people aren't very good at writing concurrent code. Cooperative concurrency is much easier to handle because context switches happen at times when both tasks are ready for it: the sender because it knows it is passing control over, the receiver because it put itself in a clean state before it handed over control. You ask how garbage collection can work if you can't invoke it at arbitrary times? You ask what use foreign export is if you can't call the exported function at arbitrary times? [I hope I'm paraphrasing your questions appropriately] Both questions start by assuming that you have Haskell and Foosh [or whatever your other language is called] running in separate threads (otherwise things wouldn't happen at arbitrary times). Put them in a single thread and there's no problem: you're not doing things at arbitrary times, you're doing them at quite specific times: Foosh had control and it called into Haskell or Haskell had control and it called into Foosh. In other words, the problems you raise come from wanting preemptive concurrency so it's no wonder that the ffi extension doesn't address them. We could start working on a concurrency extension but that's a different beast. That said, we probably do need a few hooks to make Haskell talk to other GCs better. See the paper I pointed at this afternoon for a sketch of what you need if you're to avoid space leaks. -- Alastair Reid alastair@reid-consulting-uk.ltd.uk Reid Consulting (UK) Limited http://www.reid-consulting-uk.ltd.uk/alastair/
Alastair Reid wrote: [snip]
You ask how garbage collection can work if you can't invoke it at arbitrary times? You ask what use foreign export is if you can't call the exported function at arbitrary times? [I hope I'm paraphrasing your questions appropriately]
Both questions start by assuming that you have Haskell and Foosh [or whatever your other language is called] running in separate threads (otherwise things wouldn't happen at arbitrary times). Put them in a single thread and there's no problem: you're not doing things at arbitrary times, you're doing them at quite specific times: Foosh had control and it called into Haskell or Haskell had control and it called into Foosh. [snip] No, you do not really need separate threads for this problem to occur. All you need is, say, Hugs to call a GHC-exported function as a finalizer, in the same OS thread, GHC to run a garbage collection during this function, and the garbage collection in turn to want to run a Hugs finalizer, before the finalizer called from Hugs has finished. Of course one wouldn't normally want to link GHC from Hugs, but if even these two cannot be made to meet, I don't know how you expect Haskell to call anything else with a reasonably flexible GC system; it puts the kybosh on Java for example, which I am fairly sure makes plenty of use of both callbacks and finalizers.
In any case it seems to me just as dangerous to assume that the implementation does not use OS threads, as to assume it does. You are effectively writing on top of the FFI document "If your program does this perfectly reasonable combination of finalizers, it will fall over in an undefined way should the implementation use OS threads; furthermore there is no way around this". Basically the fact that there is only one OS thread is an implementation detail, not something that the user should have to think about. Is it really the case that neither NHC nor Hugs can implement a list of actions to be taken at the first convenient point after GC has finished without implementing the whole machinery of preemptive concurrency? I take Malcolm Wallace's word for it that it isn't trivial, but why do you need for example asynchronous interruption of Haskell threads, wait queues, or time slices? Surely what you need is some way of backing up the state upon return from GC in such a way that you can run the queued IO actions, which may be hard but is a long way off preemptive concurrency. If it's really impossible for NHC or Hugs to implement this, I think I would still rather it was left to the NHC and Hugs documentation to admit that exported Haskell functions basically don't work in some circumstances, rather than to the GHC documentation to say that actually they do.
[snip] No, you do not really need separate threads for this problem to occur. All you need is, say, Hugs to call a GHC-exported function as a finalizer, in the same OS thread, GHC to run a garbage collection during this function, and the garbage collection in turn to want to run a Hugs finalizer, before the finalizer called from Hugs has finished.
The Hugs and GHC runtimes can talk to each other just fine (or, if they can't it's a simple oversight and well fix it). There's no problem with GHC and Hugs each telling each other that some object they own has one less pointer to it. Next time it is convenient for the runtime, it can run a GC, perhaps recognize that there's a few GHC objects it can release and it tells the GHC runtime that it can release them. The reason we can do this is because it has limited scope: just a few data structures have to be tweaked to avoid GHC coming in when Hugs' data structures are in an inconsistent state. It's quite a different matter to allow arbitrary Haskell code to be run - that means the entire runtime system and libraries have to be made reentrant.
Of course one wouldn't normally want to link GHC from Hugs, but if even these two cannot be made to meet, I don't know how you expect Haskell to call anything else with a reasonably flexible GC system; it puts the kybosh on Java for example, which I am fairly sure makes plenty of use of both callbacks and finalizers.
That's fine, they can have all the finalizers they want. The finalizers can fiddle with things in the runtime system to tell the GC whatever they want.
In any case it seems to me just as dangerous to assume that the implementation does not use OS threads, as to assume it does.
The internal structure of your apps is up to you - use locks to avoid using single-threaded code in multithreaded manner.
You are effectively writing on top of the FFI document "If your program does this perfectly reasonable combination of finalizers, it will fall over in an undefined way should the implementation use OS threads; furthermore there is no way around this". Basically the fact that there is only one OS thread is an implementation detail, not something that the user should have to think about.
Programmers are used to dealing with code which is single threaded or not reentrant. It's quite common.
Is it really the case that neither NHC nor Hugs can implement a list of actions to be taken at the first convenient point after GC has finished without implementing the whole machinery of preemptive concurrency? I take Malcolm Wallace's word for it that it isn't trivial, but why do you need for example asynchronous interruption of Haskell threads, wait queues, or time slices? Surely what you need is some way of backing up the state upon return from GC in such a way that you can run the queued IO actions, which may be hard but is a long way off preemptive concurrency.
The way GHC implements preemption is an optimized form of: set a bit when preemption is needed; and make sure that generated code will test that bit whenever it is in a position to perform a context switch. What you're asking Hugs and NHC to do is: add a function to a list whenever you have a finalizer to run; make sure the interpreter will test that bit whenever it is in a position to perform a context switch. It's basically the same. We don't have to mess around with signals to provide a regular timer interrupt but that's the easy bit of the code. We can probably avoid messing around with multiple C stacks. That's a significant saving but, it's the complexity of that is fairly self-contained - we could probably steal some code from some other language implementation. The cost is going over all data structures in the system making sure that operations on them are suitably atomic. One of the issues I remember from old versions of GHC was that some of the primops would do some work, allocate some memory, then finish the job. The classic error to make in that code was for the second half of the code to assume almost anything about what happened in the first half of the code: how much space is on the stack, does a table have free space in it, is this pointer into the middle of an object ok? The problem is the scope: every single data structure and every bit of code that accesses it has to be vetted and we have to keep it in mind as we maintain the code. It's a high price to pay and I don't think it's necessary (because all you really need is for the runtime systems to talk to each other in very limited ways).
If it's really impossible for NHC or Hugs to implement this, I think I would still rather it was left to the NHC and Hugs documentation to admit that exported Haskell functions basically don't work in some circumstances, rather than to the GHC documentation to say that actually they do.
It's a matter of taste how you do these things. -- Alastair Reid alastair@reid-consulting-uk.ltd.uk Reid Consulting (UK) Limited http://www.reid-consulting-uk.ltd.uk/alastair/
Alastair Reid wrote: [snip]
What you're asking Hugs and NHC to do is: add a function to a list whenever you have a finalizer to run; make sure the interpreter will test that bit whenever it is in a position to perform a context switch. Am I really asking that much? In that paper you wrote you already propose that the Haskell system implement functions which tell C when it enters GC and when it leaves GC. So what it seems you need is for this to set a flag so that (1) When the (Foogle) finalizer runs, it runs normally if this flag is unset (no GC is running, so presumably safe). Otherwise add the action to a queue. (2) When Haskell leaves GC it checks the queue and runs any pending actions, backing up if it's necessary to do so.
The flag and queue need to be atomically accessed, but since (for Hugs and NHC) you are assuming only one OS thread, that should be enough. You don't for example have to worry about exported functions being called absolutely everywhere. [snip]
The cost is going over all data structures in the system making sure that operations on them are suitably atomic. [snip] If something like that would work, the cost during normal operation would only be the cost of setting and clearing the flag at the start and end of GC, and checking the queue at the end of GHC.
I'm sorry, it's frightfully arrogant of me to argue over details of Haskell compilers with their implementors, but what else can I do here? Anyway the point is a general one; can we implement FFI without needing the whole machinery of concurrency?
If it's really impossible for NHC or Hugs to implement this, I think I would still rather it was left to the NHC and Hugs documentation to admit that exported Haskell functions basically don't work in some circumstances, rather than to the GHC documentation to say that actually they do.
It's a matter of taste how you do these things.
If we take it that there is no way for a finalizer to call functions exported from NHC or Hugs, my personal preference would be to regard this as an undesirable implementation glitch which should be documented in the NHC and Hugs documentation. This would then be something users of the FFI with NHC and Hugs have to worry about, rather than users of the FFI in general. I don't think it makes the FFI worthless for NHC and Hugs, any more than the recent discovery that IO is not really a monad on most implementations makes the Haskell standard worthless.
participants (2)
-
Alastair Reid -
George Russell