Article View: comp.sys.mac.programmer.codewarrior
Article #660The world's simplest C++ thread safe memory debugger
From: Howard Hinnant
Date: Tue, 30 Sep 2003 01:25
Date: Tue, 30 Sep 2003 01:25
436 lines
15462 bytes
15462 bytes
Last week while describing a very simple memory debugger I wrote:
> It is not thread safe. But it is simple enough it easily could be made
> so. Hmm... perhaps I'll write about the new Metrowerks::threads next.
With the gauntlet thrown, and the overwhelming reader response just
daring me to try and make this puppy thread safe, I just couldn't resist
the challenge. :-) (is that crickets I hear chirping? oh never mind)
By way of review, I last wrote about simple_mem_debug which had an
interface like:
struct mem_info
{
std::size_t size_;
bool array_;
};
typedef Metrowerks::hash_map<void*, mem_info, Metrowerks::hash<void*>,
std::equal_to<void*>,
mallocAllocator<std::pair<void*const, mem_info> > >
MemMap;
MemMap& get_memmap();
That is, this "memory debugger" is nothing more than a hash_map mapping
void* to the mem_info struct, and a singleton-like function "get_memmap"
that gets the one true map so that you can inspect it. Operators new
and delete have been overloaded to fill out the hash_map with the
appropriate information. Very simple, and yet very useful.
To make this little tool thread safe I will draw on the
Metrowerks::threads package which first ships with CWDS / Mac version 9.
This library has mutex objects, lock objects, condition variables and
threads. Its interface is based largely on the boost::threads library:
http://www.boost.org/libs/thread/doc/index.html
The Metrowerks version will run under Mac PEF or Mach-O and even a
single threaded mode. It is sensitive to the MSL C flag:
_MSL_THREADSAFE . You simply need to include <msl_thread> to access
this part of MSL C++ (which lives under namespace Metrowerks).
The basic idea for adding thread safety to simple_mem_debug is to have
get_memmap() return a smart pointer instead of a raw reference. And
this smart pointer would not only grant access to the MemMap, but it
will also lock a mutex for accessing the MemMap for the duration of the
smart pointer's lifetime. Only when your copy of the smart pointer
destructs do you release the mutex and allow other threads access.
MemPtr get_memmap();
This will require client code to change slightly. For example what was
once:
const MemMap& mem_debug = get_memmap();
MemMap::const_iterator i = mem_debug.find(p);
Will now become:
MemPtr mem_debug = get_memmap();
MemMap::const_iterator i = mem_debug->find(p);
But that is the extent of the changes to client code. Clients will not
have to deal with mutexes or locks explicitly. They are hidden away
inside of MemPtr.
So what does MemPtr look like?
I chose to once again call on Metrowerks::alloc_ptr (<msl_utility>) for
this job. You may recall that this is an auto_ptr-like smart pointer
that takes an allocator for a deletion policy. We can create an
"allocator" equipped with the proper thread safety tools that would
unlock the mutex when the pointer is "disposed of". In reality, the
pointer will not be heap based and will not need disposing of at all.
So all this "allocator" has to to do is unlock the mutex.
Below is the full simple_mem_debug.h header, below which is a little
more detail:
// simple_mem_debug.h
#ifndef SIMPLE_MEM_DEBUG_H
#define SIMPLE_MEM_DEBUG_H
#include "mallocAllocator.h"
#include <hash_map>
#include <cstddef>
#include <msl_utility>
#include <msl_thread>
struct mem_info
{
std::size_t size_;
bool array_;
};
typedef Metrowerks::hash_map<void*, mem_info, Metrowerks::hash<void*>,
std::equal_to<void*>,
mallocAllocator<std::pair<void*const, mem_info> > >
MemMap;
template <class T, class Mutex, class Lock>
class unlocker
{
public:
typedef std::size_t size_type;
typedef std::ptrdiff_t difference_type;
typedef T* pointer;
typedef const T* const_pointer;
typedef T& reference;
typedef const T& const_reference;
template <class U> struct rebind
{ typedef unlocker<U, Mutex, Lock> other; };
explicit unlocker(Mutex& m) : mut_(m), lock_(m) {}
unlocker(const unlocker& u)
: mut_(u.mut_), lock_(u.mut_)
{}
template <class U> unlocker(const unlocker<U, Mutex, Lock>& u)
: mut_(u.mut_), lock_(u.mut_)
{}
void deallocate(pointer, size_type) {lock_.unlock();}
private:
Mutex& mut_;
Lock lock_;
template <class U, class M, class L> friend class unlocker;
};
typedef Metrowerks::alloc_ptr
<
const MemMap,
unlocker
<
const MemMap,
Metrowerks::recursive_mutex,
Metrowerks::recursive_mutex::scoped_lock
>
> MemPtr;
MemPtr get_memmap();
#endif // SIMPLE_MEM_DEBUG_H
The new "unlocker" class template is the "allocator" (that really isn't
an allocator) which will serve to unlock the mutex when we need it to.
Btw, I would like to thank Daniel Wallin from over at boost for helping
me design unlocker.
unlocker has the obligatory allocator-like typedef's which alloc_ptr
actually does pay attention to, and use. unlocker also holds a
reference to a Mutex, and a Lock by value. These are both made template
parameters just in the interest of reuse and easy maintenance. For this
application, they could be hardwired to Metrowerks::recursive_mutex and
Metrowerks::recursive_mutex::scoped_lock respectively.
The unlocker constructor takes only a Mutex, and initializes both the
internal Mutex& and the Lock with this value. After running this
constructor, the unlocker "owns" the mutex because the Lock locks the
mutex upon the Lock's construction.
The copy constructor (and the templated variant) deserves a close look
despite the simple appearance. They initialize both members from the
source's Mutex (the source's Lock is not accessed). This requires that
Mutex be a recursive mutex, as after the copy, both unlocker objects
will own the mutex. For this application, both unlocker objects should
always be in the same thread, thus the need for a recursive mutex.
The implicit assignment operator of unlocker is not valid, because of
the Mutex& member.
The implicit destructor of unlocker does exactly what we need it to: it
destructs Lock which will unlock the Mutex if it has not already been
unlocked.
This leaves the deallocate member. It ignores the pointer and size_type
passed to it and simply calls the unlock member function on the Lock.
Thus when the alloc_ptr deallocates its pointer, this is all that will
happen.
MemPtr becomes a typedef to alloc_ptr outfitted with an unlocker using a
Metrowerks::recursive_mutex and a
Metrowerks::recursive_mutex::scoped_lock. MemPtr also sports a const
MemMap so that clients can not accidently invoke non-const operations on
the hash_map.
simple_mem_debug.cpp
--------------------
The source to simple_mem_debug has several jobs:
Obviously:
1. Implement MemPtr get_memmap().
2. Override operators new and delete to fill in the MemMap.
And less obviously:
3. Provide a private non-const interface to the MemMap for
the use of the operators new and delete.
4. Synchronize the construction of any static objects used
in the implementation.
Here is the complete source, with detailed comments following:
// simple_mem_debug.cpp
#include "simple_mem_debug.h"
#include <cassert>
#include <new>
#include <msl_utility>
#include <msl_thread>
typedef Metrowerks::alloc_ptr
<
MemMap,
unlocker
<
MemMap,
Metrowerks::recursive_mutex,
Metrowerks::recursive_mutex::scoped_lock
>
> VMemPtr;
void check_mem_leak()
{
assert(get_memmap()->empty());
}
VMemPtr get_memmap_imp()
{
typedef Metrowerks::recursive_mutex mutex;
typedef mutex::scoped_lock scoped_lock;
static mutex mut;
static MemMap memmap;
static bool init = false;
if (!init)
{
std::atexit(check_mem_leak);
init = true;
}
return VMemPtr(&memmap, unlocker<MemMap, mutex, scoped_lock>(mut));
}
void init_get_memmap()
{
get_memmap_imp();
}
Metrowerks::once_flag simple_mem_init_flag = _MSL_THREAD_ONCE_INIT;
MemPtr get_memmap()
{
Metrowerks::call_once(init_get_memmap, simple_mem_init_flag);
return get_memmap_imp();
}
VMemPtr get_vmemmap()
{
Metrowerks::call_once(init_get_memmap, simple_mem_init_flag);
return get_memmap_imp();
}
void* do_new(std::size_t size, bool is_array) throw(std::bad_alloc)
{
if (size == 0)
size = 1;
Metrowerks::alloc_ptr<char, mallocAllocator<char> > ptr;
while (true)
{
ptr.reset((char*)std::malloc(size));
if (ptr.get() != 0)
break;
std::new_handler p = std::set_new_handler(0);
std::set_new_handler(p);
if (!p)
throw std::bad_alloc();
p();
}
VMemPtr map = get_vmemmap();
mem_info& m = (*map)[ptr.get()];
m.size_ = size; //std::__msize(ptr.get());
m.array_ = is_array;
return ptr.release();
}
void* operator new (std::size_t size) throw(std::bad_alloc)
{
return do_new(size, false);
}
void* operator new (std::size_t size, const std::nothrow_t&) throw()
{
try
{
return operator new(size);
}
catch(...)
{
}
return 0;
}
void do_delete(void* ptr, bool is_array) throw()
{
if (ptr)
{
VMemPtr m = get_vmemmap();
MemMap::iterator i = m->find(ptr);
assert(i != m->end());
assert(i->second.array_ == is_array);
m->erase(i);
std::free(ptr);
}
}
void operator delete (void* ptr) throw()
{
do_delete(ptr, false);
}
void operator delete (void* ptr, const std::nothrow_t&) throw()
{
operator delete(ptr);
}
void* operator new [](std::size_t size) throw(std::bad_alloc)
{
return do_new(size, true);
}
void* operator new [](std::size_t size, const std::nothrow_t&) throw()
{
try
{
return operator new[](size);
}
catch(...)
{
}
return 0;
}
void operator delete[](void* ptr) throw()
{
do_delete(ptr, true);
}
void operator delete[](void* ptr, const std::nothrow_t&) throw()
{
operator delete[](ptr);
}
There are two interfaces in this source to the MemMap: const and
non-const. These are represented by:
MemPtr
VMemPtr
(V for non-const ... I just picked a letter, I don't know... ;-) )
The VMemPtr typedef expresses the type for the non-const smart pointer
to the map. The alloc_ptr machinery will allow conversions from the
non-const variant to the const version (but not vice-versa).
check_mem_leak() is unchanged from the thread unsafe version last week.
The meat and potatoes of get_memmap() has been moved into
get_memmap_imp() in order to synchronize the construction of the local
static objects that lie within. For the moment, ignore the
synchronization problem, and just look at get_memmap_imp().
The real addition to this function to make it pass back a thread safe
smart pointer instead of a raw reference is the addition of a static
Metrowerks::recursive_mutex object, and the construction of the smart
pointer to return. Note that it is creating the non-const version of
the smart pointer (VMemPtr), which is initialized with the static mutex
(not to mention the pointer to the hash_map, and a temporary unlocker
object). The init flag and atexit action was explained last week, so I
won't repeat it here.
Next you see:
void init_get_memmap();
which does nothing but call get_memmap_imp() while ignoring
get_memmap_imp's return. Ok, before you send me to the funny farm, just
hear me out for a couple more minutes (longer if you read slowly :-) ).
We really don't want two threads to call get_memmap_imp() simultaneously
during its first execution. Although two threads can call
get_memmap_imp() after its first execution with no problem. The issue
is the construction of the local statics within get_memmap_imp(). The
compiler does not insure that only one thread will construct these
statics. So we must do so. And init_get_memmap() is a part of that
solution.
It is now time to take a good look at get_memmap(). This function uses
Metrowerks::call_once to invoke init_get_memmap(), and then simply
returns get_memmap_imp(). Metrowerks::call_once is part of the
Metrowerks::threads package. It will insure that init_get_memmap() is
called exactly once in a multithreaded environment. And call_once takes
a signature of the form void(*)(). Thus init_get_memmap(). So the
call_once assures that the first thread in calls get_memmap_imp()
*uninterrupted*. This first call's purpose is just to construct the
statics, and the results of the function are otherwise just thrown away.
Then get_memmap_imp() is called again with the smart pointer returned.
Note that the second call to get_memmap() will only call
get_memmap_imp() once -- Metrowerks::call_once will see to that. And
finally note that get_memmap() subtly changes the smart pointer return
type from the non-const version to the const version.
There is a get_vmemmap() which is a simple knock off of get_memmap() so
that operators new and delete can get non-const access to the hash_map.
The rest of simple_mem_debug.cpp follows nicely from the original
(non-thread-safe) description. The only differences are that do_new and
do_delete now work with a (smart) pointer instead of a reference. Note
that this is also the only difference that external clients will notice.
Finally note that this all works just fine in single thread mode. The
mutexes "lock" and "unlock" just as you would expect, and call_once,
still just calls once.
Don't let the length of this post dissuade you from simple_mem_debug.
The description is actually much longer than the code itself! :-)
-Howard
The MSL C++ guy
Metrowerks
Technical Details
Message-ID:
<hinnant-CF8AA0.21254129092003@syrcnyrdrs-02-ge0.nyroc.rr.com>Path:
arc1-ca.pugleaf.net!news1.google.com!newsfeed.stanford.edu!cyclone.bc.net!sjc70.webusenet.com!news.webusenet.com!elnk-nf2-pas!elnk-pas-nf1!newsfeed.earthlink.net!newsfeed2.easynews.com!newsfeed1.easynews.com!easynews.com!easynews!news-west.rr.com!news.rr.com!news-rtr.nyroc.rr.com!news-out.nyroc.rr.com!twister.nyroc.rr.com.POSTED!53ab2750!not-for-mailShow Full Headers
Complete Article Headers
X-Google-Language: ENGLISH,ASCII-7-bit X-Google-Thread: 10bcce,688428e11d0b0079,start X-Google-Attributes: gid10bcce,public X-Google-Thread: f635e,688428e11d0b0079,start X-Google-Attributes: gidf635e,public X-Google-ArrivalTime: 2003-09-29 18:26:16 PST Path: arc1-ca.pugleaf.net!news1.google.com!newsfeed.stanford.edu!cyclone.bc.net!sjc70.webusenet.com!news.webusenet.com!elnk-nf2-pas!elnk-pas-nf1!newsfeed.earthlink.net!newsfeed2.easynews.com!newsfeed1.easynews.com!easynews.com!easynews!news-west.rr.com!news.rr.com!news-rtr.nyroc.rr.com!news-out.nyroc.rr.com!twister.nyroc.rr.com.POSTED!53ab2750!not-for-mail From: Howard Hinnant <hinnant@metrowerks.com> Newsgroups: comp.sys.mac.programmer.codewarrior,codewarrior.mac Subject: The world's simplest C++ thread safe memory debugger Organization: Metrowerks User-Agent: MT-NewsWatcher/3.3b1 (PPC Mac OS X) Message-ID: <hinnant-CF8AA0.21254129092003@syrcnyrdrs-02-ge0.nyroc.rr.com> Lines: 436 Date: Tue, 30 Sep 2003 01:25:41 GMT NNTP-Posting-Host: 24.58.14.109 X-Complaints-To: abuse@rr.com X-Trace: twister.nyroc.rr.com 1064885141 24.58.14.109 (Mon, 29 Sep 2003 21:25:41 EDT) NNTP-Posting-Date: Mon, 29 Sep 2003 21:25:41 EDT