3

I have the following code:

fn main() {
    let message = "Can't shoot yourself in the foot if you ain't got no gun";
    let t1 = std::thread::spawn(|| {
        println!("{}", message);
    });
    t1.join();
}

rustc gives me the compilation error:

closure may outlive the current function, but it borrows message, which is owned by the current function

This is wrong since:

  1. The function it's referring to here is (I believe) main. The threads will be killed or enter in UB once main is finished executing.

  2. The function it's referring to clearly invokes .join() on said thread.

Is the previous code unsafe in any way? If so, why? If not, how can I get the compiler to understand that?

Edit: Yes I am aware I can just move the message in this case, my question is specifically asking how can I pass a reference to it (preferably without having to heap allocate it, similarly to how this code would do it:

std::thread([&message]() -> void {/* etc */});

(Just to clarify, what I'm actually trying to do is access a thread safe data structure from two threads... other solutions to the problem that don't involve making the copy work would also help).

Edit2: The question this has been marked as a duplicate of is 5 pages long and as such I'd consider it and invalid question in it's own right.

George
  • 3,521
  • 4
  • 30
  • 75

1 Answers1

5

Is the previous code 'unsafe' in any way ? If so, why ?

The goal of Rust's type-checking and borrow-checking system is to disallow unsafe programs, but that does not mean that all programs that fail to compile are unsafe. In this specific case, your code is not unsafe, but it does not satisfy the type constraints of the functions you are using.

  1. The function it's referring to clearly invokes .join() on said thread.

But there is nothing from a type-checker standpoint that requires the call the .join. A type-checking system (on its own) can't enforce that a function has or has not been called on a given object. You could just as easily imagine an example like

let message = "Can't shoot yourself in the foot if you ain't got no gun";
let mut handles = vec![];
for i in 0..3 {
    let t1 = std::thread::spawn(|| {
        println!("{} {}", message, i);
    });
    handles.push(t1);
}
for t1 in handles {
    t1.join();
}

where a human can tell that each thread is joined before main exits. But a typechecker has no way to know that.

  1. The function it's referring to here is (I believe) main. So presumably those threads will be killed when main exists anyway (and them running after main exists is ub).

From the standpoint of the checkers, main is just another function. There is no special knowledge that this specific function can have extra behavior. If this were any other function, the thread would not be auto-killed. Expanding on that, even for main there is no guarantee that the child threads will be killed instantly. If it takes 5ms for the child threads to be killed, that is still 5ms where the child threads could be accessing the content of a variable that has gone out of scope.

To gain the behavior that you are looking for with this specific snippet (as-is), the lifetime of the closure would have to be tied to the lifetime of the t1 object, such that the closure was guaranteed to never be used after the handles have been cleaned up. While that is certainly an option, it is significantly less flexible in the general case. Because it would be enforced at the type level, there would be no way to opt out of this behavior.

You could consider using crossbeam, specifically crossbeam::scope's .spawn, which enforces this lifetime requirement where the standard library does not, meaning a thread must stop execution before the scope is finished.

In your specific case, your code works fine as long as you transfer ownership of message to the child thread instead of borrowing it from the main function, because there is no risk of unsafe code with or without your call to .join. Your code works fine if you change

let t1 = std::thread::spawn(|| {

to

let t1 = std::thread::spawn(move || {
Gurwinder Singh
  • 38,557
  • 6
  • 51
  • 76
loganfsmyth
  • 156,129
  • 30
  • 331
  • 251
  • I didn't say there's not chance of threads still running when main exit, I said it was ub and as such shouldn't really be relevant to any compiler if it happens, but I get the logic applies by the checker here. – George Aug 03 '17 at 08:12
  • 1
    However for the sake of my sanity I do enjoy sharing state such as container and ro variables between threads. I will try using crossbeam once I get to a normal computer. – George Aug 03 '17 at 08:13
  • 3
    @George: Note that Rust's very reason to exist is to have the compiler eliminate UB. This is what Graydon Hoare was striving for and why Mozilla decided to fund a full-time team to work on it. So, yes, rustc will do its best to prevent UB; it's what it was created for ;) – Matthieu M. Aug 03 '17 at 14:42
  • @Matthieu MI don't think you understand what I mean here. Let me put it otherwise: If my computer catches fire that's UB, but I as a programmer shouldn't care about it. – George Aug 04 '17 at 07:48
  • Same with threads lingering after the execution of main, it can happen theoretically, but at that point it's no longer the responsebillity of your program (and even in a program written in Rust, this is still considered UB) – George Aug 04 '17 at 07:49
  • 1
    Also, there's something to say about obfuscation not being a fix to a problem and Rust being horrible when it comes to handling conversions between s/us and 2/4/8bit types (which will result in a lot of UB because there's basically no helper mechanisms in the language for it... especially if you compile cross platform, god help you) – George Aug 04 '17 at 07:51
  • @George: There is a vast difference between a faulty hardware (the most scary source of "bugs", really) and faulty software. The former needs to be dealt with at hardware level (ECC memory is commonly used in servers, CPU/motherboards monitor temperate/voltage to avoid meltdowns, etc...) and the latter needs to be dealt with at software level (because the hardware blindly executes what you tell it to). I will argue, always, that it is the programmer's responsibility to deal with **all** software issues, and this includes threads running after main ends. – Matthieu M. Aug 04 '17 at 08:31
  • @George: As for conversions between signed/unsigned and bitwidths; there is no Undefined Behavior here. Actually, since signedness and bitwidths are explicit, there is not even platform specific behavior (unlike C's `int` whose bitwidth is never really known). The only platform specific behavior you'll get is that `isize`/`usize` may have different bitwidths, though both have the same on a given platform, but once again that's not UB. It's perfectly defined, for the current platform. – Matthieu M. Aug 04 '17 at 08:34
  • programmer's responsibility to deal with all software issues ... There's a difference to be made between user space and kernel space and a difference to be made between your program and another one. You are responsible for preventing bugs in your program, extending to anything past that is an exercise in futility – George Aug 04 '17 at 08:43
  • SO really isn't the place to debate about language philosophy. Rust tries to prevent undefined behavior where possible, and this is a case where it is absolutely possible. Whether you agree with that design is unrelated to the question of how the language behaves. – loganfsmyth Aug 04 '17 at 17:12