Description
Exception safety is a very well-known problem in C++.
Consider this:
struct Foo {
int* ptr;
};
void modify(Foo& value) {
delete foo.ptr;
foo.ptr = new int;
}
void error_proof_modify(Foo& value) {
try {
modify(value);
} catch(...) {
cerr << "Error, but let's continue!";
}
}
If the call to new
throws an exception, the error gets caught and the execution continue. However the Foo
object is now in an invalid state with its pointer pointing to memory that has been free'd.
Before #20615 and before the change in Send
this is not possible in Rust, because when a thread panics all objects that are local to this thread are no longer accessible. It's also the reason why mutexes are poisoned in case of a panic.
But now that it's possible to send local variables to other threads, and ignore when this other thread panics, situations like this can totally happen.
Let's take this code for example:
pub struct Foo {
val1: int,
val2: int,
val3: int,
calculation_result: int, // must always be equal to val1+val2+val3
}
impl Foo {
pub fn set_val1(&mut self, value: int) {
self.val1 = value;
self.update_calculation();
}
pub fn set_val2(&mut self, value: int) {
self.val2 = value;
self.update_calculation();
}
pub fn set_val3(&mut self, value: int) {
self.val3 = value;
self.update_calculation();
}
fn update_calculation(&mut self) {
if self.val1 == 127 { panic!("for the sake of this example, we panic") };
self.calculation_result = self.val1 + self.val2 + self.val3;
}
}
Looks safe, right?
But what if you use that "ignore panics" trick?
let mut foo: Foo = ...; // whatever
Thread::scoped(|| {
foo.set_val1(127);
}).join(); // ignoring the panic
use_foo(&foo); // continue using `foo`
At the end of this code, you have a Foo
object in an invalid state because calculation_result
is not equal to val1+val2+val3
as it should be. Continuing to use the Foo object in this invalid state could lead to weird and hard to debug results (depending on the rest of the code).
With the latest reforms in thread, library writer really have to take exception safety (or "panic safety") into account by avoiding the kind of code written above. You may argue that it's just the fault of the person who wrote the Foo
struct. The problem is that exception safety is hard. Too hard to get right.
In my opinion, JoinGuard::join
should just panic if the underlying thread panicked, without leaving the choice to the user. The only situation in which a try_join
function (or another name) is safe is for JoinGuard<'static>
.
cc @aturon