__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
好了,我们介绍完了lock,freelist_mutex和freelist,现在回头再到当初中断的地方。
<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.h第546行),和_S_get_pool的实现(mt_allocator.h第470行)。每一个_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,其中1,2,3,6号线程id已经分配出去,只有4,5,7,8还是空闲的,_M_old_freelist指向节点4。图中还给出了每个节点的next指针的指向,最后一个节点8的next显然是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此循环,新链表的节点5的next会指向7,而节点7的next会指向8,_M_old_freelist这时也指向了旧链表的节点8,如下图所示:
接下来,新链表里节点8的next应该指向何方?因为旧链表里,节点8的next是NULL,所以不能作为参考了。第518行代码告诉了我们答案:指向第freelist._M_max_threads个节点(节点9)。而我们从前面对新链表的“串联”操作里知道,新链表本来就是像糖葫芦一样串好了的,于是9,10,11,12自然就加入了空闲链表的行列:
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);
初始化free和used计数器数组。free计数器用来记录每个线程当前空闲的内存块个数,used记录分配出去的内存块个数。多线程内存分配器容易出现的一个问题是,某个线程的空闲块链表变得很长,但是这个线程却再也不需要这些内存;同时其他线程却总是内存紧缺,于是不得不从全局链表里不断申请内存。如果这种现象重复下去,最终程序可能因内存不足而退出,而实际上这个程序任何时刻只需要50M不到的内存就足够了。mt allocator为了防止这种现象,采用了一种把线程里的空闲内存归还给全局链表的算法,而这个算法需要每个线程有自己的free和used计数器。
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只是一个函数指针类型,参数连名字也没有。这个重载函数从没有被调用过,所以我也不确定它的作用是什么。
- __mt_alloc源码分析(8)
- __mt_alloc源码分析(1)
- __mt_alloc源码分析(2)
- __mt_alloc源码分析(3)
- __mt_alloc源码分析(4)
- __mt_alloc源码分析(5)
- __mt_alloc源码分析(6)
- __mt_alloc源码分析(7)
- __mt_alloc源码分析(9)
- __mt_alloc源码分析(10)
- __mt_alloc源码分析(11)
- __mt_alloc源码分析(12)
- Mahout贝叶斯算法源码分析(8)
- Zookeeper源码分析(8)- CommitProcessor
- ArrayList源码分析(jdk1.8)
- HashMap源码分析(jdk1.8)
- ArrayList源码分析(JDK1.8)
- java源码分析(8)-Boolean
- C#数据库连接
- 弹出窗口window.open()的参数列表
- Learning python 系列7
- windows 下使用QT,
- Hibernate +oracle配置
- __mt_alloc源码分析(8)
- LINQ to SQL(Refer from codeproject)
- Oracle维护常用SQL语句
- 求救。。。。
- 弹出窗口总结
- 触发器 tigger
- 求救。。。。
- 李开复:21世纪最需要的7种人才
- 求救。。。。