Should locks be dropped before calling waker.wake()?

In a multithreaded context, I am using AtomicWaker to schedule wake-ups. When wake or wake_by_ref is called, does the inner function call immediately begin invoking the next async function pointing to another task, or, is it called sometime after the function call to wake/wake_by_ref?

For more reference:

When wake is called, the last line shows a function call:

    #[inline]
    #[stable(feature = "futures_api", since = "1.36.0")]
    pub fn wake(self) {
        // The actual wakeup call is delegated through a virtual function call
        // to the implementation which is defined by the executor.
        let wake = self.waker.vtable.wake;
        let data = self.waker.data;

        // Don't call `drop` -- the waker will be consumed by `wake`.
        crate::mem::forget(self);

        // SAFETY: This is safe because `Waker::from_raw` is the only way
        // to initialize `wake` and `data` requiring the user to acknowledge
        // that the contract of `RawWaker` is upheld.
        unsafe { (wake)(data) };
    }

I'm concerned that this function call begins execution of another task before finishing the current task. Because, if it does this, then it's necessary to drop any locks in order to prevent deadlocking

1 Like

Rust is different in this regard than e.g. coroutine implementations in C++ or Kotlin: .wake() will never run tasks inline, but just makes sure the Future gets scheduled again by the executor as soon as possible.

This eliminates a certain class of deadlock issues. And unless your call to .wake holds a critical scheduler lock, you won't experience a deadlock.

Nevertheless it'it can be a good idea to release previous locks before calling .wake() - since in the case of a multithreaded runtime the new task might get scheduled immediately on another thread, run into the lock and block there until the mutex is released. That additional block and unblock can be avoided by releasing locks before calling .wake().

1 Like

That’s dependent on the executor, isn’t it? I haven’t tried to write an executor that eagerly executes on a wake call, but at first glance it seems like a conforming strategy for RawWakerVTable::wake— You could call Future::poll inline there, hoping store a final result.

1 Like

Yes, you should drop locks before calling wake. The task being polled inside wake would be very unusual, but it doesn't matter because there is a different thing that can happen inside the call to wake: The future could be dropped.

This is because the waker might be holding the last strong reference to the future, and the call to wake() consumes that waker. There are many examples of futures that lock mutexes inside their destructors, for example this is the case with many of Tokio's IO futures, since they often build a linked list inside the futures themselves. This is valid because the futures are pinned, but if you drop the future, you must remove that future from the linked list, which can involve locking a mutex.

7 Likes

What if you call .wake() from inside the future that you are trying to wake? If it were to instantly resume the future, you would suddenly get a second mutable reference to the future itself when trying to resume the future a second time. This is not safe. Aside from the very fact that having two mutable references to the same thing is UB, one of the problems would be that rustc hasn't updated the internal state of the future yet in case of async fn, so it will be resumed at the last yield point.

Sure, in that case the waker is simply not able to poll the future.

Thanks for the replies. I think that explicit documentation of this should be given to downstream users

Also important: calling register may also call wake, so make sure to drop any locks or refrain from locking before calling register!