20

In Rust, a panic terminates the current thread but is not sent back to the main thread. The solution we are told is to use join. However, this blocks the currently executing thread. So if my main thread spawns 2 threads, I cannot join both of them and immediately get a panic back.

let jh1 = thread::spawn(|| { println!("thread 1"); sleep(1000000); };
let jh2 = thread::spawn(|| { panic!("thread 2") };

In the above, if I join on thread 1 and then on thread 2 I will be waiting for 1 before ever receiving a panic from either thread

Although in some cases I desire the current behavior, my goal is to default to Go's behavior where I can spawn a thread and have it panic on that thread and then immediately end the main thread. (The Go specification also documents a protect function, so it is easy to achieve Rust behavior in Go).

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Greg Weber
  • 3,228
  • 4
  • 21
  • 22
  • 1
    Panics **are** immediately reported and the thread stops running. Your question really seems to be "how can I join on the first thread that ends of a set of running threads". You want behavior similar to `select`. – Shepmaster Mar 14 '16 at 13:34
  • 1
    What you're saying about Go is not correct. In Go, if a goroutine panics and if this panic is not recovered from, the whole process is aborted. The panic is never "panicked back" to the main thread. Or do you really mean that you need the whole process to stop if the panic has not been caught? – Vladimir Matveev Mar 14 '16 at 13:34
  • good point, in go the main thread doesn't get unwound, the program just crashes, but the original panic is reported. This is in fact the behavior I want (although ideally resources would get cleaned up properly everywhere). – Greg Weber Mar 14 '16 at 17:35
  • will ` std::process::exit` called from thread terminate entire program or only the current thread? mb you can use it instead of panic and return some exit code to main thread? – Ivan Temchenko Mar 15 '16 at 08:24
  • calling exit is a good idea. It doesn't seem to be possible until `recover` has stabilized though. – Greg Weber Mar 16 '16 at 03:58

3 Answers3

22

Updated for Rust 1.10+, see revision history for the previous version of the answer

good point, in go the main thread doesn't get unwound, the program just crashes, but the original panic is reported. This is in fact the behavior I want (although ideally resources would get cleaned up properly everywhere).

This you can achieve with the recently stable std::panic::set_hook() function. With it, you can set a hook which prints the panic info and then exits the whole process, something like this:

use std::thread;
use std::panic;
use std::process;

fn main() {
    // take_hook() returns the default hook in case when a custom one is not set
    let orig_hook = panic::take_hook();
    panic::set_hook(Box::new(move |panic_info| {
        // invoke the default handler and exit the process
        orig_hook(panic_info);
        process::exit(1);
    }));

    thread::spawn(move || {
        panic!("something bad happened");
    }).join();

    // this line won't ever be invoked because of process::exit()
    println!("Won't be printed");
}

Try commenting the set_hook() call out, and you'll see that the println!() line gets executed.

However, this approach, due to the use of process::exit(), will not allow resources allocated by other threads to be freed. In fact, I'm not sure that Go runtime allows this as well; it is likely that it uses the same approach with aborting the process.

Vladimir Matveev
  • 120,085
  • 34
  • 287
  • 296
  • 1
    It seems there is no way to kill a thread? If there was then to cleanup resources I could hold a reference to each spawned thread and kill each one. – Greg Weber Mar 16 '16 at 14:07
  • 1
    There is no portable way to kill a thread at all. For example, you cannot kill (cancel) an arbitrary thread using pthreads (POSIX threads, which are natively available in Linux, for example). Various runtimes may provide some facilities for that (like Java's `Thread#stop()`) , but internally they are implemented using synchronization primitives and, moreover, they are not very safe and convenient to use. The only safe way to terminate a thread remotely is to manually provide a remotely triggered flag which a thread knows how to check. This may or may not be suitable for your purpose. – Vladimir Matveev Mar 16 '16 at 14:34
  • There is now a stable version of this API. If you're running Rust 1.10.0 or newer, the equivalent panic handler APIs are now: https://doc.rust-lang.org/beta/std/panic/fn.set_hook.html and https://doc.rust-lang.org/beta/std/panic/fn.take_hook.html – Lucien Greathouse Mar 12 '20 at 22:04
10

I tried to force my code to stop processing when any of threads panicked. The only more-or-less clear solution without using unstable features was to use Drop trait implemented on some struct. This can lead to a resource leak, but in my scenario I'm ok with this.

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


static THREAD_ERROR_CODE: i32 = 0x1;
static NUM_THREADS: u32 = 17;
static PROBE_SLEEP_MILLIS: u64 = 500;

struct PoisonPill;

impl Drop for PoisonPill {
    fn drop(&mut self) {
        if thread::panicking() {
            println!("dropped while unwinding");
            process::exit(THREAD_ERROR_CODE);
        }
    }
}

fn main() {
    let mut thread_handles = vec![];

    for i in 0..NUM_THREADS {
        thread_handles.push(thread::spawn(move || {
            let b = PoisonPill;
            thread::sleep(Duration::from_millis(PROBE_SLEEP_MILLIS));
            if i % 2 == 0 {
                println!("kill {}", i);
                panic!();
            }
            println!("this is thread number {}", i);
        }));
    }

    for handle in thread_handles {
        let _ = handle.join();
    }
}

No matter how b = PoisonPill leaves it's scope, normal or after panic!, its Drop method kicks in. You can distinguish if the caller panicked using thread::panicking and take some action — in my case killing the process.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Very nice answer. Especially if you want to chose threads that abort process and don't mix with user panic hooks! – S.R Jul 26 '22 at 12:20
3

Looks like exiting the whole process on a panic in any thread is now (rust 1.62) as simple as adding this to your Cargo.toml:

[profile.release]
panic = 'abort'

[profile.dev]
panic = 'abort'

A panic in a thread then looks like this, with exit code 134:

thread '<unnamed>' panicked at 'panic in thread', src/main.rs:5:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted (core dumped)
Jakob
  • 193
  • 1
  • 5