5

We have an internal memory manager that we use with one of our products. The memory manager overrides the new and delete operators, and works fine in single-threaded appliations. However, I'm now tasked to make it work with multi-threaded applications too. To my understanding the following pseudo code should work, but it hangs on a spin, even with try_lock(). Any ideas?

Update #1

Causes "Access Violation":

#include <mutex>

std::mutex g_mutex;

/*!
\brief Overrides the Standard C++ new operator
\param size [in] Number of bytes to allocate
*/
void *operator new(size_t size)
{
   g_mutex.lock(); // Access violation exception
   ...   
}

Causes thread to hang forever in a spin:

#include <mutex>

std::mutex g_mutex;
bool g_systemInitiated = false;


/*!
\brief Overrides the Standard C++ new operator
\param size [in] Number of bytes to allocate
*/
void *operator new(size_t size)
{
   if (g_systemInitiated == false) return malloc(size);
   g_mutex.lock(); // Thread hangs forever here. g_mutex.try_lock() also hangs
   ...   
}

int main(int argc, const char* argv[])
{
   // Tell the new() operator that the system has initiated
   g_systemInitiated = true;
   ...
}

Update #2

A recursive mutex also causes the thread to hang forever in a spin:

#include <mutex>

std::recursive_mutex g_mutex;
bool g_systemInitiated = false;


/*!
\brief Overrides the Standard C++ new operator
\param size [in] Number of bytes to allocate
*/
void *operator new(size_t size)
{
   if (g_systemInitiated == false) return malloc(size);
   g_mutex.lock(); // Thread hangs forever here. g_mutex.try_lock() also hangs
   ...   
}

int main(int argc, const char* argv[])
{
   // Tell the new() operator that the system has initiated
   g_systemInitiated = true;
   ...
}

Update #3

Jonathan Wakely suggested I should try unique_lock and/or lock_guard, but the lock still hangs in a spin.

unique_lock test:

#include <mutex>

std::mutex g_mutex;
std::unique_lock<std::mutex> g_lock1(g_mutex, std::defer_lock);
bool g_systemInitiated = false;

/*!
\brief Overrides the Standard C++ new operator
\param size [in] Number of bytes to allocate
*/
void *operator new(size_t size)
{
   if (g_systemInitiated == false) return malloc(size);
   g_lock1.lock(); // Thread hangs forever here the first time it is called
   ...   
}

int main(int argc, const char* argv[])
{
   // Tell the new() operator that the system has initiated
   g_systemInitiated = true;
   ...
}

lock_guard test:

#include <mutex>

std::recursive_mutex g_mutex;
bool g_systemInitiated = false;


/*!
\brief Overrides the Standard C++ new operator
\param size [in] Number of bytes to allocate
*/
void *operator new(size_t size)
{
   if (g_systemInitiated == false) return malloc(size);
   std::lock_guard<std::mutex> g_lock_guard1(g_mutex); // Thread hangs forever here the first time it is called
   ...   
}

int main(int argc, const char* argv[])
{
   // Tell the new() operator that the system has initiated
   g_systemInitiated = true;
   ...
}

I think my problem is that delete is called by the C++ 11 mutex library when locking. delete is also overridden like so:

/*!
\brief Overrides the Standard C++ new operator
\param p [in] The pointer to memory to free
*/
void operator delete(void *p)
{
    if (g_systemInitiated == false)
    {
       free(p); 
    }
    else
    {
       std::lock_guard<std::mutex> g_lock_guard1(g_mutex);
       ...
    }
}

This causes deadlocking situation that I can't see any good solution to except for making my own locking that does not spawn any calls to new or delete while locking or unlocking.

Update #4

I've implemented my own custom recursive mutex that have no calls to new or delete, also, it allows the same thread to enter a locked block.

#include <thread>

std::thread::id g_lockedByThread;
bool g_isLocked = false;
bool g_systemInitiated = false;

/*!
\brief Overrides the Standard C++ new operator
\param size [in] Number of bytes to allocate
*/
void *operator new(size_t size)
{
   if (g_systemInitiated == false) return malloc(size);

   while (g_isLocked && g_lockedByThread != std::this_thread::get_id());
   g_isLocked = true; // Atomic operation
   g_lockedByThread = std::this_thread::get_id();
   ...   
   g_isLocked = false;
}

/*!
\brief Overrides the Standard C++ new operator
\param p [in] The pointer to memory to free
*/
void operator delete(void *p)
{
    if (g_systemInitiated == false)
    {
       free(p); 
    }
    else
    {
       while (g_isLocked && g_lockedByThread != std::this_thread::get_id());
       g_isLocked = true; // Atomic operation
       g_lockedByThread = std::this_thread::get_id();
       ...   
       g_isLocked = false;
    }
}

int main(int argc, const char* argv[])
{
   // Tell the new() operator that the system has initiated
   g_systemInitiated = true;
   ...
}

Update #5

Tried Jonathan Wakely suggestion, and found that it definitely seems that there is something wrong with Microsoft's implementation of C++ 11 Mutexes; his example hangs if compiled with the /MTd (Multi-threaded Debug) compiler flag, but works fine if compiled with the /MDd (Multi-threaded Debug DLL) compiler flag. As Jonathan rightly pointed out std::mutex implementations are supposed to be constexpr's. Here is the VS 2012 C++ code I used to test the implementation issue:

#include "stdafx.h"

#include <mutex>
#include <iostream>

bool g_systemInitiated = false;
std::mutex g_mutex;

void *operator new(size_t size)
{
    if (g_systemInitiated == false) return malloc(size);
    std::lock_guard<std::mutex> lock(g_mutex);
    std::cout << "Inside new() critical section" << std::endl;
    // <-- Memory manager would be called here, dummy call to malloc() in stead
    return malloc(size);
}

void operator delete(void *p)
{
    if (g_systemInitiated == false) free(p); 
    else
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        std::cout << "Inside delete() critical section" << std::endl;
        // <-- Memory manager would be called here, dummy call to free() in stead
        free(p);
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    g_systemInitiated = true;

    char *test = new char[100];
    std::cout << "Allocated" << std::endl;
    delete test;
    std::cout << "Deleted" << std::endl;

    return 0;
}

Update #6

Submitted a bug report to Microsoft: https://connect.microsoft.com/VisualStudio/feedback/details/776596/std-mutex-not-a-constexpr-with-mtd-compiler-flag#details

  • 1
    I don't see how that can link, where do you define your g_mutex? there are two declarations, you should have extern infront of one of them. – AndersK Jan 14 '13 at 14:25
  • What do you your new and delete operators actually do (aside from allocating/freeing memory)? – Peter Ruderman Jan 14 '13 at 14:37
  • @claptrap It's pseudo code. –  Jan 14 '13 at 15:11
  • @PeterRuderman They pass the requests to our custom memory manager. http://en.wikipedia.org/wiki/Memory_management –  Jan 14 '13 at 15:12
  • 1
    I guess what I really wanted to know was is this completely custom heap or just a decorator around CRT that adds some tracking logic or some such. I guess it's the former. In this case, throwing a lock around new and delete is way too coarse grained and will likely kill performance. Why do you need a custom memory manager? If it is not for performance reasons, then you should consider just using the standard new and delete (or a thin wrapper around malloc/free). If it is for performance reasons, then making it thread safe is a complex problem. – Peter Ruderman Jan 14 '13 at 15:19
  • @PeterRuderman It's a complete managed heap for performance reasons. –  Jan 14 '13 at 17:17
  • Why oh why are you calling `g_mutex.lock()` instead of using `unique_lock` or `lock_guard`? Which compiler & std::lib are you using, on what platform? – Jonathan Wakely Jan 15 '13 at 00:40
  • @IngeHenriksen you should post code that can be compiled or state that it is pseudo code. – AndersK Jan 15 '13 at 10:41
  • @claptrap I don't want to argue, but I did state that it was pseudo code, you probably just missed it. –  Jan 15 '13 at 11:35
  • @JonathanWakely I've tried that now (see the update) but it still hangs. IDE is Visual Studio 2012 V11.0.51106.01 Update 1 using the built-in Microsoft C++ compiler. –  Jan 15 '13 at 12:23
  • I didn't say the lock types would help, but explicit ock/unlock calls are very bad practice. The first version should work, but should use `std::lock_guard`. The second version has a data race. Update #2 has a data race. Update #3 is utterly broken: do not use a global `unique_lock` object, it should be local to the scope where you want the lock; the lock_guard test has a data race. Update #4 has data races, you shouldn't be trying to implement your own recursive mutex if you can't use `unique_lock` correctly, sorry. – Jonathan Wakely Jan 15 '13 at 13:34
  • Do you ever actually unlock your mutexes?! – Jonathan Wakely Jan 15 '13 at 13:42
  • @JonathanWakely Yes, I unlock them always. I will have a look at your other suggestions. Thank you. –  Jan 15 '13 at 13:46
  • 1
    I hope you understand that this line `if (g_systemInitiated == false) return malloc(size);` is a big, big problem. –  Jan 15 '13 at 14:01
  • @VladLazarenko It's difficult to know what you are mean since you are so cryptic. However, if you mean that `malloc` does not copy & initialize the object (call its constructor etc) then I can tell you that it does - the compiler handles all of that since it is inside an overridden `new`. If it's the global `g_systemInitiated` that has you worried I can assure you that this is not my production code and is just meant as a simple pseudo code example to illustrate the problem at hand. –  Jan 17 '13 at 14:30
  • @Inge, sorry for not clarifying. I meant that a snippet is not thread safe. In concurrent environment you may have very hard to debug problems. Read on double check lock, that is a common solution, if my memory serves me well still... –  Jan 17 '13 at 14:34
  • @VladLazarenko I've added `volatile` to `g_systemInitiated`, that should hold it for now. Thanks! –  Jan 17 '13 at 15:48

2 Answers2

1

The mutex library uses new, and std::mutex is not recursive (i.e. reentrant) by default. A chicken-and-egg problem.

UPDATE As has been pointed out in the comments below, using std::recursive_mutex may work. But the classic C++ problem of the order of static initialization of globals being not well defined remains, as does the danger of outside access to the global mutex (best to put it inside an anonymous namespace.)

UPDATE 2 You may be switching g_systemInitiated to true too early, i.e. before the mutex has had a chance to complete its initialization, and so the "first-time-through" call to malloc() never happens. To force this, you could try replacing the assignment in main() with a call to an initialization function in the allocator module:

namespace {
    std::recursive_mutex    g_mutex;
    bool                    g_initialized = false;
}
void initialize()
{
    g_mutex.lock();
    g_initialized = true;
    g_mutex.unlock();
}
arayq2
  • 2,502
  • 17
  • 21
  • Ok. So, is there a way to circumvent this? A way to lock an overridden `new` operator? –  Jan 14 '13 at 14:10
  • Not that I know of for std::mutex. But with the lower-level pthreads library, you can (statically) initialize a global pthreads_mutex_t with PTHREAD_RECURSIVE_MUTEX_INITIALIZER, which may work around the problem. But you would lose the convenience of coding with the std::thread API, of course. – arayq2 Jan 14 '13 at 14:21
  • well he can use std::recursive_mutex instead – AndersK Jan 14 '13 at 14:28
  • Good point, forgot about that. It's worth a shot. But in general, the "need" for a recursive mutex is usually indicative of poor or even bad design to begin with (which is why I forgot about it!). – arayq2 Jan 14 '13 at 14:35
  • Also, see: [Recursive Lock (Mutex) vs Non-Recursive Lock (Mutex)](http://stackoverflow.com/questions/187761/recursive-lock-mutex-vs-non-recursive-lock-mutex). – arayq2 Jan 14 '13 at 14:42
  • @claptrap Tried std::recursive_mutex now but it still hangs in concrt.h's _SpinOnce() method after the first lock. –  Jan 14 '13 at 15:04
  • 4
    Where does `std::mutex` use `new`? It's required to have a `constexpr` constructor, which rules out `new` at initialization. `std::mutex` has a `constexpr` constructor specifically so you don't have to worry about order of static init. – Jonathan Wakely Jan 15 '13 at 00:38
  • @JonathanWakely You are right about `std::mutex` being an `constexpr`, but calls _are_ made to `delete` which is the source of the "chicken & the egg" mess I'm in. –  Jan 15 '13 at 12:26
  • 1
    `delete` in `std::mutex`? Then the implementation is broken. – Jonathan Wakely Jan 15 '13 at 13:24
  • 1
    @JonathanWakely in `std::mutex's lock()` to be precisise. `Concurrency::details::QuickBitSet::Grow(unsigned int size)` Line 1921 in `Concurency.h` calls `delete` according to my stack trace, this is after I call `lock()`. –  Jan 15 '13 at 13:53
1

All of your examples are broken, except the first one, which is just very bad practice for not using a scoped lock type.

This should work, if it doesn't your compiler or standard library is broken:

#include <mutex>

std::mutex g_mutex;

void *operator new(size_t size)
{
   std::lock_guard<std::mutex> lock(g_mutex);
   ...   
}
Jonathan Wakely
  • 166,810
  • 27
  • 341
  • 521
  • 1
    It definitely seems that there is something wrong with Microsoft's implementation of C++ 11 Mutexes; your example hangs id I compile with the `/MTd` (Multi-threaded Debug) compiler flag, but works fine if I compile with the `/MDd` (Multi-threaded Debug DLL) compiler flag. I will add an update to my question. –  Jan 15 '13 at 14:25
  • Could you explain why you say that the standard library is broken if this does not work? Does the standard mandate that the implementation of std::mutex must not depend on `new`? – Curious Sep 08 '18 at 18:22
  • Default initialization of a mutex can't use new because it's a constexpr function and locking a mutex can't use new because it's noexcept – Jonathan Wakely Sep 08 '18 at 20:56
  • Hmm, the standard does not say the lock() method is noexcept in [thread.mutex.class]. Where do you see that lock() is noexcept? – Curious Sep 08 '18 at 21:57
  • You're right, it's not noexcept, but it can only throw `system_error` due to missing privileges or deadlock being detected, but not for memory allocation failure. – Jonathan Wakely Sep 10 '18 at 09:47