Skip to content

Not joining all threads = memory leaked on process exit #759

@faern

Description

@faern

Hi. I'm implementing a library at a low level which allocates and frees memory manually. To try to keep it correct I run example binaries and tests under Valgrind a lot. However, when using async-std my programs and tests very often seem to not correctly join threads before exiting, resulting in leaks that make Valgrind sad and my automated tests fail. I experience this on Linux and have not tried it elsewhere.

A simple failing test. Placed under tests/all_tests.rs:

#[async_std::test]
async fn just_sleep() {
    async_std::task::sleep(std::time::Duration::from_millis(1)).await;
}

You can run it under vanilla Valgrind with valgrind --leak-check=full ./target/debug/whatever_the_filename_is. But cargo valgrind has prettier output. Due to the racy nature of this it might not always fail, but for me it does ~90% of the runs:

$ cargo valgrind --test all_tests
....
       Error Leaked 24 B
        Info at malloc (vg_replace_malloc.c:309)
             at alloc::alloc::alloc (alloc.rs:84)
             at alloc::alloc::exchange_malloc (alloc.rs:206)
             at alloc::sync::Arc<T>::new (sync.rs:302)
             at futures_timer::global::current_thread_waker (global.rs:104)
             at futures_timer::global::run (global.rs:59)
             at futures_timer::global::HelperThread::new::{{closure}} (global.rs:28)
             at std::sys_common::backtrace::__rust_begin_short_backtrace (backtrace.rs:126)
             at std::thread::Builder::spawn_unchecked::{{closure}}::{{closure}} (mod.rs:470)
             at <std::panic::AssertUnwindSafe<F> as core::ops::function::FnOnce<()>>::call_once (panic.rs:315)
             at std::panicking::try::do_call (panicking.rs:292)
             at __rust_maybe_catch_panic (lib.rs:80)
       Error Leaked 288 B
        Info at calloc (vg_replace_malloc.c:762)
             at allocate_dtv
             at _dl_allocate_tls
             at pthread_create@@GLIBC_2.2.5
             at std::sys::unix::thread::Thread::new (thread.rs:67)
             at std::thread::Builder::spawn_unchecked (mod.rs:489)
             at std::thread::Builder::spawn (mod.rs:382)
             at futures_timer::global::HelperThread::new (global.rs:26)
             at <futures_timer::timer::TimerHandle as core::default::Default>::default (timer.rs:281)
             at futures_timer::delay::Delay::new (delay.rs:39)
             at async_std::io::timeout::timeout::{{closure}} (timeout.rs:40)
             at <std::future::GenFuture<T> as core::future::future::Future>::poll::{{closure}} (future.rs:43)
     Summary Leaked 312 B total

A very similar leak stack trace can be obtained from the following test:

#[test]
fn just_sync_sleep() {
    std::thread::spawn(move || {
        std::thread::sleep(std::time::Duration::from_millis(1));
    });
}

Which is fixed by joining all threads before exiting:

#[test]
fn just_sync_sleep() {
    let t = std::thread::spawn(move || {
        std::thread::sleep(std::time::Duration::from_millis(1));
    });
    t.join()
}

This leads me to suspect the #[async_std::main] and #[async_std::test] macros don't properly join all threads before exiting.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions