STL容器放入共享内存

STL 可以说是C++的重要的组成部分,它提供了一系列方便使用的容器,可以供我们存放数据,节省了很多开发时间。不过如果打算在多个进程之间共享一个STL容器,却常常会遭遇一些困难。比如你正在使用一个map,一般的IPC方法,可能下意识的你就会选择使用共享内存,如果可以把这个map放入一块共享内存供多个进程操作,那该多方便呢。然而问题却没这么简单,由于STL帮我们完美的封装好了map的内部方法,包括内存分配方案,这也导致了我们没法把map直接简单的用placement new放置到已知的共享内存上。

要完成这个任务就需要我们自己实现一个基于共享内存的allocator,替换map默认的allocator,在这个allocator中实现map的内存分配方案。关于allocator的讲解可以看侯捷的《STL源码剖析》这本书,写一个简单的allocator直接套下边的模板就可以了,主要实现的是这几个函数:

  • max_size() 容器的最大容量
  • allocate(num) 为num个元素分配内存
  • construct(p) 将p所指的元素初始化
  • destroy(p) 销毁p所指向的元素
  • deallocate(p, num) 收回p所指的num个空间

具体代码如下:

#include <limits>
#include <iostream>

namespace MyLib
{
    template <class T>
    class MyAlloc
    {
    public:
        // type definitions
        typedef T        value_type;
        typedef T*       pointer;
        typedef const T* const_pointer;
        typedef T&       reference;
        typedef const T& const_reference;
        typedef std::size_t    size_type;
        typedef std::ptrdiff_t difference_type;

        // rebind allocator to type W
        template <class W>
        struct rebind
        {
            typedef MyAlloc<W> other;
        };

        // return address of values
        pointer address (reference value) const
        {
            return &value;
        }
        const_pointer address (const_reference value) const
        {
            return &value;
        }

        /* constructors and destructor
         * - nothing to do because the allocator has no state
         */
        MyAlloc() throw()
        {}
        MyAlloc(const MyAlloc&) throw()
        {}
        template <class W>
        MyAlloc (const MyAlloc<W>&) throw()
        {}
        ~MyAlloc() throw()
        {}

        // return maximum number of elements that can be allocated
        size_type max_size () const throw()
        {
            return std::numeric_limits<std::size_t>::max() / sizeof(T);
        }

        // allocate but don't initialize num elements of type T
        pointer allocate (size_type num, const void* = 0)
        {
            // print message and allocate memory with global new
            std::cerr << "allocate " << num << " element(s)"
                      << " of size " << sizeof(T) << std::endl;
            pointer ret = (pointer)(::operator new(num*sizeof(T)));
            std::cerr << " allocated at: " << (void*)ret << std::endl;
            return ret;
        }

        // initialize elements of allocated storage p with value value
        void construct (pointer p, const T& value)
        {
            // initialize memory with placement new
            new((void*)p)T(value);
        }

        // destroy elements of initialized storage p
        void destroy (pointer p)
        {
            // destroy objects by calling their destructor
            p->~T();
        }

        // deallocate storage p of deleted elements
        void deallocate (pointer p, size_type num)
        {
            // print message and deallocate memory with global delete
            std::cerr << "deallocate " << num << " element(s)"
                      << " of size " << sizeof(T)
                      << " at: " << (void*)p << std::endl;
            ::operator delete((void*)p);
        }
    };

    // return that all specializations of this allocator are interchangeable
    template <class T1, class T2>
    bool operator== (const MyAlloc<T1>&,
            const MyAlloc<T2>&) throw()
    {
        return true;
    }
    template <class T1, class T2>
    bool operator!= (const MyAlloc<T1>&,
            const MyAlloc<T2>&) throw()
    {
        return false;
    }
}

如果要使用这个allocator的话,可以这样:

#include <vector>
#include "myalloc.hh"

int main()
{
    // create a vector, using MyAlloc<> as allocator
    std::vector<int,MyLib::MyAlloc<int> > v;

    // insert elements
    // - causes reallocations
    v.push_back(42);
}

写出这样一个简单的vector内存分配器是很容易,不过如果要针对map或者hash_map写一份高效鲁棒的内存分配器却也不是一件容易的事情。一个想法就是在共享内存上划分出内存池,自己对这块内存池进行维护,并且还得考虑进程同步问题。然而,一件幸运的事情是,在boost项目第1.35个版本之后,已经增加了一个叫做boost::interprocess的库,其中包含了关于多线程和多进程的很多有用的辅助类库,当中也有为STL容器所写的allocator,这里有一系列非常棒的示例程序

其中有一个注意点,由于allocator是以类的形式提供的,所以当你在使用STL容器的时候,你需要保证allocator的实例没有被销毁,例如下边的函数希望产生mymap这个指针供其他函数使用,结果将是错误的。事实上,一旦该函数运行结束,mymap所指向的内存地址将会失效。

typedef int KeyType;
typedef float MappedType;
typedef std::pair<const KeyType, MappedType> ValueType;
typedef allocator<ValueType, managed_shared_memory::segment_manager> ShmemAllocator;
typedef map<KeyType, MappedType, std::less<KeyType>, ShmemAllocator> MyMap;

MyMap* get_ptr()
{
    using namespace boost::interprocess;

    managed_shared_memory segment(create_only, "MySharedMemory", 65536);
    ShmemAllocator alloc_inst (segment.get_segment_manager());

    // If you wish to give `mymap' to other functions as parameters,
    // please make sure the instance `alloc_inst' must be existed at
    // that time.
    MyMap *mymap = segment.construct<MyMap>("MyMap")(std::less<KeyType>(),alloc_inst);
    return mymap;
}

另外由 managed_shared_memory 创建的共享内存默认属性是 600。总之,结合了STL和boost库,把STL容器放入共享内存中供多进程使用,就变成了一件非常容易而且愉快的事情。