基於tc_malloc的高併發內存池

內存碎片問題;
性能問題;
高併發(在多線程同時申請內存時,效率依舊很高)。
threadcache是解決高併發的性能問題,因爲沒有鎖;
centralcache是平衡threadcache資源競爭的問題,避免一個線程用多了,另一個線程沒得用;但是這裏的平衡會付出一定的代價,當多個線程同時來申請時需要加鎖,CentralCache下面掛的是一個一個的span,span是一些內存對象的集合,這些內存是以頁爲單位,每個span下面又掛了不同字節的對象;
PageCache的span是這個對象沒有切開的,它下面掛的是1頁的內存,它的裏面的_list爲空,對象還沒有被切開,它還用不上切開的對象,它主要是做頁的分割和合並,解決內存碎片問題的,它連頁的起始地址都沒有,不過他有頁號和它是幾頁的,這樣就可以它的起始地址和內存數。
關於TLS
內存池這個項目是基於三級緩存結構的內存池,它在一定程度上減輕了malloc遺留下來的的內存碎片問題;還實現了多線程併發申請內存時,效率依舊很高。對於內存碎片,整體控制在12%左右;爲了達到高併發時仍舊高效,使用線程本地存儲(TLS),達到每個線程併發申請時,互不干擾。
TLS(Thread Local Storage)線程局部存儲,保存每個線程本地的ThreadCache的指針,作用是申請和釋放內存不用加鎖。TLS變量裏面:每一個線程有一份獨立實體,各個線程的值互不干擾。可以用來修飾那些帶有全局性且值可能變,但是又不值得用全局變量保護的變量。
TLS原理剖析:(定義一個全局變量,這個全局變量只屬於當前線程,其它線程看不見)爲了找到每一個ThreadCache,我們可以保存ThreadCache的指針,並定義一個全局變量,但是全局變量只能指向其中一個,這樣的話其它的ThreadCache就找不到了,所以這時候,我們把用來保存不同ThreadCache的指針鏈接起來,再定義一個全局指針指向它(其中一個threadcache),這樣的話,所有的線程找到的都是這個全局指針,然後再通過全局指針找到屬於自己的threadcache,每個線程裏面都有一個pid,就可以確認是屬於哪個線程的。
我們把:定義一個全局變量,但是這個全局變量不是所謂的所有線程共享,而是每個線程自己的,相當於每個線程定義了自己的全局變量,每個線程都有一個全局變量,這樣當有多個線程同時來併發申請內存時,就可以自己找自己的ThreadCache,不受到別人的干擾,系統有一種機制,定義這樣的變量,就叫TLS。
靜態的TLS是:直接定義
動態的TLS是:調用系統的API去創建的,我們這個項目裏面用到的就是靜態的TLS。

三層結構剖析
ThreadCache
第一次來,每個線程都要調內存申請的這個接口,第一次調的時候,threadcache的指針爲空,這個時候就會創建一個threadcache的一個對象,每個線程是如何獲取自己的threadcache呢?爲了避免加鎖,使用TLS做到不用加鎖(稍微解釋一下TLS,避免和網絡中的TLS庫混淆),threadcache是一個掛了對象的自由鏈表,申請的時候,只要是8字節到64k的內存大小,都來ThreadCache這裏申請,申請的時候,有一套它的映射規則:一開始是128字節以內,以8字節爲一個間隔,走它的映射,129-1024是以16字節爲間隔;控制在%12左右的內碎片的浪費,而且這個間隔爲了好算,取的是2的次方倍,整體把內碎片浪費率控制在%12左右:
整體把內碎片浪費率控制在%12左右
     [1,128]                 8byte對齊       freelist[0,16)      128/8=16
     [129,1024]              16byte對齊      freelist[16,72)     (1024-129)/16=56
    [1025,8*1024]            128byte對齊     freelist[72,128)    (8*1024-1025)/128=56
    [8*1024+1,64*1024]       1024byte對齊    freelist[72,184)
CentralCache
CentralCache的映射規則和threadcache的映射規則一模一樣,CentralCache處於一個承上啓下的作用,對上平衡threadcache的資源競爭問題,對pagecache是:輔助頁緩存進行頁的合併,當一個span的對象全部釋放回來時,將span還給pagecache,並且進行頁合併。
PageCache
PageCache主要是做頁的分割和合並,有頁號和是幾頁的,就可以算它的起始地址和內存數了。PageCache下面掛的也是一個一個的span,但是CentralCache和PageCache的span是不一樣的,首先映射規則不一樣,CentralCache和ThreadCache一樣,而PageCache是以頁爲單位的映射規則:1頁,2頁,一直到128頁。而CentralCache是以8字節爲間隔的(控制住內存的浪費率在不超過%12)。
高併發如何實現(即申請的過程)
thread cache
申請內存的過程

線程要申請內存不是直接去申請的,是被動去申請的,當一個線程來申請內存空間時,給它創建一個thread cache,同時調用併發申請接口去申請內存。(TLS保證了每個線程都有一個自己的thread cache)
當線程申請的內存小於等於64kb時,就直接在thread cache裏對應位置的鏈表處申請,時間複雜度O(1),並且沒有鎖競爭。如果此時對應位置的鏈表上並沒有掛內存塊的話,就會去central cache裏去申請一塊批量的內存,然後返回一塊空間,並將剩下的內存塊掛在自己的thread cache裏的對應FreeList裏。
多線程併發的去central cache裏獲取批量內存時central cache要加鎖,並且central cache是一個單例模式,全局只有一個,雖然這裏會加鎖導致線程串行去申請批量的內存,但是每個線程每次來申請的內存是批量的。假如一次申請的批量內存是50個,那麼之後的49次都不需要再去找central cache要了。
central cache
中心緩存是所有線程所共享,thread cache是按需從central cache中獲取的對象。central cache週期性的回收thread cache中的對象,避免一個線程佔用了太多的內存。達到內存分配在多個線程中更均衡的按需調度的目的。central cache是存在競爭的,所以從這裏取內存對象是需要加鎖的,並且central cache在全局只有一個,因此要設計爲單例模式,不過一般情況下在這裏取內存對象的效率非常高,並且一次給thread cache的內存是批量的,所以這裏競爭不會很激烈。
申請內存的過程

當thread cache裏對應FreeList鏈表裏沒有掛內存塊時,就會來central cache裏拿內存,SpanList也是一個哈希映射的鏈表,根據要申請內存塊的大小找到對應的SpanList,在SpanList中找一個不爲空的span,然後將這個span中提前切好的內存塊批量的給thread cache。如果對應的SpanList裏沒有一個不爲空的span時,就會去page cache裏去申請一個span對象,span對象裏是以頁爲單位的內存,將這個span中的內存切成對應的大小,然後將這個span對象掛在這個SpanList裏。
page cache
申請內存的過程

當central cache的SpanList裏沒有span或者沒有不爲空的span時,central cache就會向page cache裏申請一個span。首先會去看對應頁數的SpanList裏有沒有掛span,如果有直接返回一個對應頁數的span,如果沒有就會向後遍歷,尋找較大頁數的span進行切分。如果找到128頁的位置都沒有找到一個span時,就會直接去系統申請一塊128頁的內存,然後進行切分。
具體的切分過程是這樣的:假如我已經向系統申請了一塊128頁的內存然後掛在128頁的SpanList上,如果我需要1頁的span,就會將這個128頁的span切分成一個1頁的span和一個127頁的span,再將這個1頁的span切分好返回給central cache,然後把剩下的127頁的span掛在127頁的SpanList上。
內存碎片是如何處理的(即釋放的過程)
thread cache
釋放內存的過程

當某個線程用完一塊內存要歸還給它自己的thread cache時,直接根據內存塊的大小找到對應的位置插入到對應位置的FreeList鏈表中。如果鏈表的長度過長,就要回收一部分內存塊到central cache裏,保證每一個FreeList裏不會掛太多的內存塊。
central cache
釋放內存的過程

當給用戶的內存用完時,就會將內存掛到threadcache中的對應的FreeList中,當對應的thread cache裏某個FreeList裏掛的內存太多時,就會批量的釋放回central cache,這時中心緩存中span裏的_usecount會減去歸還的內存塊的個數,當span中的_usecount等於0時,就說明所有內存已經全部歸還。所以在SpanList裏不會存在_usecount等於0的span,因爲一旦某個span的_usecount爲0時,就會被移出這個SpanList,然後去page cache裏進行span的合併。
需要注意的是:

這裏歸還回來的內存塊極有可能來自不同的span,所以這裏用到了STL中的關聯容器map,建立了PageID和span的映射,同一個span切出來的內存塊PageID都和span的PageID相同,這樣就能很好的找出某個內存塊屬於哪一個span了。
page cache
釋放內存的過程

如果central cache的某個span的_usecount爲0時,該span就會被釋放回page cache裏,此時page cache有個空閒的span,這個span會依次尋找它的前後頁的span,看是否可以合併,如果可以合併,就會繼續向前尋找。這樣就可以將切小的內存合併成大的span,減少內存碎片。具體合併的過程是這樣的:假如現在有一個PageID爲50的3頁的span,有一個PageID爲53的6頁的span。這兩個span就可以合併,會合併成一個PageID爲50的9頁的span,然後掛在9頁的SpanList上。
 

三級緩存結構
ThreadCache:線程緩存整體是一個對象數組,數組的每一層是一個自由鏈表。


CentralCache:中心緩存是一個基於單例模式的跨度數組,數組的每一層是一個雙向帶頭循環的跨度鏈表,它的作用是平衡多線程的資源競爭問題。


PageCache:頁緩存也是一個基於單例模式的結構,主要作用是完成大批量內存的申請、釋放和頁的合併(內存碎片問題)。

三層結構的整體框架圖如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章