Alastair wrote [snip]
More importantly though, this does nothing at all to guarantee atomicity of Haskell code that manipulates global variables. Consider a data structure consisting of a list of objects. The main thread might add to this list and do searches in the list and the finalizer removes objects from the list:
type State = IORef [Object]
-- used by main newObject :: Object -> IO () newObject o = do os <- readIORef state writeIORef state (o:os)
-- used by finalizer killObject :: Object -> IO () killObject o = do os <- readIORef state writeIORef state (filter (/= 0) os)
Some possible interleavings of the IO actions in these functions can result in an object not being added to or removed from the object list. [snip] I'm not sure I really understand the problem. The FFI standard (Release Candidate 4, the one I have printed out here) does not define IORefs, and of course Haskell 98 doesn't either. Therefore, although this code is broken, this particular example doesn't matter if all we are considering is code written in Haskell 98 + FFI.
However to me this code just looks totally wrong because of course I use GHC, a system with preemptive scheduling, and would regard it as incompetent to use anything other than an MVar here. Even for Hugs I don't like this code, because the functions newObject and killObject will fail in the future should anyone try to port them to a system with pre-emptive concurrency, unless the maintainers are careful to avoid running them concurrently. So as Hugs has MVar's, I think it is bad practice not to use them here, unless you have some very good excuse (like being desperate for performance). You're basically implementing an API which has the hidden condition that the two functions must not be called simultaneously. But in any case it's somewhat woolly just to talk in terms of existing compilers. Supposing we posit that there is a GlobalVariables standard. Then indeed Alastair's example would fall over. So we can say that here is a problem which occurs with Haskell98 + FFI + GlobalVariables. However I would expect a GlobalVariables standard to specify both MVars and IORefs. The reason is precisely because of this sort of thing. Of course, as Alastair says, if all you're interested in is a single-threaded implementation, MVars seems a waste of time since either they don't block at all or they deadlock. But they *are* very necessary if you are intending to write code which may in the future be run on an implementation with full concurrency. It's precisely the same reason as why the FFI standard has a "threadsafe" keyword; although it is these days usually unnecessary, it is nevertheless very important to have it there because of what might happen in the future. So I can sum this up by saying (1) a GlobalVariables standard should specify both MVars and IORefs. (2) Someone writing finalizers on a Haskell98 + FFI + GlobalVariables implementation would have absolutely no excuse for using code such as that Alastair has given, since this is a really blinding example of where MVars are needed. Anyone writing a finalizer should be aware (indeed the FFI standard should say) that the finalizer may be run at any point (after all, when else would you expect it to run) and should take precautions against it. This is much better than expecting them to rely on arcana such as the details of Hugs scheduling strategy. In any case I disagree with Alastair when he says bugs of this sort are as bad as bugs caused by calling Haskell from a finalizer in Hugs. At least in this case Haskell's RTS is still healthy. Assuming the crash comes reasonably often, you can narrow it down by putting in loads of print statements, until eventually you get to the stage of asking "Who put what into the State variable". Not very pleasant, but you will get there. However if I understand it correctly, calling Haskell from a finalizer under the FFI may lead to nothing but a core-dump. Furthermore print statements will not help you because the finalizer might have been called anywhere; at the best you will only find out where the GC happened. Even if you guess that the reason Haskell's RTS blew up at just that point was because of a rogue finalizer during which an external Haskell function happened to be called, you have no way in Haskell of finding out what that finalizer might be, even less of finding out what external Haskell function the outside world was trying to invoke.
[snip] I'm not sure I really understand the problem. The FFI standard (Release Candidate 4, the one I have printed out here) does not define IORefs, and of course Haskell 98 doesn't either. Therefore, although this code is broken, this particular example doesn't matter if all we are considering is code written in Haskell 98 + FFI.
If there's no shared mutable Haskell state, writing finalizers in Haskell buys you little - what does a finalizer do other than cleanup shared state? (Note that it doesn't cleanup unshared state - what would be the point?) I think IORefs are the most widely accepted extension of Haskell. The FFI spec doesn't depend on IORefs but it should certainly be designed to work in an environment where they exist.
However to me this code just looks totally wrong because of course I use GHC, a system with preemptive scheduling, and would regard it as incompetent to use anything other than an MVar here.
I completely agree. I do not believe we can have any useful form of Haskell finalizers (meaning finalizers that manipulate shared Haskell state) without adding MVars. But, since concurrency is not so widely implemented as IORefs, we are trying to produce an FFI design which does not require concurrency extensions. My solution to this conflict is not to add Haskell finalizers.
However to me this code just looks totally wrong because of course I use GHC, a system with preemptive scheduling, and would regard it as incompetent to use anything other than an MVar here. Even for Hugs I don't like this code, because [...]
I think you're taking my code out of context. The purpose in writing that code using IORefs was to show the kind of code you should not write because it would suffer from race conditions. I completely agree that some form of locking is required to make the code safe. I guess I'd be willing to have Haskell finalizers if we agreed on an appropriate form of locking that all implementers can and will implement. NHC currently lacks a locking mechanism. I believe that using Hugs' MVars in conjunction with SimonM's patch will lead to deadlock in the cases that would have suffered race conditions using IORefs.
Supposing we posit that there is a GlobalVariables standard.
Just want to remark that the variables don't have to be global. Haskell finalizers are specified by closures and closures can point to any IORefs you fancy.
Anyone writing a finalizer should be aware (indeed the FFI standard should say) that the finalizer may be run at any point (after all, when else would you expect it to run) and should take precautions against it.
What I hear you saying is that the FFI standard should depend on a concurrency standard. I think there was a strong sentiment that we should avoid this. I agree though that it is necessary if we allow Haskell finalizers. -- Alastair Reid alastair@reid-consulting-uk.ltd.uk Reid Consulting (UK) Limited http://www.reid-consulting-uk.ltd.uk/alastair/
Alastair Reid wrote:
[snip] I'm not sure I really understand the problem. The FFI standard (Release Candidate 4, the one I have printed out here) does not define IORefs, and of course Haskell 98 doesn't either. Therefore, although this code is broken, this particular example doesn't matter if all we are considering is code written in Haskell 98 + FFI.
If there's no shared mutable Haskell state, writing finalizers in Haskell buys you little - what does a finalizer do other than cleanup shared state? (Note that it doesn't cleanup unshared state - what would be the point?)
[snip]
What I hear you saying is that the FFI standard should depend on a concurrency standard. I think there was a strong sentiment that we should avoid this. I agree though that it is necessary if we allow Haskell finalizers. [snip] I think we should remember that the FFI standard has to address various audiences (1) those who want to implement portable code in just FFI + Haskell98. This group does not have access to functions for conveniently manipulating mutable state, therefore Alastair's problem with IORefs will not be a problem for them. However Haskell inside finalizers will at any rate not harm them. Furthermore it is incorrect to say that just because there is no Haskell mutable state, there is no reason such people may want it to be possible to call Haskell while a finalizer is running. Consider someone who, say, calls out from Haskell to Java (to do funny graphics, say) and writes a finalizer which calls Java code. At the same time, they also want to do some completely separate pure computation in Haskell, which is made available to Java. Since Java at least does have preemptive concurrency, while it is running at all, it is perfectly possible that Java will call the Haskell computation while the Java finalizer is running. You want a license to make the roof fall in at this point; I don't think you should have it. (2) those who want to implement portable code in FFI + Haskell98 + something else. (I suspect this is the larger group.) To discuss this at all we simply have to speculate about what "something else" might be. One obvious "something else" is mutable state. Alastair is assuming that "something else" is IORefs but not MVars. However I assert this is implausible or at least unfortunate, because for reasons I've explained, it would normally be a good idea for implementations and programmers to provide and use MVars, even if they do not provide and use concurrency, since otherwise you can't write functions like Alastair's newObject and killObject without making unnecessary assumptions about the properties of the implementation. If NHC does not provide MVars, I think it should. I apologise to Malcolm, but in any case I doubt if implementing MVars in a world with only one thread would cost him a great deal of effort.
To sum up, it's NOT that I want the FFI standard to depend on concurrency. It's once again that I don't want the FFI standard to depend on the absence of concurrency (as Alastair's IORef code does).
To sum up, it's NOT that I want the FFI standard to depend on concurrency. It's once again that I don't want the FFI standard to depend on the absence of concurrency (as Alastair's IORef code does).
Please to not refer to that code out of context again. I wrote that code _specifically_ to demonstrate where race conditions could arise. I clearly flagged it as code that "does nothing at all to guarantee atomicity of Haskell code that manipulates global variables" and in which "Some possible interleavings of the IO actions in these functions can result in an object not being added to or removed from the object list". I think we are in full agreement that some form of locking is required if Haskell finalizers that manipulate shared Haskell state are allowed. The only disagreement I see is: 1) Whether IORefs are a heavily used, widely implemented Haskell extension that the FFI standard cannot afford to ignore. 2) Whether providing a sufficiently powerful concurrency implementation is a sufficiently large burden that compelling FFI implementations to provide concurrency is unreasonable. (Sufficiently strong == finalizers are not starved, MVars can be used by finalizers.) 3) Whether we should specify something that we all know how to implement today and which we have 8 years experience of and consider revising it in the future as the shoe is demonstrated to pinch. -- Alastair
Alastair Reid wrote: [snip] > Please to not refer to that code out of context again. I wrote that > code _specifically_ to demonstrate where race conditions could arise. > I clearly flagged it as code that "does nothing at all to guarantee > atomicity of Haskell code that manipulates global variables" and in > which "Some possible interleavings of the IO actions in these > functions can result in an object not being added to or removed from > the object list". OK, I apologise for taking you out of context. But in that case, if all your code demonstrates is that given Haskell98 + FFI + IORefs + no MVars + Haskell finalizers + an incompetent programmers, you may get race conditions, does that really demonstrate very much? > > I think we are in full agreement that some form of locking is required > if Haskell finalizers that manipulate shared Haskell state are > allowed. We are currently defining Haskell98 + FFI. There is no shared internal Haskell state to manipulate in Haskell98 + FFI. Therefore, in such circumstances, this statement is vacuously true. As I have already pointed out, in the absence of shared internal Haskell state there may still be good reasons for permitting Haskell code to run inside finalizers, for example if the outside world independently calls some (perhaps pure) Haskell function, while a finalizer is running. I think the simplest solution, in these circumstances, is to say that the extensions required to implement mutable state are not our problem in this standard. The FFI standard is not responsible when the programmer obtains race conditions due to misuse of features (such as choosing IORefs when MVars would be more appropriate) which are not even specified in it. Thus I do not think Alastair's argument for retaining this restriction in the standard holds. We also seem to have got into details about what NHC and Hugs should do. As I've already made clear, I think it would be better for the standard not to include this restriction, so that the NHC and Hugs documentation must explicitly (and clearly) say that they do not follow the standard here, the reason being that this is a potentially serious problem leading to unreproducible core-dumps and heaven knows what. However, although I am not a Hugs user, my personal opinion would be that Hugs should permit Haskell finalizers, but only run them at the next scheduling point (or at the end of execution), if no better way can be found. The user would still need to be warned to use yield periodically if necessary, but the consequences of not doing so would be not nearly as dire as the alternative. I don't like memory leaks, but infinitely prefer them to unreproducible core-dumps occuring at random points in the program. Hugs would then keep to the letter of the standard. If instant finalizers are desired, there is of course no reason we should not also have an addUnsafeForeignFinalizer function with the existing behaviour. What is the situation with NHC?
George Russell <ger@tzi.de> writes:
If instant finalizers are desired, there is of course no reason we should not also have an addUnsafeForeignFinalizer function with the existing behaviour.
I think I would be in favour of permitting both Haskell finalisers and foreign language finalisers. At the moment, it is rather convenient in the internals of nhc98's runtime system to have finalisers (e.g. for Handles) implemented purely in C, for efficiency if nothing else. On the other hand I can entirely see why Haskell finalisers are desirable. They are useful for debugging purposes, and they can help avoid space leaks.
What is the situation with NHC?
I'm sure the details of nhc98's implementation are largely irrelevant since we need to settle on a correct design regardless of the current state of play. But in case you are interested, at the moment, ForeignPtr uses a C-only finaliser :: FunPtr (Ptr a -> IO ()) whilst ForeignObj (deprecated) uses a Haskell finaliser :: IO (). Haskell finalisers are re-entry safe (i.e. you can go into foreign-land and back into Haskell), but C finalisers are currently not (you cannot safely call Haskell from a C finaliser, although it would only take a little tweaking to fix this). It is safe for a C finaliser to mess with shared state in the C world. It is not safe for a Haskell finaliser to mess with shared state in Haskell, because there are no locking mechanisms. (And if you try to use MVars, you'll get a compile time error because we don't provide them.) However it is safe for the Haskell finaliser to call C routines that manipulate shared state in the C world. Regards, Malcolm
George Russell <ger@tzi.de> writes:
it would normally be a good idea for implementations and programmers to provide and use MVars, even if they do not provide and use concurrency, ... If NHC does not provide MVars, I think it should.
I don't think MVars make any sense without concurrency, so I don't see the point of implementing the one without the other. I'm willing to be persuaded otherwise. (It would be nice to have some concurrency in nhc98, of course, but I don't foresee that happening soon.)
I apologise to Malcolm, but in any case I doubt if implementing MVars in a world with only one thread would cost him a great deal of effort.
Well it would be simple enough provided you didn't mind your computation halting with deadlock rather unpredictably. :-) Actually, I'm just wondering whether I can use the GC as a poor-man's scheduler. If a finaliser blocks on an MVar, save its state, keep the finaliser in the pending queue, and return to the main thread. Then on the next GC, try the same finaliser again, ad infinitum until it succeeds. Regards, Malcolm
(It would be nice to have some concurrency in nhc98, of course, but I don't foresee that happening soon.)
I remember implementing cooperative concurrency in Hugs as being rather easy. Of course, that was building on a base which already used a continuation passing IO monad... If you want to try, I could dig out some details but the main issues are: 1) When you have a choice of which thread to wake next, it matters which you wake next. IIRC, waking the wrong one makes it impossible to implement a producer-consumer pattern. 2) There's a lot of interaction with non-deterministic exception handling. 3) unsafePerformIO foo had better not block since we have no way to block the non-monadic code that invoked it. In that case, we raise an exception or give up (don't remember which).
Actually, I'm just wondering whether I can use the GC as a poor-man's scheduler. If a finaliser blocks on an MVar, save its state, keep the finaliser in the pending queue, and return to the main thread. Then on the next GC, try the same finaliser again, ad infinitum until it succeeds.
The finalizer won't become runnable until someone does a putMVar so you can deal with this case by storing the finalizer's state in a queue of blocked threads attached to the MVar and having putMVar do a context switch to the thread at the head of the MVar's queue of blocked threads. If you want finalizers to have priority over other threads, have two queues: one for finalizers, one for normal threads. -- Alastair
On 11-Oct-2002, George Russell <ger@tzi.de> wrote:
I think we should remember that the FFI standard has to address various audiences (1) those who want to implement portable code in just FFI + Haskell98. This group does not have access to functions for conveniently manipulating mutable state, therefore Alastair's problem with IORefs will not be a problem for them. However Haskell inside finalizers will at any rate not harm them
This argument is invalid, because those who want to implement portable code in just Haskel98 + FFI will very quickly use the FFI to implement IORefs or something similar.
Consider someone who, say, calls out from Haskell to Java (to do funny graphics, say) and writes a finalizer which calls Java code. At the same time, they also want to do some completely separate pure computation in Haskell, which is made available to Java. Since Java at least does have preemptive concurrency, while it is running at all, it is perfectly possible that Java will call the Haskell computation while the Java finalizer is running. You want a license to make the roof fall in at this point; I don't think you should have it.
If you have a non-thread-safe Haskell implementation, and you try to call it from two different threads without protecting this somehow, then the roof *will* fall in at least some of the time. This follows directly from the definition of "non-thread-safe". So you want to disallow non-thread-safe Haskell implementations from supporting the FFI? -- Fergus Henderson <fjh@cs.mu.oz.au> | "I have always known that the pursuit The University of Melbourne | of excellence is a lethal habit" WWW: <http://www.cs.mu.oz.au/~fjh> | -- the last words of T. S. Garp.
Fergus Henderson wrote:
On 11-Oct-2002, George Russell <ger@tzi.de> wrote:
I think we should remember that the FFI standard has to address various audiences (1) those who want to implement portable code in just FFI + Haskell98. This group does not have access to functions for conveniently manipulating mutable state, therefore Alastair's problem with IORefs will not be a problem for them. However Haskell inside finalizers will at any rate not harm them
This argument is invalid, because those who want to implement portable code in just Haskel98 + FFI will very quickly use the FFI to implement IORefs or something similar.
Yes, I suppose they could also hack it up in Haskell98 by writing values to files or by using IO.hGetBuffering and IO.hSetBuffering. I really think there are limits to the extent we need to protect hackers from their own ingenuity.
Consider someone who, say, calls out from Haskell to Java (to do funny graphics, say) and writes a finalizer which calls Java code. At the same time, they also want to do some completely separate pure computation in Haskell, which is made available to Java. Since Java at least does have preemptive concurrency, while it is running at all, it is perfectly possible that Java will call the Haskell computation while the Java finalizer is running. You want a license to make the roof fall in at this point; I don't think you should have it.
If you have a non-thread-safe Haskell implementation, and you try to call it from two different threads without protecting this somehow, then the roof *will* fall in at least some of the time. This follows directly from the definition of "non-thread-safe".
So you want to disallow non-thread-safe Haskell implementations from supporting the FFI?
I agree this is a problem, though I don't think there is much we can do about it. However the problem I describe arises even without multiple operating system threads, for example if both Haskell and Java implement concurrency using "green threads" (as I think Sun's Java implementation used to call them), so exactly as GHC and Hugs does it, with just one OS thread doing its own scheduling. Where both Hugs and Java are running in such a situation, they would presumably all be running in the same thread, with only one running at any one time.
Alastair Reid <alastair@reid-consulting-uk.ltd.uk> writes:
If there's no shared mutable Haskell state, writing finalizers in Haskell buys you little - what does a finalizer do other than cleanup shared state? (Note that it doesn't cleanup unshared state - what would be the point?)
To be honest, I had always thought that cleaning up unshared state /was/ the main point of finalisers. They are a user-level mechanism for releasing system resources (memory, files, etc) that are not otherwise directly under the control of the Haskell memory management system (GC). The idea of using them to fiddle with the live state of the program graph is a relatively new one to me, and frankly I'm not surprised it's so difficult to get it right in a pure language. Regards, Malcolm
participants (4)
-
Alastair Reid -
Fergus Henderson -
George Russell -
Malcolm Wallace