Tokio and UnwindSafe

How does tokio::task::spawn (and its variants) avoid an UnwindSafe bound on the supplied future? Is there some mechanism besides std::panic::catch_unwind that the poller uses?

Just like how this is "incorrect" for std::thread::spawn, it is also "incorrect" for tokio::spawn to lack these. However, the traits are safe, so it's not a big deal.

2 Likes

Note that you don't need UnwindSafe to actually catch the panics - unwind safety isn't about the ability to catch the panics, but about what you can safely assume about anything shared between the code that panicked and the code catching the panic.

The point of UnwindSafe is to tell you that this type's invariants are never broken by a panic, and thus it's safe to inspect a value of that type after a panic has been caught. Because tokio::task::spawn does not inspect the contents of the future after a panic has been caught, it doesn't need the future to be UnwindSafe. It does need other things (such as the implementation details of JoinHandle) to be UnwindSafe, since it will touch them after a panic, but Tokio is written so that those things are UnwindSafe.

2 Likes

Couldn't you create a future that owns a Rc<RefCell<T>>, spawn it locally, have it panic, and then later observe the RefCell contents? Is UnwindSafe just an advisory tag?

It's safe to implement and there's even a provided (and safe) way to ignore it, so yes. It's meant to serve as a lint for logic errors. If it's possible to create UB by ignoring it, that's still the fault of some unsafe somewhere, and not of the programmer ignoring it.

See also.

3 Likes

You can, and as it happens, both Rc and RefCell are UnwindSafe if T is both RefUnwindSafe and UnwindSafe.

This is the only reason UnwindSafe exists - it allows you to opt-in to error E0277 where you think that there's a strong chance that a parameter not being unwind safe is a logic error. catch_unwind uses it, because it expects that anything modified in the closure you pass it will also be referenced after any unwind, and this should be caught.

Doesn't Rc<S>: UnwindSafe require S: RefUnwindSafe (which RefCell<T> doesn't satisfy)?

What is different about catch_unwind vs spawning a task or thread? Just that you need to opt out of unwind safety rather than in? Or is there something special about unwinding a thread?

Good point - I misread the docs for RefCell.

So, the only way to not be unwind safe is to share something that is not unwind safe with your environment - everything that's internal to you is irrelevant (since it's all destroyed during unwinding anyway) - and the presumption is different between spawn and catch_unwind.

With catch_unwind, if the closure shares something with the environment outside catch_unwind that's not unwind safe, it's quite likely that it does so because you're going to mutate it from inside the closure and then inspect it later - the authors of catch_unwind want you to know that you're doing something that probably won't work the way you want it to.

With spawn, the normal case is that you're not going to inspect anything that you've passed into the closure - chances are that if it is shared with the environment, it's either unwind safe (e.g. Mutex<_>), or it's just an artefact of how closures capture things by default, and you're not going to look at it after handing it off. In this case, the E0277 error because your argument to spawn is not unwind safe is almost always going to be a false positive, and rather than forcing you to use AssertUnwindSafe all the time, it's easier to not have the bound to begin with.

1 Like

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.