Skip to content

block_in_place + block_on + tokio::sync::Mutex may lead to a deadlock #7892

@fuzzypixelz

Description

@fuzzypixelz

In eclipse-zenoh/zenoh#2409 I have a scenario roughly equivalent to to the following Future implementation:

/// A [`Future`] that polls a single mutex in two branches.
///
/// On the first poll, we poll a [`Mutex::lock_owned`] (i.e. [`Self::lock_fut`]) on the mutex while
/// it is held (see [`main`]). Thus we return [`Poll::Pending`].
///
/// On the second poll we use [`task::block_in_place`] and [`Handle::block_on`] to await
/// [`Mutex::lock`] on the mutex.
///
/// Because [`Self::lock_fut`] is assigned the only available permit by Tokio's semaphore logic, we
/// are unable to acquire the mutex on the second poll.
struct BlockOnSecondPoll {
    polled: AtomicBool,
    mutex: Arc<Mutex<()>>,
    lock_fut: Pin<Box<dyn Future<Output = tokio::sync::OwnedMutexGuard<()>>>>,
}

impl Future for BlockOnSecondPoll {
    type Output = ();

    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
        let was_polled = self.polled.fetch_or(true, atomic::Ordering::SeqCst);

        if was_polled {
            tracing::info!("will block forever");
            let () = *task::block_in_place(|| Handle::current().block_on(self.mutex.lock()));
            unreachable!()
        } else {
            match self.lock_fut.as_mut().poll(cx) {
                Poll::Ready(_) => unreachable!(),
                Poll::Pending => {
                    tracing::info!("pending");
                    Poll::Pending
                }
            }
        }
    }
}

(see https://github.com/fuzzypixelz/tokio-inaccessible-permit for the full code)

BlockOnSecondPoll reliably deadlocks the process. Tokio's behavior is perfectly correct here. But I believe that the warning section of the block_in_place documentation:

Be aware that although this function avoids starving other independently spawned tasks, any other code running concurrently in the same task will be suspended during the call to block_in_place. This can happen e.g. when using the join! macro. To avoid this issue, use spawn_blocking instead of block_in_place.

Could be a bit more strongly worded ;)

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-tokioArea: The main tokio crateM-syncModule: tokio/syncT-docsTopic: documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions