Overflow safety when moving a variable to a thread

Whilst trying out concurrent programming in Rust I noticed that the compilers safety checks around overflows are not present when moving a variable into a thread.

This panics thread 'main' panicked at 'attempt to subtract with overflow', src/main.rs:18:13
(Playground link)

use std::thread;
use std::time::Duration;

#[derive(Clone)]
struct Hello {
    timestamp: u8
}

fn main() {
    let test = Hello {
        timestamp: 5
    };

    let hellos: Vec<Hello> = [Hello { timestamp: 10 }, Hello { timestamp: 20 }].to_vec();

    let other_time = 10;

    let a = test.timestamp - other_time;
    println!("test  {}", a);

    let handles: Vec<_> = hellos.into_iter().map(|record: Hello| {
        thread::spawn(move || {
            thread::sleep(Duration::from_millis(100));
            println!("(2nd thread) hello timestamp is {}, minus other time is {}", record.timestamp, record.timestamp - other_time);
        })
    }).collect();

    for handle in handles {
        handle.join().unwrap();
    }
}

Yet the compiler catches this overflow and does not compile

(Playground link)

struct Hello {
    timestamp: u8
}

fn main() {
    let test = Hello {
        timestamp: 5
    };

    let other_time = 10;

    let a = test.timestamp - other_time;
    println!("test  {}", a);
}
   |
12 |     let a = test.timestamp - other_time;
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^ attempt to compute `5_u8 - 10_u8`, which would overflow
   |
   = note: `#[deny(arithmetic_overflow)]` on by default

Removing the reference to other_time in the thread does allow the compiler to successfully check this at compile time.

I am curious to know what other safety guarantees are dropped when moving a variable into a thread.

Compile-time overflow checks are lints, not safety guarantees.

2 Likes

In general, this is not something that can be checked fully during compile time (doing so would be equivalent with solving the famous halting problem, which is proved to be uncomputable).

The best that can be done is opportunistic checking that detects some cases (basically sorting instances into 3 categories: yes, no, don't know). And it can be very arbitrary when something starts falling into the "don't know" category.

1 Like

Thanks both. I have read the RFC and it does make more sense now.

Are there any other linting rules that are omitted when moving variables into threads? Is this documented anywhere?

The issue isn't moving to another thread as such. Rather it is about the complexity of control/data flow. If it gets too complex for the compiler to spot an issue, that's it. (What is complex for the compiler may not be the same thing as what is complex for a human.)

There are definitely other cases, but I don't think there is a definite list. Linting about dead/unreachable code comes to mind as a prime example, and in that case you obviously won't get any runtime errors either.

1 Like

It is certainly curious that the lint disappears due to code that comes after the point where the uncondional overflow occurs. Of course the fact that threads are involved was entirely irrelevant.

Minimized example:

fn main() {
    let a = 5_u8;
    let b = 10_u8;
    let _ = a - b;

    // let _ = &b;
}
   Compiling playground v0.0.1 (/playground)
error: this arithmetic operation will overflow
 --> src/main.rs:4:13
  |
4 |     let _ = a - b;
  |             ^^^^^ attempt to compute `5_u8 - 10_u8`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

error: could not compile `playground` (bin "playground") due to previous error

Rust Playground

fn main() {
    let a = 5_u8;
    let b = 10_u8;
    let _ = a - b;

    let _ = &b;
}
   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/playground`
thread 'main' panicked at 'attempt to subtract with overflow', src/main.rs:4:13
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Rust Playground


If I had to guess: (click for wild speculation) …

There’s probably some form of compiler optimization that can, in the first case, optimize away a and b easily since they’re constant and used once, by-value, and thus the simplified code, containing something like the expression 5_u8 - 10_u8, will trigger the lint. Once a reference to b (or a) is being created anywhere in the program, the variable cannot trivially be optimized away anymore. Note that rustc itself won’t do too many optimizations^ – most of what’s performance relevant is only done a lot later, by LLVM – this is merely about optimizations done early, by rustc itself, in a way that can influence further code analysis. Usually optimizations are done in a way that code behavior (including the question whether or not code compiles) isn’t influenced, but whether or not a lint fires is likely something that is okay to affect. Also, this “optimization” must be very simple and straightforward, as it seems to happen unconditionally, even in Debug builds, i.e. it must be something that’s so easy to detect that it doesn’t make compilation slower.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.