#thread-local #future #thread #async #local-storage #atexit

nolocal-block-on

futures_lite::future::block_on that can run without using thread-locals

2 stable releases

1.0.1 Aug 5, 2024

#543 in Asynchronous


Used in async-blocking-stdio

MPL-2.0 license

10KB

nolocal-block-on

Version of futures_lite::future::block_on that does not use any thread locals internally. This is a very specialized crate designed for use in special parts of a program (such as in libc::atexit callbacks, or things occurring pre- or post- main) where thread local storage will not function.

If your future itself uses thread-local storage, though, then you're out of luck (or need to change the implementation of the futures you're using).

You almost certainly want the normal futures_lite::future::block_on except in specific circumstances as described above.

Examples

The way you invoke the function provided by this crate is the same as the futures-lite version:

let val = nolocal_block_on::block_on(async {
    1 + 2
});

assert_eq!(val, 3);

However, the futures-lite version does not work in (rare) places where thread-local storage cannot function - for example, running after the main rust runtime is getting shut down. To illustrate this, we demonstrate it via libc::atexit.

The following code, using futures_lite::future::block_on, might panic due to using thread-locals in the implementation - and in particular, when multiple threads are involved (such as with blocking::Unblock), it will panic consistently:

use futures_lite::{future::block_on, io::AsyncWriteExt};
use async_lock::Mutex;
use std::{sync::LazyLock, io::{Stdout, self}};
use blocking::Unblock;

static STDOUT: LazyLock<Mutex<Unblock<Stdout>>> = LazyLock::new(|| {
    Mutex::new(Unblock::new(io::stdout()))
});

block_on(async { STDOUT.lock().await.write(b"hello program\n").await });

extern "C" fn cleanup() {
    block_on(async {
        let mut locked_stdout = STDOUT.lock().await;
        locked_stdout.write(b"goodbye program!\n").await;
        locked_stdout.flush().await;
    });
}

unsafe { libc::atexit(cleanup); }

However, the following code using nolocal_block_on::block_on, will not panic, as the implementation doesn't use any thread-locals (for non-trivial futures, you might need to check to make sure they don't use thread-locals in their implementations):

use nolocal_block_on::block_on;
use futures_lite::io::AsyncWriteExt;
use async_lock::Mutex;
use std::{sync::LazyLock, io::{Stdout, self}};
use blocking::Unblock;

static STDOUT: LazyLock<Mutex<Unblock<Stdout>>> = LazyLock::new(|| {
    Mutex::new(Unblock::new(io::stdout()))
});

block_on(async { STDOUT.lock().await.write(b"hello program\n").await });

extern "C" fn cleanup() {
    block_on(async {
        let mut locked_stdout = STDOUT.lock().await;
        locked_stdout.write(b"goodbye program!\n").await;
        locked_stdout.flush().await;
    });
}

unsafe { libc::atexit(cleanup); }

Dependencies

~18KB