Skip to content

mi_heap_destroy fails in multithreading case (in Debug builds) #1250

@eao197

Description

@eao197

I have a case when thread A creates a new mimalloc heap (via mi_heap_new) then this heap is passed to thread B that uses this heap.
After thread B completes its work thread A destroys this heap (via mi_heap_destroy).

Then thread A creates another new mimalloc heap (via mi_heap_new) and passes it to the thread B again. Thread B uses this heap.
After thread B completes its work thread A tries to destroy this heap (via mi_heap_destroy), but this attempt crashes the application.

In Debug mode this looks like:

$ ./mimalloc_case 
allocations/deallocations completed
iteration completed
=== before mi_heap_destroy ===
=== after mi_heap_destroy ===
allocations/deallocations completed
iteration completed
=== before mi_heap_destroy ===
mimalloc: assertion failed: at "/home/eao197/sandboxes/vcpkg_stable/buildtrees/mimalloc/src/v3.2.8-9eae4fa562.clean/include/mimalloc/prim.h":520, _mi_heap_theap_peek
  assertion: "theap==NULL || theap->heap==heap"
Aborted (core dumped)

There is the code that reproduces such a case:

#include <mimalloc.h>

#include <memory_resource>
#include <stdexcept>
#include <iostream>
#include <future>
#include <mutex>
#include <condition_variable>
#include <thread>

namespace mimalloc_case
{

class memory_pool_t final : public std::pmr::memory_resource
{
    /// Heap to be used.
    mi_heap_t * _heap;

public:
    /// Main constructor.
    ///
    /// Creates a new mimalloc heap.
    ///
    /// Throws an exception on failure.
    memory_pool_t();
    ~memory_pool_t();

    memory_pool_t(const memory_pool_t &) = delete;
    memory_pool_t &
    operator=(const memory_pool_t &) = delete;

    memory_pool_t(memory_pool_t &&) = delete;
    memory_pool_t &
    operator=(memory_pool_t &&) = delete;

protected:
    void *
    do_allocate(std::size_t bytes, std::size_t alignment) override;

    void
    do_deallocate(void * p, std::size_t bytes, std::size_t alignment) override;

    bool
    do_is_equal(const std::pmr::memory_resource & other) const noexcept override;
};

memory_pool_t::memory_pool_t()
    : _heap{ mi_heap_new() }
{
    if(!_heap)
        throw std::runtime_error{ "mi_heap_new() returns NULL" };
}

memory_pool_t::~memory_pool_t()
{
    std::cout << "=== before mi_heap_destroy ===" << std::endl;
    mi_heap_destroy(_heap);
    std::cout << "=== after mi_heap_destroy ===" << std::endl;
}

void *
memory_pool_t::do_allocate(std::size_t bytes, std::size_t alignment)
{
    void * r = mi_heap_malloc_aligned(_heap, bytes, alignment);
    if(!r)
        throw std::bad_alloc();
    return r;
}

void
memory_pool_t::do_deallocate(
    void * p,
    std::size_t /*bytes*/,
    std::size_t /*alignment*/)
{
    mi_free(p);
}

bool
memory_pool_t::do_is_equal(
    const std::pmr::memory_resource & /*other*/) const noexcept
{
    return false;
}

} /* namespace mimalloc_case */

int main()
{
    using namespace mimalloc_case;

    // Variables for interaction with child thread.
    std::promise<void> * completion_promise = nullptr;
    memory_pool_t * pool_to_use = nullptr;
    bool should_complete = false;

    // Variables for thread safety.
    std::mutex data_lock;
    std::condition_variable data_ready_cv;

    // The child thread.
    std::thread child_thread{
        [&]() {
            // Local copies of data to be used.
            memory_pool_t * pool = nullptr;
            std::promise<void> * promise = nullptr;

            // The main loop of child thread.
            for(;;)
            {
                // Acquisition of data to be used.
                {
                    std::unique_lock lock{ data_lock };

                    if(!should_complete && !pool_to_use)
                        // Have to wait until data will be provided.
                        data_ready_cv.wait(lock,
                                [&pool_to_use, &should_complete]() {
                                    return nullptr != pool_to_use ||
                                            should_complete;
                                });

                    if(should_complete)
                        // Thread has to be finished.
                        break;

                    // We have data for another loop iteration.
                    pool = pool_to_use;
                    promise = completion_promise;

                    // Shared values have to be dropped to avoid fast start
                    // of the next loop iteration.
                    pool_to_use = nullptr;
                    completion_promise = nullptr;
                }

                // Allocations/deallocations.
                for(int i = 0; i < 1000; ++i)
                {
                    pool->deallocate(pool->allocate(16, 8), 16, 8);
                }

                std::cout << "allocations/deallocations completed" << std::endl;

                // Inform the main thread that another loop iteration completed.
                promise->set_value();
            }
        }
    };

    // Local function for doing a loop iteration in the child thread.
    const auto iteration = [&]() {
        memory_pool_t pool;
        std::promise<void> completion;

        // Sending the data to the child thread.
        {
            std::lock_guard lock{ data_lock };
            completion_promise = &completion;
            pool_to_use = &pool;

            data_ready_cv.notify_one();
        }

        // Wait for the completion signal.
        completion.get_future().get();

        std::cout << "iteration completed" << std::endl;
    };

    // The first iteration.
    iteration();

#if 1
    // The second iteration.
    iteration();
#endif

    // The child thread has to be stopped.
    {
        std::lock_guard lock{ data_lock };
        should_complete = true;
        data_ready_cv.notify_one();
    }

    child_thread.join();
}

// vim:expandtab:ts=4:sw=4:sts=4:

If the second iteration is turned off (by changing #if 1 to #if 0) everything is fine.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions