🚀 go-pugleaf

RetroBBS NetNews Server

Inspired by RockSolid Light RIP Retro Guy

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
#660
Author: Howard Hinnant
Date: Tue, 30 Sep 2003 01:25
436 lines
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
#661
Author: Steven Fisher
Date: Thu, 02 Oct 2003 16:19
12 lines
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
#662
Author: Howard Hinnant
Date: Thu, 02 Oct 2003 17:19
48 lines
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
#663
Author: Sean McBride
Date: Wed, 08 Oct 2003 00:34
13 lines
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
#664
Author: Howard Hinnant
Date: Wed, 08 Oct 2003 16:52
32 lines
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