Skip to content

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

Open
@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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions