RE: Signals + minimal proposal (was Re: asynchronous exceptions)

On 07 April 2006 23:37, John Meacham wrote:
I think we might be thinking of different things. here is a complete implementation of exit.
exitMVar :: MVar () -- starts full exitMVar = ..
handlerMVar :: MVar [IO ()] -- starts with [] handlerMVar = ...
onExit :: IO () -> IO () onExit action = modifyMVar handlerMVar (action:)
exitWith status = do takeMVar exitMVar -- winner takes all let handleLoop = do hs <- swapMVar handlerMVar [] sequence_ hs if null hs then return () else handleLoop handleLoop exitWith_ status
exitWith_ calls the underlying 'exit' routine of the operating system immediatly. no waiting.
Suppose I want to do some action with a temporary file: bracket newTempFile (\f -> removeTempFile f) (\f -> doSomethingWith f) Under your scheme, this code doesn't get to remove its temporary file on exit, unless I explicitly add an exit handler that throws an exception to the current thread. I think code like the above should just work. Furthermore, I think it should be an invariant that a thread is never discarded or killed, only sent an exception. Otherwise, how else can I acquire a resource and guarantee to release it when either an exception is raised, the program exits, or the computation completes? According to your definition of exitWith above, I can't both raise an exception *and* exit in the same thread. If I register an onExit handler that throws an exception to the current thread, things go wrong if the current thread also calls exitWith. Also, you couldn't call exitWith while holding an MVar, if the handlers need access to the same MVar. You didn't show WithTemporaryExitHandler, which complicates things quite a bit. Also, your implementation has a race condition - a thread might add another exit handler after the swapMVar. I think we can probably agree on one thing: exitWith should raise an exception: exitWith e = throw (ExitException e) This isn't inconsistent with your proposal, and I think it's unambiguously better. The top-level exception handler catches ExitException and performs the required steps (running handlers, calling exit_). As you said, you need a top-level exception handler anyway, this is just a small change to your proposal, moving the exit actions to the top-level exception handler. Now additionally I believe that, if the system is about to simply stop, every thread should be sent an exception and be given a chance to clean up before the system stops. If this is the case, then: - withTemporaryExitHandler is unnecessary (catch suffices) - bracket and finally "just work" - it is safe to call exitWith while holding resources such as MVars - the programmer doesn't have to distinguish cleanup actions that should happen on exit from others - library programmers can rely on exceptions being delivered and don't have to additionally install exit handlers It's the right default: if a programmer decides not to handle the exception, then nothing goes wrong.
advantages of this set up.
1. base case requires no concurrency or exceptions
Haskell already has exceptions (in the IO monad), and people are using them to manage resources. We shouldn't introduce another way to clean up. Also, in the single-threaded case my proposal makes sense too.
2. abstract threads possible, if you don't let your ThreadId escape, there is no way to get an exception you don't bring upon yourself.
Well, there's StackOVerflow and HeapOverflow, and imprecise exceptions are hard to plan for too. All IO monad code should be prepared for exceptions, IMO.
3. simple rules. expressable in pure haskell. 4. can quit immediatly on a SIGINT since the exitWith routine runs on whatever thread called exit, rather than throwing responsibility back to the other threads which might be stuck in a foreign call. (unless you explicitly ask it to)
Don't understand this one - it certainly doesn't help with SIGINT in GHC.
5. you don't have to worry about 'PleaseExit' if you don't want to.
See (2). Also, the exit exception can be treated in the same way as all other unexpected exceptions (just clean up and re-throw).
6. modularity modularity. now that concurrency is part of the standard, we will likely see a lot of libraries using concurrency internally for little things that it wants to keep abstract, or concurrent programs composed with each other. having a global 'throw something to all threads on the system' doesn't feel right.
It's having a global exit that doesn't feel right. But since exit is something that is happening to all the threads in the system, they should all be told about it.
7. subsumes the exitWith throws exceptions everywhere policy.
John
Cheers, Simon

On Mon, Apr 10, 2006 at 02:58:20PM +0100, Simon Marlow wrote:
Suppose I want to do some action with a temporary file:
bracket newTempFile (\f -> removeTempFile f) (\f -> doSomethingWith f)
Under your scheme, this code doesn't get to remove its temporary file on exit, unless I explicitly add an exit handler that throws an exception to the current thread.
I think code like the above should just work. Furthermore, I think it should be an invariant that a thread is never discarded or killed, only sent an exception. Otherwise, how else can I acquire a resource and guarantee to release it when either an exception is raised, the program exits, or the computation completes?
you ask the system to send you an exception on exit.
According to your definition of exitWith above, I can't both raise an exception *and* exit in the same thread. If I register an onExit handler that throws an exception to the current thread, things go wrong if the current thread also calls exitWith. Also, you couldn't call exitWith while holding an MVar, if the handlers need access to the same MVar.
hrm? nothing goes wrong. it is the same as calling 'throw' in the current thread. I don't see how it is unsafe, it is always unsafe to call a routine that needs an MVar you already have held open. you don't call exitWith by accident. There is always 'forkIO exitFailure' in any case.
You didn't show WithTemporaryExitHandler, which complicates things quite a bit.
it is uneeded, only a utility routine, I just didn't want to show the bookkeeping in the handler list to allow deletion of elements as it wasn't important to the scheme. any thread that wants to do bracket style cleanup just asks to be thrown an Exit exception and uses the standard 'bracket' etc.. routines.
Also, your implementation has a race condition - a thread might add another exit handler after the swapMVar.
that is why it is in a swapMVar loop, processing batch's of handlers. at some point, you just gotta accept that another thread didn't get its handler in on time, after all, if things were scheduled differently it might not have gotten there. mainly I wanted to make sure no handlers registered from within other handlers got lost, as those should run to completion being synchronously regiseterd from the handlers point of view.
I think we can probably agree on one thing: exitWith should raise an exception:
exitWith e = throw (ExitException e)
I disagree :) throwTo and throw should raise exceptions, exit should quit the program. though, perhaps we just need another function in the middle. AFAICT, what you are proposing is the same as mine but with forkIO being implemented as forkIO action = forkIO' action' where action' = do myThreadId >>= onExit . throwTo PleaseExit action I just want to have control as to whether that throw me an exception exit handler gets added and not have the implementation wait on my thread to clean up before it can exit if it has nothing special to clean up and might be deep in foreign calls. perhaps if there were just a flag on each thread saying whether they wanted to recieve exit message? though. I still don't like the idea of exitWith throwing anything, just feels really dirty. though, if there were a 'runExitHandlers' routine, what I want can be simulated by 'runExitHandlers >> exitWith_ foo'
This isn't inconsistent with your proposal, and I think it's unambiguously better. The top-level exception handler catches ExitException and performs the required steps (running handlers, calling exit_). As you said, you need a top-level exception handler anyway, this is just a small change to your proposal, moving the exit actions to the top-level exception handler.
but that means you have to wait until the thread with that top level exception handler becomes runnable. which could take arbitrary time if it is in a foreign call. I'd rather stuff be taken care of on the current thread (since we know we are runnable since we just ran exitWith), or on some new exit only thread. as if exitwith behaved as if it were called (forkIO $ exitWith) it just seems odd for your global system exit code to be hidden deep at the base of a certain threads stack somewhere. I don't mind so much exceptions being thrown everywhere to give things a chance to clean up, so much as the requirement we wait for it to fall off the distinguished 'main thread' before the program can actually quit. by default fork
3. simple rules. expressable in pure haskell. 4. can quit immediatly on a SIGINT since the exitWith routine runs on whatever thread called exit, rather than throwing responsibility back to the other threads which might be stuck in a foreign call. (unless you explicitly ask it to)
Don't understand this one - it certainly doesn't help with SIGINT in GHC.
when the signal occurs ghc sends a byte down a pipe, listening thread reads that and calls exitWith, exitWith calss exit_ which kills whole program immediatly, no need to wait for any other thread to do anything or even the foregin call running while the signal occured to complete. (if you don't want to) John -- John Meacham - ⑆repetae.net⑆john⑈

John Meacham
forkIO action = forkIO' action' where action' = do myThreadId >>= onExit . throwTo PleaseExit action
This would be a memory leak: even after the thread finishes, its onExit handler would remain registered. -- __("< Marcin Kowalczyk \__/ qrczak@knm.org.pl ^^ http://qrnik.knm.org.pl/~qrczak/

On Mon, Apr 10, 2006 at 02:19:23PM -0700, John Meacham wrote:
On Mon, Apr 10, 2006 at 02:58:20PM +0100, Simon Marlow wrote:
Suppose I want to do some action with a temporary file:
bracket newTempFile (\f -> removeTempFile f) (\f -> doSomethingWith f)
Under your scheme, this code doesn't get to remove its temporary file on exit, unless I explicitly add an exit handler that throws an exception to the current thread.
I think code like the above should just work. Furthermore, I think it should be an invariant that a thread is never discarded or killed, only sent an exception. Otherwise, how else can I acquire a resource and guarantee to release it when either an exception is raised, the program exits, or the computation completes?
you ask the system to send you an exception on exit.
(As I'm sure you are aware) I'm with Simon on this one. The default cleanup handling should "just work", and you shouldn't need to write special-case code in order to properly clean up. Having exitWith throw exceptions (as it currently does in ghc) is very nice. Perhaps we could have a special sort of thread that is just killed upon exit with no exceptions being raised, but I wouldn't be tempted to use such a thread... unless I suppose I had some unimportant long-running ffi call to make, and didn't want exiting to be slowed down by waiting for it to complete. It would definitely be nice to have interruptible ffi calls (and not just interruptible by exiting), but I'm not really sure how one would go about that. -- David Roundy http://www.darcs.net
participants (4)
-
David Roundy
-
John Meacham
-
Marcin 'Qrczak' Kowalczyk
-
Simon Marlow