-
Notifications
You must be signed in to change notification settings - Fork 1.1k
mi_heap_destroy fails in multithreading case (in Debug builds) #1250
Copy link
Copy link
Open
Description
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.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels