__mt_alloc源码分析(8)

来源:互联网 发布:mac安装office教程 编辑:程序博客网 时间:2024/06/15 17:07

__gnu_cxx::lock

OK,现在是时候研究lock了。它的定义在GCC源码的“libstdc++-v3/include/bits/concurrence.h”文件里,以下简称concurrence.h

 

<concurrence.h>

79     /// @brief  Scoped lock idiom.

80     // Acquire the mutex here with a constructor call, then release with

81     // the destructor call in accordance with RAII style.

82     class lock

 

使用构造函数加锁,析构函数解锁的辅助类,相信读者应该不会陌生。

 

83     {

84       // Externally defined and initialized.

85       mutex_type& device;

 

mutex_type是一个平台相关的类型,在我的环境里,它是pthread_mutex_t

 

87     public:

88       explicit lock(mutex_type& name) : device(name)

89       { __glibcxx_mutex_lock(device); }

 

在构造函数里加锁。__glibcxx_mutex_lock是平台相关的宏定义,在我的环境里有:

 

<concurrence.h>

48   #  define __glibcxx_mutex_lock(NAME) /

49   __gthread_mutex_lock(&NAME)

 

符号__gthread_mutex_lock是函数pthread_mutex_lock的弱引用。

 

<concurrence.h>

91       ~lock() throw()

92       { __glibcxx_mutex_unlock(device); }

 

在析构函数里解锁。宏__glibcxx_mutex_unlock在我的环境里定义为:

 

<concurrence.h>

64   # define __glibcxx_mutex_unlock(NAME) __gthread_mutex_unlock(&NAME)

 

符号__gthread_mutex_unlock是函数pthread_mutex_unlock的弱引用。

 

94     private:

95       lock(const lock&);

96       lock& operator=(const lock&);

 

禁止对lock对象进行复制。

 

97     };

 

总结一下,__gnu_cxx::lock对象在构造的时候,把一个pthread_mutex_t对象加锁,直到析构的时候再把这个pthread_mutex_t对象解锁。于是可以利用__gnu_cxx::lock对象的生命周期,对一个代码范围进行自动的锁定,而这正是前面函数_M_destroy_thread_key的代码和下面的代码使用__gnu_cxx::lock的原因。

 

回到__pool<true>::_M_initialize

好了,我们介绍完了lockfreelist_mutexfreelist,现在回头再到当初中断的地方。

 

<mt_allocator.cc>

466      if (__gthread_active_p())

467        {

468     {

 

这个“{”括号的作用是限定下面的__gnu_cxx::lock的加锁区域。

 

469       __gnu_cxx::lock sentry(__gnu_internal::freelist_mutex);

 

对线程id链表freelist进行加锁。

 

471       if (!__gnu_internal::freelist._M_thread_freelist_array

472           || __gnu_internal::freelist._M_max_threads

473          < _M_options._M_max_threads)

 

仔细研究这2个判断条件,你会发现很多东西。

第一个条件是_M_thread_freelist_array是否为0,这说明了freelist是否被初始化过。这个条件实际上隐含了:函数__pool<true>::_M_initialize会被运行多次?是的!不过先不要迷惑,且看后面的代码再说。

第二个条件是:freelist链表当前的长度是否小于内存池需要的线程id个数。既然_M_initialize会被多次运行,那么使用一个新的_M_max_threads 值来初始化一个已经在使用的freelist链表也就成为可能了。所以,如果新的_M_max_threads值比freelist链表长度小,自然什么都不许要改变;如果新的_M_max_threads值更大,那我们就需要重新初始化freelist,使它拥有更多的线程id节点数,以满足我们的最大需求。

究竟什么情况下会多次运行_M_initialize呢?答案是多线程下使用__per_type_pool_policy的时候。也许读者应该回头翻翻多线程下__per_type_pool_policy的代码(mt_allocator.h546行),和_S_get_pool的实现(mt_allocator.h470行)。每一个_Tp类型模板参数,都会实例化出一个新的__per_type_pool_policy类型,从而使得从_S_get_pool里得到的内存池对象(类型为__pool<true>)各不相同。但是,这些__pool<true>对象都会运行自己的_M_initialize函数,使用的还是同一个全局线程id链表freelist,于是问题来了:怎么样合理的初始化freelist

请读者先想想自己会怎么做:

1)如果freelist还没有被初始化过(第一个条件),那么自然就用当前__pool<true>对象的参数初始化它;

2)如果freelist已经初始化了,那么我们应该判断线程id个数是否足够,如果不足(第二个条件),那么需要重新初始化它,并使用当前__pool<true>对象的参数。

2种情况都导致同样的“下一步”:对freelist进行初始化。但情况2)比情况1)会有更多的“后续工作”需要完成,这在后面的代码里我们会看到。

 

<mt_allocator.cc>

474         {

475           const size_t __k = sizeof(_Thread_record)

476                  * _M_options._M_max_threads;

477           __v = ::operator new(__k);

478           _Thread_record* _M_thread_freelist

479         = static_cast<_Thread_record*>(__v);

 

计算线程id节点数组的总字节大小__k,然后向OS申请内存,并最后存储在局部变量_M_thread_freelist里。这里我不得不抱怨一下,_M_xxxx的命名方式向来是类的成员变量和函数专用的,为什么要用在一个局部变量上?更糟糕的是,_M_thread_freelist这个名字与成员变量__pool<true>::_M_thread_freelist竟然重名!这在开始的时候给我造成了不少的麻烦!

实际上,__pool<true>::_M_thread_freelist根本就没有被用到过,下面代码中出现的所有_M_thread_freelist都是指局部变量_M_thread_freelist,希望读者不要被代码作者“迷惑”。

 

481           // NOTE! The first assignable thread id is 1 since the

482           // global pool uses id 0

483           size_t __i;

484           for (__i = 1; __i < _M_options._M_max_threads; ++__i)

485         {

486           _Thread_record& __tr = _M_thread_freelist[__i - 1];

487           __tr._M_next = &_M_thread_freelist[__i];

488           __tr._M_id = __i;

489         }

 

把新的线程id节点数组“串联”成链表的形式,_M_id值被设置成下标值加1

 

491           // Set last record.

492           _M_thread_freelist[__i - 1]._M_next = NULL;

493           _M_thread_freelist[__i - 1]._M_id = __i;

 

设置最后一个节点。到这里为止,情况1)和2)的“共同工作”做完了。

 

495           if (!__gnu_internal::freelist._M_thread_freelist_array)

 

区分情况1)和情况2),即“freelist还没有被初始化过”还是“线程id个数是否足够”。

 

496         {

 

下面是情况1)的“后续工作”。

 

497           // Initialize per thread key to hold pointer to

498           // _M_thread_freelist.

499           __gthread_key_create(&__gnu_internal::freelist._M_key,

500                        __gnu_internal::_M_destroy_thread_key);

 

创建线程私有数据空间,并把前面介绍的_M_destroy_thread_key函数作为线程退出时的“清理函数”。符号__gthread_key_create是函数pthread_key_create的弱引用。读者可以证明一下,这个部分的代码在整个程序里只会被运行一次。

 

501           __gnu_internal::freelist._M_thread_freelist

502             = _M_thread_freelist;

 

把新的线程id链表交给freelist管理。

 

503         }

504           else

505         {

 

下面是情况2)的“后续工作”。

也许我需要先解释一下这些“后续工作”是什么,以便读者更容易看懂下面的代码。前面已经介绍过,不同的_Tp类型模板参数,会实例化出不同的__per_type_pool_policy类型,从而使得从_S_get_pool里得到的__pool<true>对象各不相同。那么这些不同的__pool<true>对象会在什么时候调用_M_initialize初始化呢?答案是任何时候。比如,针对int__pool<true>对象可能在某个使用std::vector<int>的代码点进行了初始化,但是你仍不知道针对double__pool<true>对象会在什么时候进行初始化,因为你不知道程序会在何时第一次调用函数__mt_alloc<double>:: allocate

所以,当程序最后运行到这里来的时候,freelist里面可能已经“面目全非”了:有些线程id被分配出去了,有些又被归还回来了。新的线程id链表必须完整的记录下这些信息,才能保证整个程序的正确运行。下面的代码就是这个信息的复制过程。

 

506           _Thread_record* _M_old_freelist

507             = __gnu_internal::freelist._M_thread_freelist;

508           _Thread_record* _M_old_array

509             = __gnu_internal::freelist._M_thread_freelist_array;

 

先把旧链表保存起来。_M_thread_freelist_array_M_thread_freelist的关系在前面介绍过了。其实这里还可以想一下,为什么不能在已有的线程id基础上,加上不足的那部分线程id?读者一定马上想到,所有的线程id节点都在一个数组里,是_M_thread_freelist_array存在的基础,也是__freelist能正常工作的前提假设,所以不能简单的追加线程id节点。

下面的代码把旧链表的结构复制到新链表里。

 

510           __gnu_internal::freelist._M_thread_freelist

511             = &_M_thread_freelist[_M_old_freelist - _M_old_array];

 

这又是一个用_M_xxxx给局部变量命名的例子!_M_old_freelist所指的节点,是旧链表里的第一个节点,于是“_M_old_freelist - _M_old_array”得到的就是这个节点的下标。所以上面的代码其实是把新链表的第一个节点设置成与旧链表对应的那个节点。下图描述这句代码的作用。

假设旧链表_M_old_array长度为8,其中1236号线程id已经分配出去,只有4578还是空闲的,_M_old_freelist指向节点4。图中还给出了每个节点的next指针的指向,最后一个节点8next显然是NULL。新链表由局部变量_M_thread_freelist指向,首先我们应该把新链表的_M_thread_freelist指针指向对应的4号节点,这正是上面的代码做的事情。

 

512           while (_M_old_freelist)

513             {

514               size_t next_id;

515               if (_M_old_freelist->_M_next)

516             next_id = _M_old_freelist->_M_next - _M_old_array;

517               else

518             next_id = __gnu_internal::freelist._M_max_threads;

519               _M_thread_freelist[_M_old_freelist->_M_id - 1]._M_next

520             = &_M_thread_freelist[next_id];

521               _M_old_freelist = _M_old_freelist->_M_next;

522             }

 

这段代码所做的事情是:遍历旧链表的每个节点,获得它们的next指针的指向,然后把新链表对应的节点也设置成同样的指向。比如第一次循环之后,上图的例子会变成这样:

 

 

再经过2此循环,新链表的节点5next会指向7,而节点7next会指向8_M_old_freelist这时也指向了旧链表的节点8,如下图所示:

接下来,新链表里节点8next应该指向何方?因为旧链表里,节点8nextNULL,所以不能作为参考了。第518行代码告诉了我们答案:指向第freelist._M_max_threads个节点(节点9)。而我们从前面对新链表的“串联”操作里知道,新链表本来就是像糖葫芦一样串好了的,于是9101112自然就加入了空闲链表的行列:

523           ::operator delete(static_cast<void*>(_M_old_array));

 

释放掉旧的链表。

 

524         }

525           __gnu_internal::freelist._M_thread_freelist_array

526         = _M_thread_freelist;

527           __gnu_internal::freelist._M_max_threads

528         = _M_options._M_max_threads;

 

好了,这4行代码是情况1)和情况2)的最后的共同点,因为无论哪种情况,最后都要把新的空闲链表交给freelist来管理。

 

529         }

530     }

 

至止,我们研究完了_M_initialize函数里最难的代码部分,因为这部分代码需要考虑__per_type_pool_policy策略下任何时间都可能发生的初始化工作,而且是对同一个全局变量的初始化。如果读者读懂了这部分代码,相信也会有同样的感觉。

 

532     const size_t __max_threads = _M_options._M_max_threads + 1;

533     for (size_t __n = 0; __n < _M_bin_size; ++__n)

 

初始化每个bin的内部数据。

 

534       {

535         _Bin_record& __bin = _M_bin[__n];

536         __v = ::operator new(sizeof(_Block_record*) * __max_threads);

537         __bin._M_first = static_cast<_Block_record**>(__v);

 

计算_M_first数组的字节大小,然后分配空间。_M_first的角色我们在前面已经介绍过了。

 

539         __bin._M_address = NULL;

540 

541         __v = ::operator new(sizeof(size_t) * __max_threads);

542         __bin._M_free = static_cast<size_t*>(__v);

543           

544         __v = ::operator new(sizeof(size_t) * __max_threads);

545         __bin._M_used = static_cast<size_t*>(__v);

 

初始化freeused计数器数组。free计数器用来记录每个线程当前空闲的内存块个数,used记录分配出去的内存块个数。多线程内存分配器容易出现的一个问题是,某个线程的空闲块链表变得很长,但是这个线程却再也不需要这些内存;同时其他线程却总是内存紧缺,于是不得不从全局链表里不断申请内存。如果这种现象重复下去,最终程序可能因内存不足而退出,而实际上这个程序任何时刻只需要50M不到的内存就足够了。mt allocator为了防止这种现象,采用了一种把线程里的空闲内存归还给全局链表的算法,而这个算法需要每个线程有自己的freeused计数器。

 

547         __v = ::operator new(sizeof(__gthread_mutex_t));

548         __bin._M_mutex = static_cast<__gthread_mutex_t*>(__v);

549           

550  #ifdef __GTHREAD_MUTEX_INIT

551         {

552           // Do not copy a POSIX/gthr mutex once in use.

553           __gthread_mutex_t __tmp = __GTHREAD_MUTEX_INIT;

554           *__bin._M_mutex = __tmp;

555         }

556  #else

557         { __GTHREAD_MUTEX_INIT_FUNCTION(__bin._M_mutex); }

558  #endif

 

初始化bin的锁成员。

 

559         for (size_t __threadn = 0; __threadn < __max_threads; ++__threadn)

560           {

561         __bin._M_first[__threadn] = NULL;

562         __bin._M_free[__threadn] = 0;

563         __bin._M_used[__threadn] = 0;

564           }

 

初始化3个数组的值。

 

565       }

 

对每个bin的初始化工作完成了。

 

566        }

567      else

 

这个else对应的是第588行的“if (__gthread_active_p())”。所以下面的代码是在没有链接多线程库的时候才会运行。

 

568        {

569     for (size_t __n = 0; __n < _M_bin_size; ++__n)

570       {

571         _Bin_record& __bin = _M_bin[__n];

572         __v = ::operator new(sizeof(_Block_record*));

573         __bin._M_first = static_cast<_Block_record**>(__v);

574         __bin._M_first[0] = NULL;

575         __bin._M_address = NULL;

576       }

 

把内存池当成单线程下的__pool<false>处理。

 

577        }

578      _M_init = true;

 

最后设置_M_init,说明全部初始化工作完成。

 

579    }

 

终于研究完了多线程下的__pool<true>::_M_initialize函数,现在我建议读者再回头温习一下上面所有的代码,以确认自己是否真的对它们了如指掌了。同时我还有一点需要说明,在<mt_allocator.cc>620行,还有一个

 

<mt_allocator.cc>

620    void

621    __pool<true>::_M_initialize(__destroy_handler)

这个重载函数的代码和前面无参数_M_initialize函数的一模一样,实际上__destroy_handler只是一个函数指针类型,参数连名字也没有。这个重载函数从没有被调用过,所以我也不确定它的作用是什么。

原创粉丝点击