Basic threading question: can ruby use real threads?

Because you have no control over when the exception is delivered, which may be at the worst possible moment. Even ensure does not provide adequate protection.

Consider what happens with this code if an exception happens to arrive just before the begin block is processed:

@counter += 1
begin
   # ... do stuff ...
ensure
   @counter -= 1
end

Lest you think there's an easy fix, consider what happens with this second example if an exception arrives after the begin block is entered, but before the counter has been incremented:

begin
   @counter += 1
   # ... do stuff ...
ensure
   @counter -= 1
end

-mental

···

On Thu, 10 May 2007 15:40:57 +0900, Sylvain Joyeux <sylvain.joyeux@polytechnique.org> wrote:

Could you tell us why you think [Thread#raise] is "unsafe" ?

Bill Kelly wrote:

From: "Sylvain Joyeux" <sylvain.joyeux@polytechnique.org>

The APIs are the same between MRI and JRuby, though JRuby deliberately
hedges on the implementation of certain unsafe features like
Thread#kill, Thread#raise, and Thread.critical=.

Thread#raise, "unsafe" ? It is the most useful thread-related functionality I've seen since I'm using threads ! It allows for instance to handle failing rendezvous the proper way (by using exceptions).

Could you tell us why you think it is "unsafe" ?

Hi,

I'm not sure if this is what MenTaLGuY meant, but one way that Thread#raise is unsafe, is that it can raise an exception in the
specified thread while that thread is executing an 'ensure' block.

And to make it clear, we do implement kill, raise, and critical=, with the following limitations:

- There are no guarantees all other threads will have stopped before critical= allows the current thread to continue executing.
- Kill and raise require the target thread to eventually reach a checkpoint where they are willing to "listen" to the kill or raise event. If they don't, the calling thread will wait forever.

I even made these operations a bit cleaner and faster in 0.9.9, but there's no way to do them perfectly with real concurrent threads.

- Charlie

Bill Kelly wrote:

From: "Sylvain Joyeux" <sylvain.joyeux@polytechnique.org>

The APIs are the same between MRI and JRuby, though JRuby deliberately
hedges on the implementation of certain unsafe features like
Thread#kill, Thread#raise, and Thread.critical=.

Thread#raise, "unsafe" ? It is the most useful thread-related functionality I've seen since I'm using threads ! It allows for instance to handle failing rendezvous the proper way (by using exceptions).

Could you tell us why you think it is "unsafe" ?

Hi,

I'm not sure if this is what MenTaLGuY meant, but one way that Thread#raise is unsafe, is that it can raise an exception in the
specified thread while that thread is executing an 'ensure' block.

This can cause a failure of critical resources to be cleaned up
correctly, such as locks on mutexes, etc., as some or all of the
code in the ensure block is skipped.

I first ran into this when I tried to use timeout{} to implement
a ConditionVariable#timed_wait, like:

require 'thread'
require 'timeout'
class ConditionVariable
   def timed_wait(mutex, timeout_secs)
     timeout(timeout_secs) { wait(mutex) } # THIS IS UNSAFE
   end
end

Note that 'timeout' functions by creating a temporary new thread
which sleeps for the duration, then raises an exception in the
'current' thread that invoked timeout.

If the timeout raises its exception at an unlucky moment, the
various internals of ConditionVariable#wait and Mutex#synchronize
that depend on ensure blocks to restore their class invariants are
skipped, resulting in nasty things like a permanently locked mutex.

Not fun... :frowning:

This is disturbing.

Is #timeout inherently unsafe, if it is implemented as a thread, even in MRI ruby's green threads?

Ruby gives you a lot of freedom to do anything you want inside of ensure clauses, and I guess this means that ensure clauses can't be given special treatment--the ensure clause itself might be what needs to be interrupted by the timeout. That seems to rule out treating ensure clauses as a critical section, for example. And it seems to rule out a method like Thread#raise_unless_in_ensure or Thread#raise_after_ensure_finishes.

What if there were two kind of ensure clauses, one which is uninterruptible (to be used only for cleanup that is deterministic) and one which is interruptible (and not guaranteed to finish)?

What's the best practice in current MRI ruby? Use Timeout only in cases where you know it is safe, and otherwise use #select timeouts or whatever else is appropriate?

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

Ruby gives you a lot of freedom to do anything you want inside of ensure clauses, and I guess this means that ensure clauses can't be given special treatment--the ensure clause itself might be what needs to be interrupted by the timeout. That seems to rule out treating ensure clauses as a critical section, for example.

Yeah... I agree in general. However, . . .

What if there were two kind of ensure clauses, one which is uninterruptible (to be used only for cleanup that is deterministic) and one which is interruptible (and not guaranteed to finish)?

I'd probably settle for a Thread#raise_asap (or #raise_safe, or
whatever.) Because in theory, even though one *could* write
unfriendly code in an ensure block that took a long time to execute; in
practice all the ensure blocks I can recall seeing were doing what appeared to be very simple, deterministic cleanup.

With that, it would be trivial to implement a Timeout#timeout_safe; and
I suspect it would be rare indeed when someone would need or want
to use the unsafe version...

What's the best practice in current MRI ruby? Use Timeout only in cases where you know it is safe, and otherwise use #select timeouts or whatever else is appropriate?

That's pretty much my current practice. I use Timeout sparingly, and am
extremely careful with it.

Regards,

Bill

···

From: "Joel VanderWerf" <vjoel@path.berkeley.edu>

This is disturbing.

Is #timeout inherently unsafe, if it is implemented as a thread, even in
MRI ruby's green threads?

Correct. #timeout as presently implemented is NEVER safe to use.

What if there were two kind of ensure clauses, one which is
uninterruptible (to be used only for cleanup that is deterministic) and
one which is interruptible (and not guaranteed to finish)?

Nope, that's still not sufficient. The fundamental problem is that other threads can _arbitrarily_ mess with control flow by injecting exceptions via Thread#raise. There's no way to guarantee invariants will be preserved, no matter how clever your ensure implementation is.

-mental

···

On Sat, 12 May 2007 03:46:58 +0900, Joel VanderWerf <vjoel@path.berkeley.edu> wrote:

- Kill and raise require the target thread to eventually reach a
checkpoint where they are willing to "listen" to the kill or raise
event. If they don't, the calling thread will wait forever.

I think this is fair. I also think that the core developer may need to
really think about what should be a checkpoint *in the language itself*
(for instance, end of a block, end of a method, whatever). For instance,
not allowing to have a checkpoint in an "ensure" context would fix the
ensure-related issues (but may not fix others that I don't see)

My main use of Thread#raise is "returning" from a ConditionVariable#wait.
Can I assume this is seen as a checkpoint by JRuby ?

Sylvain Joyeux

No, that's still not sufficient. If an exception can be injected at an arbitrary point by an external source, there's simply no way to write sane code.

The only safe model for inter-thread communication is one where both participants agree to communicate. You'd need something like a Thread#receive_exception that was explicitly called on the receiving side -- of course, that's not too helpful for the purposes to which Thread#raise is usually put.

The fundamental problem is really that most of Ruby's blocking operations don't have allowance for timeout built into their API, which is particularly sad since they're all built on top of select (in MRI, anyway) and that would be really easy to do. The whole #timeout thing is an unsafe workaround which can never be made safe by its very nature.

-mental

···

On Sat, 12 May 2007 06:43:39 +0900, "Bill Kelly" <billk@cts.com> wrote:

I'd probably settle for a Thread#raise_asap (or #raise_safe, or
whatever.) Because in theory, even though one *could* write
unfriendly code in an ensure block that took a long time to execute; in
practice all the ensure blocks I can recall seeing were doing what
appeared to be very simple, deterministic cleanup.

MenTaLguY wrote:
...

What if there were two kind of ensure clauses, one which is
uninterruptible (to be used only for cleanup that is deterministic) and
one which is interruptible (and not guaranteed to finish)?

Nope, that's still not sufficient. The fundamental problem is that other threads can _arbitrarily_ mess with control flow by injecting exceptions via Thread#raise. There's no way to guarantee invariants will be preserved, no matter how clever your ensure implementation is.

But what if we limit that arbitrary power by adding a new construct?

Suppose thread1 is executing this code:

   begin
   ensure_uninterruptible # not in ruby yet
     # quick and deterministic cleanup code
   end

and suppose that thread2 does this:

   thread1.raise

Can't we implement ensure_uninterruptible in such a way that thread1.raise waits until thread1 finishes the clause (or maybe raises an error in thread2 instead of in thread1)?

I'm not sure it's a good idea, but is there a reason it couldn't be done with green threads?

···

--
       vjoel : Joel VanderWerf : path berkeley edu : 510 665 3407

I'd probably settle for a Thread#raise_asap (or #raise_safe, or
whatever.) Because in theory, even though one *could* write
unfriendly code in an ensure block that took a long time to execute; in
practice all the ensure blocks I can recall seeing were doing what
appeared to be very simple, deterministic cleanup.

No, that's still not sufficient. If an exception can be injected at an arbitrary
point by an external source, there's simply no way to write sane code.

I thought what I was proposing was to _limit_ the current arbitrariness.

When I write code, I'm used to considering that any method call I make
may raise an exception. (Including exceptions like NoMemoryError and
Interrupt.)

If we could prevent Thread#raise from happening within an ensure block,
and we could guarantee that an assignment to a variable would similarly
be non-interruptable (meaning, an exception can't be raised between
the point where a method call completes, and its result is assigned to
a variable) ... wouldn't that be getting us pretty close to being able to
write "sane" code?

Or am I just missing out on something fundamental? (If so, I'm most
definitely interested to learn.)

Regards,

Bill

···

From: "MenTaLguY" <mental@rydia.net>

On Sat, 12 May 2007 06:43:39 +0900, "Bill Kelly" <billk@cts.com> wrote:

Sure. But it's still not enough -- you'll notice that the examples I gave earlier were concerned with race conditions around entry of the protected section itself, not even the ensure clause.

What you actually need to do is make uninterruptability the universal default; interruptability at a specific point must be specifically allowed for.

-mental

···

On Sat, 12 May 2007 07:08:05 +0900, Joel VanderWerf <vjoel@path.berkeley.edu> wrote:

Can't we implement ensure_uninterruptible in such a way that
thread1.raise waits until thread1 finishes the clause (or maybe raises
an error in thread2 instead of in thread1)?

If we could prevent Thread#raise from happening within an ensure block,
and we could guarantee that an assignment to a variable would similarly
be non-interruptable (meaning, an exception can't be raised between
the point where a method call completes, and its result is assigned to
a variable) ... wouldn't that be getting us pretty close to being able to
write "sane" code?

Closer, but in this context "safe" is an all-or-nothing proposition. Simply making variable assignments atomic falls far, far short of what's needed. What you're actually groping towards is atomic transactions -- the ability to take an arbitrary block of code and say "if this block does not complete successfully, any of its effects should be rolled back before propagating the exception". Of course, not all effects (e.g. IO) _can_ be rolled back, so you're still not entirely safe in that case.

Or am I just missing out on something fundamental? (If so, I'm most
definitely interested to learn.)

Yes. You have to be worried about all of the code involved (e.g. also the arbitrarily complex implementations of any methods you call), not just your own immediate code.

-mental

···

On Sat, 12 May 2007 07:47:57 +0900, "Bill Kelly" <billk@cts.com> wrote:

Another way to do this is to work in terms of atomic transactions (e.g. STM, if your STM implementation is itself safe in the face of asynchronous exceptions).

-mental

···

On Sat, 12 May 2007 08:25:28 +0900, MenTaLguY <mental@rydia.net> wrote:

What you actually need to do is make uninterruptability the universal
default; interruptability at a specific point must be specifically allowed
for.

Or am I just missing out on something fundamental? (If so, I'm most
definitely interested to learn.)

Yes. You have to be worried about all of the code involved (e.g. also the
arbitrarily complex implementations of any methods you call), not just your
own immediate code.

Oh. That's different.

I was just looking for whether we could arrive at constraints that would allow a given routine to be written in a way that 100% safely handled
these new hypothetical Thread#raise semantics.

If I happen to be calling other routines that aren't written to handle
exceptions safely, well, yeah, that sucks for me.

But if one method can be written to be safe, why can't they all? Why, then,
would I have to worry about anything other than my own immediate code,
unless I am assuming the code I'm calling is broken WRT exception handling, in which case... well, it's broken. But that's different.

?

Regards,

Bill

···

From: "MenTaLguY" <mental@rydia.net>

If I happen to be calling other routines that aren't written to handle
exceptions safely, well, yeah, that sucks for me.

That goes for most of core and stdlib, though. At least in the face of
_asynchronous_ exceptions. Have a look at Set#replace, for instance.

But if one method can be written to be safe,

I'm not sure any non-trivial method can be. Additionally, if we're
dealing with a non-green-threaded case where an exception can be delivered
at any time (not just set scheduling points), it's really not possible
at all.

Rather than my continuing to make bald assertions, though, if you'd like
to provide a code sample I can probably illustrate what I mean.

-mental

···

On Sat, 12 May 2007 08:40:43 +0900, "Bill Kelly" <billk@cts.com> wrote:

Rather than my continuing to make bald assertions, though, if you'd like
to provide a code sample I can probably illustrate what I mean.

Thanks,

Mainly this has happened when I wanted to do some sort of timed-wait on a condition variable.

I've seen this concept reccur as recently as this week...
Obviously if Ruby had the concept of timing out on a
mutex or cond-var wait...

But I'm sorry, I've gotten ahead of myself. I think, if we
could just ...... sort of, <3

Er, e hehe

Well my conceptualization was something along the lines
of what boost::shared_ptr does in C++; that is: there are
certain language semantics that are _guaranteed_....

Smart-ptrs only work in C++ because of language-given
semantics that describe exactly what will happen on an
assignment following a function call, and dtors called when a function exits, etc.

My feeling was, ensure blocks in ruby, could be similarly
described, such that, one could _guarantee_ certain
semantics .... similar to what's guaranteed in C++ that
makes wrapped ptrs be 100% reliable as smart-ptrs.

RUBY NEEDS SMART PTR SEMANTICS

is a concept

worth-while?

:smiley:

Just a thought,

Regards,

Bill

···

From: "MenTaLguY" <mental@rydia.net>

RUBY NEEDS SMART PTR SEMANTICS

Ugh, sorry for the noise.

I was having a weird day yesterday.

···

From: "Bill Kelly" <billk@cts.com>