Network accept loop with graceful shutdown implementation
I have a prospective implementation of a network accept loop with graceful shutdown. This email builds upon the previous discussion "Help needed interrupting accepting a network connection". In this code, just the accept loop part has been factored out and put into its own module. My hope is that if a fully correct version can be written, it will be useful to other projects. The source is available at http://code.catdancer.ws/acceptloop/ and is in the public domain. This AcceptLoop module is currently experimental, relying on an "either-or" guarantee for interruptible operations... which I don't know yet whether Haskell implementations provide -- or even intend to provide. Chris Kuklewicz provided several critical insights: * The return value of the 'accept' call can be passed out of the accept thread, allowing the code which implements accept with graceful shutdown to be separated from the code which handles the incoming client connection. * Inside of a 'block', interruptible operations may not (will not?) allow an asynchronous exception to be raised if the operation does not block. * Clarification for me of the desirable property that inside of a 'block', interruptible operations are either-or: either they allow an asynchronous operation to be raised, or they perform their operation, but not both. I made the following design decisions: In my original implementation, I used a custom datatype to throw a dynamic asynchronous exception to the thread calling 'accept'. In Chris' rewrite, he used 'killThread', which throws a 'ThreadKilled' asynchronous exception. I choose to continue to throw (and catch) only the specific, custom exception for the purpose of breaking out of the 'accept'. A robust implementation may need to catch other exceptions, however the desired behavior of the thread on receiving an *unexpected* exception may be different, and so I choose to leave handling of such an unexpected exception unimplemented for now. Chris uses STM instead of MVar's for communication with the accept thread. The challenge of writing code with STM or MVar's in the presence of asynchronous exceptions is exactly the same: either a call to 'atomically' or a call to an MVar operation such as 'putMVar' may allow an asynchronous exception to be raised inside of a 'block', and the code then needs to deal with that. It seems to me that MVar's could be implemented in terms of STM, and so the question is: are MVar's a more natural, higher level description of the desired semantics in this case, and would composable transactions be useful? A key insight is that in this implementation, a separate thread is used not for the purposes of concurrency, but solely to limit the scope of the thrown asynchronous exception. Once a result is available from the accept thread (either "Just" an incoming connection, or "Nothing" to say that the accept loop has shutdown), that result can then be used in a concurrent fashion, such as being handled in a child thread, passed into an STM transaction or written to a channel, etc. But there is no need to use composable transactions *inside* of the AcceptLoop module. Thus the only reason to use STM instead of MVar's inside the implementation is if it turns out that STM has the desired "either-or" behavior but MVar's don't. To avoid the "unlock (return ())" issue that Chris discovered, this implementation uses an additional MVar to indicate that a shutdown is in process. Thus (if the implementation is correct) the accept loop will shutdown either because of the MVar flag or by receiving the asynchronous exception inside of the 'accept'. To address the issue that Chris noticed of a race condition that new threads cannot be started in a 'block' state, yet another MVar is set by the accept thread to indicate that it is now inside of a 'block' and is ready to receive the asynchronous exception.
Cat Dancer wrote:
I have a prospective implementation of a network accept loop with graceful shutdown.
Could you add info about where to get your code (or the code) itself to the wiki at http://haskell.org/haskellwiki/Concurrency_demos/Graceful_exit ?
To avoid the "unlock (return ())" issue that Chris discovered, this implementation uses an additional MVar to indicate that a shutdown is in process. Thus (if the implementation is correct) the accept loop will shutdown either because of the MVar flag or by receiving the asynchronous exception inside of the 'accept'.
Small clarification: You don't need a safepoint in your code. But "unblock yield" is the right code for a safepoint; the "unblock (return ())" suggested by the published paper *does not work* in my small test, while "unblock yield" worked every time in a small test. Simon may updated the documentation eventually to reflect this.
To address the issue that Chris noticed of a race condition that new threads cannot be started in a 'block' state, yet another MVar is set by the accept thread to indicate that it is now inside of a 'block' and is ready to receive the asynchronous exception.
On 12/7/06, Chris Kuklewicz <haskell@list.mightyreason.com> wrote:
Could you add info about where to get your code (or the code) itself to the wiki at http://haskell.org/haskellwiki/Concurrency_demos/Graceful_exit ?
OK, I did.
"unblock yield" is the right code for a safepoint
Be careful. You are relying on the runtime to wake up the throwing thread, to let it run long enough to raise the asynchronous exception, and to have that complete before the thread doing the "unblock yield" is resumed. While a particular Haskell implementation might do all that on a single processor system, it's hard to see how such a guarantee could ever be offered on a multiprocessor system. These days it's common to buy a computer with a dual core cpu, before long we'll be getting cpu's with ten cores... and in a few years we'll be seeing hundred core cpu's. "yield" is meaningless on a multiprocessor system. I'd suggest that if you're using a concurrent language and you find that the only way you can implement an algorithm is by using "yield", either A) you're wrong and there is a way to implement it without "yield", or B) your concurrent language is deficient and should be fixed! :-)
On 12/7/06, Chris Kuklewicz <haskell@list.mightyreason.com> wrote:
Small clarification: You don't need a safepoint in your code. But "unblock yield" is the right code for a safepoint; the "unblock (return ())" suggested by the published paper *does not work* in my small test, while "unblock yield" worked every time in a small test. Simon may updated the documentation eventually to reflect this.
I think people are misunderstanding the nature of a safepoint. The safepoint is a point at which you are prepared to have exceptions delivered. This does not mean that they *will* be delivered, just that they can. If you need to *wait* for an asynchronous exception, then you shouldn't be using them at all. -- Taral <taralx@gmail.com> "You can't prove anything." -- Gödel's Incompetence Theorem
Taral wrote:
On 12/7/06, Chris Kuklewicz <haskell@list.mightyreason.com> wrote:
Small clarification: You don't need a safepoint in your code. But "unblock yield" is the right code for a safepoint; the "unblock (return ())" suggested by the published paper *does not work* in my small test, while "unblock yield" worked every time in a small test. Simon may updated the documentation eventually to reflect this.
I think people are misunderstanding the nature of a safepoint. The safepoint is a point at which you are prepared to have exceptions delivered. This does not mean that they *will* be delivered, just that they can. If you need to *wait* for an asynchronous exception, then you shouldn't be using them at all.
Right. If a thread mostly runs inside 'block' with the occasional safe point, then your exceptions are not really asynchronous, they're synchronous. In this case, I'd say a better solution is to have an explicit event queue, and instead of the safe point take an event from the queue. The action on receiving an event can be to raise an exception, if necessary. Cheers, Simon
participants (4)
-
Cat Dancer -
Chris Kuklewicz -
Simon Marlow -
Taral