How to timeout sync function?

Hello all,

I am trying to add some sort of timeout to sync call I making to sync function.

fun do_something()  { 
  todo!()
}

fun wait_until() {
    do_something() // this could take 10 seconds let's say and I want to give up after 2 second
}

Here is my naive attempt assuming that I can modify do_something to be async fun do_something
NOTE: I also would like to know what could be done if I couldn't change anything about do_something

fun wait_until() {
    use std::sync::mpsc::sync_channel;
    let (sender, receiver) = sync_channel(1);
    
    do_something().map(|| { sender.send(Ok(())) }).mapErr(|e| { sender.send(Err(e)) });
    
    receiver.recv_timeout(Duration::from_secs(2))

}

However, I found that I can't map/mapErr without await so that's not gonna work.

Then, I thought I could maybe use a thread:

// Note that it's sync again
fun do_something()  { 
  todo!()
}

fun wait_until() {
   use std::sync::mpsc::sync_channel;
   let (sender, receiver) = sync_channel(1);

   std::thread::spawn(|| {
       sender.send(do_something());
    });

   receiver.recv_timeout(Duration::from_secs(2))
}

Then I got bunch of lifetime errors in my actually code. wait_until has bunch of parameters which I need to pass to do_something and it all got messy (somehow) and I thought this shouldn't be this hard given the fact that this could be done with very small change using tokio if the code was async

use tokio::time::{timeout, Duration};

async fn long_future() {
    // do work here
}

let res = timeout(Duration::from_secs(1), long_future()).await;

so I gave up in this one as well.

I would love to know how could you add timeout to sync function without creating much mess. Thanks in advance!

Oops, I just read you want to timeout a sync function. That's generally not possible, if I'm right (unless the function allows you to specify a timeout). You might fork a process perhaps.

Without using async I don't think there is a standard way to do so. Depending on what you are doing you might be able to find some OS-supported way to add a timeout to any network/disk IO, but that isn't supported by std and would require passing the deadline to all sub-functions. But if you're doing IO, async is exactly the way to make it timeout properly (but you have to make sure to use async IO methods in it.

If you are doing something that is CPU bound, or implemented by some sync library you don't control, "aborting" it would require spawning it in another process.

Maybe it's possible to do something inherently unsafe using nix::unistd::fork if you target Posix/Linux/BSD.

What you really want is pthread_kill on POSIX platform, pthread_cancel if you are not interested in Musl or TerminateThread on Windows.

All functions are incredibly unsafe and require insane amount of planning to use them correctly.

I'm 99% sure if topicstarter would try to use them s/he'll spend month untangling the mess, created as the result.

You can't. Sync code is not supposed to be stopped, period. There were many attempts to add that capability but all solution are in the range from “it's such a huge mess” to “it would take us years to make it work somewhat reliably”.

2 Likes

I think killing a thread requires more caution than killing a process. But I agree that likely for the OP, none of these solutions will be a good way to go.

To illustrate why killing a thread is unsafe: imagine that, at the moment you killed it, the thread is holding a lock. What do you do?

  • Nothing: the lock is now held permanently, probably causing other threads in the application to block.
  • Run unwind code just like a panic: now the code in all the functions the thread is running must be capable of unwinding from an arbitrary point. That requires the compiler to generate lots more unwind glue, and it is extremely difficult for programmers to write correct code (or at least correct unsafe code) under those conditions.
  • Don't run unwind but unlock the lock: okay, now it's partially-modified and possibly invalid, so you have problems like the arbitrary unwind case in ensuring it's not invalid in an unsound way, and if the lock is like std::sync::Mutex then it should have the poison flag set, which isn't very much better for the application than “other threads start deadlocking”, though more theoretically recoverable.

Historically, Java (a JIT-VM language which can therefore usually control 100% of the machine code each thread executes, and thus is in a good position for this problem) provided an operation to kill threads, then deprecated it because they found in practice that it could not be reasonably used — even though it wasn't unsound (in the Rust sense of causing memory corruption) it would inevitably lead to application errors due to locks.

You can kill a thread if you can be assured that it shares no locks with other threads — it is performing a completely independent computation — and after the thread finishes or is killed, you are going to inspect its output knowing that any part of it might be half-written. But that is a very hard thing to do because you have to use no libraries except for ones that are cooperating with the no-locks rule. And — if the thread shares no state, then why not make it a subprocess, instead? That's much easier to get correct.

2 Likes

The OS is able to kill processes quite safely. In the past, I solved similar issues with

  • spawn a process that does the work
  • wait_timout the result
  • kill the process on timeout

If sending data via files or stdio is not good enough, you can do shared memory, but this is also quite tricky and unsafe.

1 Like

For the "nothing" case, while Java could implement it (even though it ended up not being a good idea) in Rust that's not even possible because it is incompatible with the (silent) guarantee that either the process aborts or the current thread's execution continues. This is assumed by a lot of unsafe code, even in the standard library, and it's probably too late to change it.

Consider for example if you could spawn a thread with std::thread::scope that borrows from the stack, then kill the thread that called std::thread::scope before it joins with the child thread, and suddently the child thread has a dangling reference.

Thanks everyone who shared insight on the topic. Fortunately, this was sort of an experiment on my side so I won't purse adding time out to sync function as of now. It was my pleasure to read all the insightful comments. Thanks all! :slight_smile:

Just one parting note: fork is very rarely the proper solution because it clones state of the whole process, yet only one thread is cloned.

Which means, essentially, in a child process all threads are suddenly stopped without any warnings!

It's safe in thread-less world, but, alas, Rust programs often spawn threads silently which makes fork as dangerous as stopping threads in practice.

1 Like

Ah, I didn't think of that. Thanks for the warning.

I sometimes wonder how we survived before async became a common thing to do. :sweat_smile:

Just a trivial AtomicBool flag does wonders. And covers 99% of important use cases adequately.

1 Like

The options I see are:

  • move it into a process that can be killed (as others have mentioned)
  • kill the thing that's being waited on (e.g. a socket)
  • continue to work after the timeout and let the timed out task linger in the background (this likely will lead to a resource leak)
  • muck around with low-level APIs like interruptible IO or socket timeouts
  • modify the API to take a timeout or a cancellation token

I'd recommend the last one most of the time when designing synchronous, blocking APIs.

Also note that sync/async and blocking/nonblocking are distinct. The classic epoll + O_NONBLOCK loop doesn't use any async but functions implemented that way generally don't block except in the epoll waiting for more work to do.

Another option: you might be able to unblock a signal like SIGHUP on the thread using pthread_sigmask, send a signal to your own process from another thread, and change your sync call to check an exit condition or otherwise retry on io::ErrrorKind::Interrupted or equivalent for whatever says all you're doing.

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.