Thread View: comp.sys.mac.programmer.codewarrior
5 messages
5 total messages
Started by Howard Hinnant
Tue, 30 Sep 2003 01:25
The world's simplest C++ thread safe memory debugger
Author: 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
Re: The world's simplest C++ thread safe memory debugger
Author: Steven Fisher
Date: Thu, 02 Oct 2003 16:19
Date: Thu, 02 Oct 2003 16:19
12 lines
2011 bytes
2011 bytes
Howard Hinnant wrote: > 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) I'm reading and learning, I just don't have any comments right now. It's stuff I'm going to need one day, I can already tell, but nothing I can use yet. So by all means, keep writing things! :) Only vaguely releated: Have you considered starting a web page like Ben's palmoswerks to organize these tips in?
Re: The world's simplest C++ thread safe memory debugger
Author: Howard Hinnant
Date: Thu, 02 Oct 2003 17:19
Date: Thu, 02 Oct 2003 17:19
48 lines
3309 bytes
3309 bytes
In article <teYeb.5819$6C4.4794@pd7tw1no>, Steven Fisher <sdfisher@spamcop.net> wrote: > Howard Hinnant wrote: > > > 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) > > I'm reading and learning, I just don't have any comments right now. It's > stuff I'm going to need one day, I can already tell, but nothing I can > use yet. So by all means, keep writing things! :) Thanks Steven. > Only vaguely releated: Have you considered starting a web page like > Ben's palmoswerks to organize these tips in? I've thought about it, but haven't done it yet. I do have some older tips at: http://home.twcny.rr.com/hinnant/tip_archive/ Unfortunately some of these tips date back several years and have become obsolete. However others are still just as valid today. I need to sort out which is which and throw out the old. For example tip #2 on this site is "On the Refcounting of string". But the Pro 9 string is no longer refcounted. And the Pro 9 string is available to Pro 8 customers by looking in the Thrill Seekers folder on the reference CD. The whole point of that tip was that you could choose whether or not you wanted a refcounted string with #define _MSL_NO_REFCOUNT_STRING. But today _MSL_NO_REFCOUNT_STRING no longer has any effect. Tip #12 "vector - Pro 6" starts out with announcing a bug and how to fix it. Needless to say, that bugfix has long since shipped! :-) And indeed, it is high time for a completely new tip on vector since the Pro 9 vector has a completely revamped and turbo charged engine under the hood (just try inserting into a vector<vector<string> > and see if you notice anything really really fast ;-) ). Thanks for the gentle tap in the backside. I'll try to update this site, and maybe even move it onto the official Metrowerks website. -Howard The MSL C++ guy Metrowerks
Re: The world's simplest C++ thread safe memory debugger
Author: Sean McBride
Date: Wed, 08 Oct 2003 00:34
Date: Wed, 08 Oct 2003 00:34
13 lines
1560 bytes
1560 bytes
In article <hinnant-CF8AA0.21254129092003@syrcnyrdrs-02-ge0.nyroc.rr.com>, Howard Hinnant <hinnant@metrowerks.com> 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) Any plans to replace the (non-thread safe) DebugNew with your new memory debugger? Or merge the two and include them with CW?
Re: The world's simplest C++ thread safe memory debugger
Author: Howard Hinnant
Date: Wed, 08 Oct 2003 16:52
Date: Wed, 08 Oct 2003 16:52
32 lines
2696 bytes
2696 bytes
In article <cwatson-0102BE.20340207102003@aeinews.aei.ca>, Sean McBride <cwatson@cam.org> wrote: > In article > <hinnant-CF8AA0.21254129092003@syrcnyrdrs-02-ge0.nyroc.rr.com>, > Howard Hinnant <hinnant@metrowerks.com> 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) > > Any plans to replace the (non-thread safe) DebugNew with your new memory > debugger? Or merge the two and include them with CW? No such plans that I'm aware of. My intent with simple_mem_debug was more along the lines of distributing a copyright-free basic memory debugger that was simple enough that everyone could customize it to their own needs, and yet still useful if you didn't enhance it at all. And I must admit that simple_mem_debug is also a nice vehicle to show some practical use cases of a few Metrowerks extensions (hash_map, alloc_ptr, recursive_mutex, call_once). :-) DebugNew still sports features that simple_mem_debug doesn't, for example: DebugNewForgetLeaks(), DebugNewSetErrorHandler(). And then there is the backwards compatibility issue of changing the interface of DebugNew. -Howard
Thread Navigation
This is a paginated view of messages in the thread with full content displayed inline.
Messages are displayed in chronological order, with the original post highlighted in green.
Use pagination controls to navigate through all messages in large threads.
Back to All Threads