純C環境下內存模型與Java平臺不一致?加上內存屏障(fence)或者lock指令就行。
C環境下缺少對象模型?無非是給每個數據塊提供個*init方法(比如pthread_mutex_t的pthread_mutex_init,pthread_barrier_t的pthread_barrier_init),之後再將邏輯的部分直接寫到函數裏(phtread_mutex_lock、pthread_barrier_wait),無非是這些邏輯並不是像Java平臺上一樣被綁定到某個"對象"。這些都不是本質上困難的地方。
真正的困難在於內存回收。
Java平臺,我們能夠對內存做的唯一事情就是申請、創建(new關鍵字),如此一來便得到一個新的對象。之後,我們無法直接針對這塊內存釋放。我們最多隻能把自己目所能及範圍內的關於這塊內存的"引用"置爲null,從而期待GC去回收(前提是這塊內存不存在其他引用)。同時,我們根本無法知道究竟哪個時刻這塊內存會被回收,我們只能認爲,Runtime environment能夠選擇最適當的時刻。
**同時,Java虛擬機給出了一個更強的保證:只要你的對象(引用)obj != null,那麼這個引用所指示的對象便是肯定存在的,我們可以絕對安全地調用obj.method()而不用害怕任何意外。**
把這個場景放在C環境下比喻似乎就在說:如果你能看到某個地址(數值),那麼就放心對他做任何合理的事吧!Runtime environment確保了你的安全!
總之,Java運行時給出了強大的保證:
1 看得到的對象都存在,你可以放心對它操作。
2 那些正在被回收的、你不能安全操作的對象(內存),你絕對無法看到。
如此一來,無論是內存獨佔或共享、線程併發或非併發,我們都無需擔心內存本身的問題(Java GC回收所有內存,全平臺的垃圾回收器)。
那麼現在我們回頭來看C環境。它的運行時環境根本不幫你做任何事,你甚至可以虛構一個內存地址,然後將它強制轉化爲虛構的struct類型,接着對它操作。只不過這樣很可能破壞了內存,導致不可預期的結果,同時這個錯誤會一直潛伏,你根本不知道何時出現。甚至於,都不需要定義結構體信息,假如你瞭解運行機器的具體信息,你都可以直接在一個地址上做地址偏移來操作內存......當然,前提是這塊內存是安全的。
那麼問題就很清楚了,
Java環境下內存安全回收由虛擬機完全負責。
C環境下,內存安全回收由邏輯單元(線程)本身來負責。
也就是說C環境下,我們不僅要處理算法本身的邏輯,同時也要額外去處理內存回收的問題。
那麼C環境下內存回收困難在哪呢?
首先,我們不考慮不需要回收的內存。我們知道,獨佔的內存容易回收,對於共享的內存,假如有個邏輯點,我們確保所有線程當下以及將來都不會使用這塊內存,那麼也是可以安全回收的。
**所以真正難於回收的是滿足以下3個條件的內存:**
1. **共享的。**
2. **需要回收的(有些程序設計成不回收)。**
3. **沒有明確的可以安全回收內存的邏輯點(存在被併發讀寫,但是有明確邏輯點回收的設計,比如主邏輯join)**
OK,我們接着引出Maged M. Michael,在2004發表的《Hazard Pointers:Safe Memory Reclamation for Lock-Free Objects》。
Hazard Pointer可以說是最知名的一種共享內存的回收方案。借用Erez Petrank的ppt,它可以概括如下:
**對於每一塊被共享的內存,這些技術都需要爲它配置另外N(MAX_THREAD)個內存來標記它是否被對應的線程訪問,同時爲了釋放一塊內存,都需要遍歷這N個內存位置從而判定是否可以安全回收(儘管可以採用先排序,二分搜索等方式降低檢索的數據量,但是檢索過程的代價依然是隨着MAX_THREAD而增長的,就算你總操作數一樣。**儘管它的算法是lock-free(甚至wait-free)。***這裏的缺陷在於每一塊共享數據都與線程本身緊緊耦合,幾乎沒有擴展性。***
而另一種被廣泛討論的回收方案是epoch-based Reclamation[Practical lock-freedom][5]以及相似的技術[Performance of memory reclamation for lockless synchronization][6],**它的性能很好,但是如[內存管理規則][7]所說,它並不是無阻塞的算法,它在本質上就會阻塞,所以progress無法得到保證。**
其中2005年,哥德堡大學的研究團隊發表的方案[Practical and Efficient Lock-Free Garbage Collection Based on Reference Counting][8],**儘管也採用了引用計數的方式(與我將要介紹的方式類似),但是卻並沒有解除線程本身與共享數據之間的依賴,並且scan時依然要遍歷整個線程組。**
在不計較lock指令或者fence的情況下,是否存在一種方法能夠同時做到:
- **解除共享數據與線程之間的強制依賴,不需要爲每一塊共享數據造另N個標記(正因如此,通常線程數N較小),從而極大得增強算法的擴展性,並且節省內存。**
- **減少甚至取消判斷內存是否可被回收的遍歷過程,消除MAX_THREAD的限制(算法視角),從而極大地減少了算法的搜索代價,同時增強了算法的靈活性和可用性(無MAX_THREAD限制)。**
- **同時,它保持non-blocking的progress。**
- **另外,能不能提供一種方式,在我們開發某些併發數據結構時,它能夠正確回收對象層次內部的數據呢?**
爲了達成以上三個目標,我向大家介紹自創的**SHP(Scalable Hazard Pointers)**。
----------
SHP
===
Scalable Hazard Pointers分爲如下三個部分來講述:
- **3RE&S協議(每一塊滿足以上三個條件的內存,都通過3RE協議規定的方式回收)**
- **併發無鎖帶引用計數的有序單鏈表(我們組織被記錄地址的方式)**
- **可擴充的Reocrd資源庫(我們維護Record的方式)**
- **不斷的優化(局部保留Record+每線程每內存變量、鏈表遍歷保留前驅、批量獲取Record、統一回收的大塊數據)**
- **帶參數的retire,通過傳入定製的freeMemory方法,從而在回收當前內存前,先回收內部指針開闢的數據**
3RE&S Protocol
----
3RE&S指的是如下四個,針對被分配內存地址(pointer)的抽象操作:
- **RECORD(增加一個記錄計數)**
- **REMOVE(移除一個記錄計數)**
- **RETIRE(標記該記錄的retire,表明之後若記錄計數爲0則可回收)**
- **SCAN:遍歷已經retire的所有record,有策略地回收內存**
同時,對於共享數據的讀、寫爲下面兩種方式:
- read: read共享變量+RECORD+再檢驗共享變量
- write: write共享變量+RETIRE共享變量(write操作一般的應用場景會在read之後)
這個協議總結了,包含了hazardpointer以及許多類似技術的處理方式,RECORD操作時將共享內存地址本身也作爲傳輸傳入。
幾乎任何一種算法,併發共享的數據都可通過以上方式回收。
這裏的關鍵點是:
- 一塊內存是否被回收這件事情最好由SCAN操作中的原子操作來完成,原因是我們肯定是在retire之後才考慮回收內存,同時RECORD可以回退。
- **RECORD之後一般有個再判斷的操作。**
這裏給出個針對Michael原始論文的對比例子:
void enqueue(int value){
NodeType* node;
posix_memalign(&node, 64, sizeof(NodeType));
memset(node, 0, sizeof(NodeType));
node->value = value;
node->next = NULL;
NodeType* t;
for(;;){
t = Tail;
psly_record(&Tail, t);
if(Tail != t) {
psly_remove(t);
continue;
}
NodeType* next = t->next;
if(Tail != t) {
psly_remove(t);
continue;
}
if(next != NULL){
psly_remove(t);
__sync_bool_compare_and_swap(&Tail, t, next);
continue;
}
if(__sync_bool_compare_and_swap(&t->next, NULL, node)) {
psly_remove(t);
break;
}
psly_remove(t);
}
__sync_bool_compare_and_swap(&Tail, t, node);
}
int dequeue(){
int data;
NodeType* h;
for(;;){
h = Head;
//myhprec->HP[0] = h;
psly_record(&Tail, h);
if(Head != h) {
psly_remove(h);
continue;
}
NodeType* t = Tail;
NodeType* next = h->next;
psly_record(&(h->next), next);
//myhprec->HP[1] = next;
if(Head != h) {
psly_remove(next);
psly_remove(h);
continue;
}
if(next == NULL) {
psly_remove(next);
psly_remove(h);
return -1000000;
}
if(h == t){
psly_remove(next);
psly_remove(h);
__sync_bool_compare_and_swap(&Tail, t, next);
continue;
}
data = next->value;
//myhprec->HP[1] = NULL;
//myhprec->HP[0] = NULL;
if(__sync_bool_compare_and_swap(&Head, h, next)) {
psly_remove(next);
psly_remove(h);
retireNode(h);
break;
}
psly_remove(next);
psly_remove(h);
}
//myhprec->HP[0] = NULL;
//myhprec->HP[1] = NULL;
return data;
}
concurrent lock-free ordered singly linked-list with reference counting(基於Harris's list的帶引用計數,可回收節點的list.)
----
首先,爲什麼要爲每塊共享內存維護N個變量,這樣做不僅浪費內存而且增加搜索代價。理想情況下我們應該只需要一個內存數據來處理一個共享內存。那麼我們怎麼來處理多個線程引用它的情況呢?這裏的一個自然的想法是引入引用計數(reference count),(注意,這裏的引用計數是線程引用共享數據,與另一個對象層次間的引用無關),我們用refcount代表當下訪問它的線程數,refCount>0的內存絕對不會被回收,refCount == 0代表沒有線程引用它,處於可以被回收的狀態。我們將這樣的一個內存數據稱爲Record,
struct Record{
//數據字段
void* pointer;
int refcount;
}
第二,由於Record內存本身是要被維護的,所以我們的策略是隨需分配,已分配的不在回收,我們將Record作爲一種可重用的資源,用於追蹤那些共享內存。那麼意味着Record能夠被高效地從資源庫併發獲取和返回。
這裏的技巧是用一個固定的self字段,標記Record本身,(比如低10位作爲indexNum,接下來4位作爲arrayNum),用於唯一的標記Record自身。這麼做的目的是爲了支持後面的map。
想象一下,我們已經有了很多共享的pointer(地址),每個分配一個Record,接着我們要如何組織它們呢?從而高效的支持add,remove,search操作。
這裏的方式是用一個大的map,它具有一個大的buckets數目比如1024。
typedef struct RecordList {
Record* volatile head ;
Record* volatile tail ;
} RecordList ;
typedef struct RecordMap {
RecordList* lists[1024] ;
} RecordMap ;
RecordList初始化之後久擁有了固定的head/tail。
很明顯,我們這裏自然的策略是將具有相同後綴的地址base到同一個list上,同時由於開闢的內存地址一般是單調遞增,並且保持16字節對齊,我們可以根據地址本身來避開了hits。
我們可以抽象出如下的接口如下:
struct Record{
//Record庫維護字段
int volatile next ;
int self;
//數據字段
void* pointer;
int refcount;
}
int record(void* pointer) {
long key = ((long) pointer) >> 4 ;
RecordList* list = map.lists[key & (1024-1)];
return handle_records(list, pointer, RECORD);
}
那麼接下來的問題是如何在一個list上組織那些帶有同樣後綴地址的Record?
這裏推薦著名的[Harris' list][11],它做到用一個併發無鎖單鏈表來組織有序數據,支持增加、刪除、搜索。
我們需要給Record再加一個retire'bit和一個long字段nextRecord,它裏面包含四個字段:
- 下一個Record的self(位移)(最右23位 next 8百萬+節點)
- 下一個Record本身的版本號(20位 nextV 百萬+個版本號)
- 該Record本身的版本號(20位 nodeV 百萬+個版本號)
- 該Record是否已經被邏輯刪除的標記(最高位 isDeleted)
- retireBit代表該Record是否被retire
struct Record{
//Record庫維護字段
int volatile next ;
int self;
//數據字段
void* pointer;
int refcount;
//retire
bool retireBit;
long nextRecord;
}
然後,對它進行了相當程度的改造,改造的地方如下:
- **不將next字段(harris's list的next字段,不是Record資源庫的)作爲地址,將它作爲數值,把Record的self字段原子交換過去。(CAS nextRecord)**
- **對於被切除的Record,我們要自己回收這些Record並將它重新放入資源庫(Harris'list基於GC可以不用管)。(nextRecord)**
- **因爲Record是可複用,所以我們爲它增加版本號字段(nodeV),同時它的next字段也需要版本號來回避ABA問題(nextV) **
- **原來的insert操作有對象的情況下什麼都不做,我們給引用計數+1 (refcount),如果發現已經retire那麼回退**
- **需要一個bit來標記這塊內存被retire了(retireBit),同時需要另一個bit來標記是否被邏輯刪除(邏輯刪除後,該塊內存就可以被回收了,nextRecord最高位)**
注意,由於以上的操作存在相互依賴,比如新增的Record不能鏈接到已經邏輯刪除的Record上。
所以,當一個Record被從資源庫get到之後,生命週期爲:
1. 它的nodeVersion+1,refCount+1,clear DELETE,同時將它的nextValue設置爲後續節點。
2. 接着它會經歷refCount +1/-1的序列操作。
3. nextVersion+1 & nextValue改變 的序列操作。(與2沒有先後關係)
4. retire的bit被設置的操作。(會導致RECORD的失敗或則回退)
5. 最高位DELETE被cmpandswap。(之後該pointer指示的地址可以被回收)
6. 包含該Record在內連在一起被DELETE的節點,都從list上切除,依次回收,並將Record返回資源庫。
7. 該Record被return回資源庫。
這裏最核心的技術就是基於兩點判定節點沒有失效:
1. **共享變量的值依舊是傳入地址值。**
2. **節點的 nodeVersion 以及 DELETE bit維持原狀。**
可擴充的Reocrd資源庫
----
我們需要一些Record去持有地址,我們採用隨需批量malloc的方式。這種方式的特點是:
- 可重複利用:大量malloc和free的方式對操作系統的內存維護也不友好,我們替換爲支持併發的getRecord/returnRecord/idx_Record。
- 多用途(也就是nextRecord的工作方式):因爲我們開闢的是一塊大內存,使用的是其中一小塊數據。那麼每小塊數據都可以唯一的標記爲:大內存首地址+大內存中的offset。這樣,在64位機器上,我們不需要再使用64位地址,而只需要用較小的位數便可以,從而帶來了極大的好處(當原子操作時,64位中剩下的位數可以用於其他作用)。同時,20位便可以標記(1<<20,百萬多個小塊內存)。
- 支持多種方式實現,只需要提供getRecord/returnRecord/idx_Record接口(下面會給出具體的一種實現),
- 而我們付出的代價僅僅只是一定固定長度的數組。
下面給出一個大概例子,採用的經典的[MSQueue][12],當然,存在其他更高效的方式[lcrq][13]:
int PSLY_Record_IDXNUM = 16;
int PSLY_Record_IDXBIT = ((1 << 16) - 1);
int PSLY_Record_ARRAYNUM_MAX = (1 << 4);
int PSLY_Record_ARRAYNUM = (1 << 2);
int PSLY_Record_ARRAYBITS = ((1 << 4) -1);
int PSLY_Record_ARRBIT = (((1 << 4) - 1) << 16);
int PSLY_Record_ARRBITR = ((1 << 4) - 1);
int PSLY_Record_ARRIDXBIT = ((((1 << 4) - 1) << 16) | ((1 << 16) - 1));
int PSLY_Record_NEXTIDXNUM = 16;
int PSLY_Record_NEXTIDXBIT = ((1 << 16) - 1);
int PSLY_Record_NEXTTAILNUM = 1;
int PSLY_Record_NEXTTAILBIT = (((1 << 1) - 1) << 16);
int PSLY_Record_NEXTVERSIONNUM = (32 - 1 - 16);
int PSLY_Record_NEXTVERSIONBIT = ((~0)^((((1 << 1) - 1) << 16) | ((1 << 16) - 1)));
int PSLY_Record_NEXTVERSIONONE = (1 + ((((1 << 1) - 1) << 16) | ((1 << 16) - 1)));
int PSLY_Record_TAILIDXNUM = 16;
int PSLY_Record_TAILIDXBIT = ((1 << 16) - 1);
int PSLY_Record_TAILVERSIONNUM = (32 - 16);
int PSLY_Record_TAILVERSIONBIT = ((~0) ^ ((1 << 16) - 1));
int PSLY_Record_TAILVERSIONONE = (1 + ((1 << 16) - 1));
int PSLY_Record_HEADIDXNUM = 16;
int PSLY_Record_HEADIDXBIT = ((1 << 16) - 1);
int PSLY_Record_HEADVERSIONNUM = (32 - 16);
int PSLY_Record_HEADVERSIONBIT = ((~0) ^ ((1 << 16) - 1));
int PSLY_Record_HEADVERSIONONE = (1 + ((1 << 16) - 1));
typedef struct Record {
int volatile next __attribute__((aligned(128)));
int self ;
long volatile nextRecord __attribute__((aligned(128)));
void* volatile pointer ;
} Record __attribute__((aligned(128)));
typedef struct RecordQueue {
int volatile head ;
int volatile tail ;
} RecordQueue ;
static Record* volatile psly_Records[1 << 4];
static RecordQueue volatile psly_Record_queues[1 << 4];
static int volatile recordTake = 0;
Record* idx_Record(int index) {
return psly_Records[(index & PSLY_Record_ARRBIT) >> PSLY_Record_IDXNUM] + (index & PSLY_Record_IDXBIT);
}
Record* get_Record() {
for(;;) {
int localArrayNum = PSLY_Record_ARRAYNUM;
//取最高隊列
int array = localArrayNum - 1;
RecordQueue* queue = psly_Record_queues + array;
Record* arr = psly_Records[array];
for(;;){
int headIndex = (queue->head);
int indexHead = headIndex & PSLY_Record_HEADIDXBIT;
Record* head = arr + indexHead;
int tailIndex = (queue->tail);
int indexTail = tailIndex & PSLY_Record_TAILIDXBIT;
int nextIndex = (head->next);
if(headIndex == (queue->head)) {
if(indexHead == indexTail){
if((nextIndex & PSLY_Record_NEXTTAILBIT) == PSLY_Record_NEXTTAILBIT)
break;
__sync_bool_compare_and_swap(&queue->tail, tailIndex, (((tailIndex & PSLY_Record_TAILVERSIONBIT) + PSLY_Record_TAILVERSIONONE ) & PSLY_Record_TAILVERSIONBIT)|(nextIndex & PSLY_Record_TAILIDXBIT));
} else {
if(__sync_bool_compare_and_swap(&queue->head, headIndex, (((headIndex & PSLY_Record_HEADVERSIONBIT) + PSLY_Record_HEADVERSIONONE) & PSLY_Record_HEADVERSIONBIT)|(nextIndex & PSLY_Record_HEADIDXBIT))) {
return head;
}
}
}
}
// 輪詢某些隊列
for(int i = 0; i < localArrayNum; ++i) {
int array = __sync_fetch_and_add(&recordTake, 1) % localArrayNum;
RecordQueue* queue = psly_Record_queues + array;
Record* arr = psly_Records[array];
for(;;){
int headIndex = (queue->head);
int indexHead = headIndex & PSLY_Record_HEADIDXBIT;
Record* head = arr + indexHead;
int tailIndex = (queue->tail);
int indexTail = tailIndex & PSLY_Record_TAILIDXBIT;
int nextIndex = (head->next);
if(headIndex == (queue->head)) {
if(indexHead == indexTail){
if((nextIndex & PSLY_Record_NEXTTAILBIT) == PSLY_Record_NEXTTAILBIT)
break;
__sync_bool_compare_and_swap(&queue->tail, tailIndex, (((tailIndex & PSLY_Record_TAILVERSIONBIT) + PSLY_Record_TAILVERSIONONE ) & PSLY_Record_TAILVERSIONBIT)|(nextIndex & PSLY_Record_TAILIDXBIT));
} else {
if(__sync_bool_compare_and_swap(&queue->head, headIndex, (((headIndex & PSLY_Record_HEADVERSIONBIT) + PSLY_Record_HEADVERSIONONE) & PSLY_Record_HEADVERSIONBIT)|(nextIndex & PSLY_Record_HEADIDXBIT))) {
return head;
}
}
}
}
}
// 遍歷所有隊列
for(int i = 0; i < localArrayNum; ++i) {
int array = i;
RecordQueue* queue = psly_Record_queues + array;
Record* arr = psly_Records[array];
for(;;){
int headIndex = (queue->head);
int indexHead = headIndex & PSLY_Record_HEADIDXBIT;
Record* head = arr + indexHead;
int tailIndex = (queue->tail);
int indexTail = tailIndex & PSLY_Record_TAILIDXBIT;
int nextIndex = (head->next);
if(headIndex == (queue->head)) {
if(indexHead == indexTail){
if((nextIndex & PSLY_Record_NEXTTAILBIT) == PSLY_Record_NEXTTAILBIT)
break;
__sync_bool_compare_and_swap(&queue->tail, tailIndex, (((tailIndex & PSLY_Record_TAILVERSIONBIT) + PSLY_Record_TAILVERSIONONE ) & PSLY_Record_TAILVERSIONBIT)|(nextIndex & PSLY_Record_TAILIDXBIT));
} else {
if(__sync_bool_compare_and_swap(&queue->head, headIndex, (((headIndex & PSLY_Record_HEADVERSIONBIT) + PSLY_Record_HEADVERSIONONE) & PSLY_Record_HEADVERSIONBIT)|(nextIndex & PSLY_Record_HEADIDXBIT))) {
return head;
}
}
}
}
}
//不夠增加
if(localArrayNum == PSLY_Record_ARRAYNUM_MAX)
return NULL;
if(localArrayNum == PSLY_Record_ARRAYNUM) {
if(psly_Records[localArrayNum] == NULL) {
int array_ = localArrayNum;
Record* record;
void * ptr;
int ret = posix_memalign(&ptr, 4096, (1 << PSLY_Record_IDXNUM) * sizeof(Record));
record = ptr;
memset(record, 0, (1 << PSLY_Record_IDXNUM) * sizeof(Record));
for(int j = 0; j < (1 << PSLY_Record_IDXNUM) - 1; ++j){
record->self = (array_ << PSLY_Record_IDXNUM) | j;
record->next = j+1;
record->pointer = NULL;\
record->nextRecord = 0;
record += 1;
}
record->self = (array_ << PSLY_Record_IDXNUM) | ((1 << PSLY_Record_IDXNUM) - 1);
record->next = PSLY_Record_NEXTTAILBIT;
record->pointer = NULL;
record->nextRecord = 0;
//printf("I'm here %d %ld\n", localArrayNum, pthread_self());
if(!__sync_bool_compare_and_swap(&psly_Records[array_], NULL, ptr))
{free(ptr);}
else
/*printf("extend to %d\n", localArrayNum + 1)*/;
}
if(localArrayNum == PSLY_Record_ARRAYNUM)
__sync_bool_compare_and_swap(&PSLY_Record_ARRAYNUM, localArrayNum, localArrayNum + 1);
}
}
}
void return_Record(Record* record) {
long local = (record->next);
local |= PSLY_Record_NEXTTAILBIT;
record->next = local;
int self = record->self;
int array = (self >> PSLY_Record_IDXNUM) & PSLY_Record_ARRBITR;
Record* arr = psly_Records[array];
RecordQueue* queue = psly_Record_queues + array;
for(;;) {
int tailIndex = (queue->tail);
int indexTail = tailIndex & PSLY_Record_TAILIDXBIT;
Record* tail = arr + indexTail;
int nextIndex = (tail->next);
if(tailIndex == (queue->tail)){
if((nextIndex & PSLY_Record_NEXTTAILBIT) == PSLY_Record_NEXTTAILBIT) {
if(__sync_bool_compare_and_swap(&tail->next, nextIndex, (((nextIndex & PSLY_Record_NEXTVERSIONBIT) + PSLY_Record_NEXTVERSIONONE) & PSLY_Record_NEXTVERSIONBIT)|(self & PSLY_Record_NEXTIDXBIT))){
__sync_bool_compare_and_swap(&queue->tail, tailIndex, (((tailIndex & PSLY_Record_TAILVERSIONBIT) + PSLY_Record_TAILVERSIONONE) & PSLY_Record_TAILVERSIONBIT)|(self & PSLY_Record_TAILIDXBIT));
return;
}
} else {
__sync_bool_compare_and_swap(&queue->tail, tailIndex, (((tailIndex & PSLY_Record_TAILVERSIONBIT) + PSLY_Record_TAILVERSIONONE) & PSLY_Record_TAILVERSIONBIT)|(nextIndex & PSLY_Record_TAILIDXBIT));
}
}
}
}
typedef struct RecordList {
Record* volatile head ;
Record* volatile tail ;
} RecordList ;
typedef struct RecordMap {
volatile RecordList* lists[131070] ;
} RecordMap ;
static volatile RecordMap map;
#define INIT_RESOURCE(listNum) \
for(int i = 0; i < (PSLY_Record_ARRAYNUM); ++i){ \
Record* record; \
void * ptr;\
int ret = posix_memalign(&ptr, 4096, (1 << PSLY_Record_IDXNUM) * sizeof(Record));\
psly_Records[i] = record = ptr; \
memset(record, 0, (1 << PSLY_Record_IDXNUM) * sizeof(Record)); \
for(int j = 0; j < (1 << PSLY_Record_IDXNUM) - 1; ++j){ \
record->self = (i << PSLY_Record_IDXNUM) | j; \
record->next = j+1; \
record->pointer = NULL;\
record->nextRecord = 0;\
record += 1; \
} \
record->self = (i << PSLY_Record_IDXNUM) | ((1 << PSLY_Record_IDXNUM) - 1); \
record->next = PSLY_Record_NEXTTAILBIT; \
record->pointer = NULL;\
record->nextRecord = 0;\
}\
for(int i = 0; i < PSLY_Record_ARRAYNUM_MAX; ++i){\
psly_Record_queues[i].head = 0; \
psly_Record_queues[i].tail = (1 << PSLY_Record_IDXNUM) - 1; \
} \
for(int i = 0; i < listNum; ++i) { \
void* ptr;\
int ret = posix_memalign(&ptr, 4096, sizeof(RecordList));\
Record* head = get_Record();\
Record* tail = get_Record();\
head->nextRecord = newNext(head->nextRecord, tail); \
map.lists[i] = ptr;\
map.lists[i]->head = head; \
map.lists[i]->tail = tail; \
}
#define UNINIT_RESOURCE(listNum) \
for(int i = 0; i < (PSLY_Record_ARRAYNUM); ++i){ \
free(psly_Records[i]); \
} \
for(int i = 0; i < listNum; ++i) {\
free(map.lists[i]);\
}
這裏的psly_Records[1 << 4],psly_Record_queues[1 << 4] 代表總共可以提供16組,每組65536個數據(PSLY_Record_IDXNUM = 16),初始分配4組數據(PSLY_Record_ARRAYNUM = (1 << 2)),之後如果不夠就擴充一組。
**不斷的優化**
----
**局部變量**:
對於一塊共享內存S,我們爲它的Record保持一個局部變量reordS,只需要在RECORD時候返回,後續的REMOVE跟RETIRE就不需要去查詢了,相對於之前的enqueue給出的例子如下:
+++爲變動代碼
void enqueue(int value){
NodeType* node;
posix_memalign(&node, 64, sizeof(NodeType));
memset(node, 0, sizeof(NodeType));
node->value = value;
node->next = NULL;
NodeType* t;
for(;;){
t = Tail;
+++ Record* recordT = psly_record(&Tail, t);
+++ if(recordT == NULL)
+++ continue;
if(Tail != t) {
+++ psly_remove(recordT);
continue;
}
NodeType* next = t->next;
if(Tail != t) {
+++ psly_remove(recordT);
continue;
}
if(next != NULL){
+++ psly_remove(recordT);
__sync_bool_compare_and_swap(&Tail, t, next);
continue;
}
if(__sync_bool_compare_and_swap(&t->next, NULL, node)) {
+++ psly_remove(recordT);
break;
}
+++ psly_remove(recordT);
}
__sync_bool_compare_and_swap(&Tail, t, node);
}
**這樣以來,每次操作,最需要在RECORD時候遍歷一次。**
我們還可以做的更好
**線程私有數據**:
有些場景的共享內存,會在一段長期時間內不會改變,這種情況的話每次都去查詢maplist顯得很浪費,我們可以做個緩存來節省查詢。
對於一塊確定的共享內存S,我們嘗試爲每線程配置一個私有變量(static __tread),用一段結構化的代碼跟蹤它的Record,這樣以來就不需要每次都查詢maplist了,
雖然不是非常合適,但我們還是拿前面Hazard pointer的例子做個示例,代碼如下:
void enqueue(int value){
NodeType* node;
posix_memalign(&node, 64, sizeof(NodeType));
memset(node, 0, sizeof(NodeType));
node->value = value;
node->next = NULL;
NodeType* t;
for(;;){
t = Tail;
+++ static __thread LocalRecord localRecordT;
Record* recordT;
+++ if(localRecordT.pointer == NULL) {
+++ recordT = psly_record(&Tail, t, NULL, NULL);
if(recordT == NULL)
continue;
+++ else {
+++ localRecordT.pointer = t;
+++ localRecordT.record = recordT;
+++ localRecordT.nextRecord = recordT->nextRecord;
+++ }
+++ } else {
+++ if(t != localRecordT.pointer || isChange(localRecordT.record, localRecordT.nextRecord) || t != localRecordT.record->pointer) {
+++ localRecordT.pointer = NULL;
+++ continue;
+++ }
+++ recordT = psly_record(&Tail, t, localRecordT.record, localRecordT.nextRecord);
+++ if(recordT == NULL) {
+++ localRecordT.pointer = NULL;
+++ continue;
+++ }
+++ }
if(Tail != t) {
psly_remove(recordT);
continue;
}
NodeType* next = t->next;
if(Tail != t) {
psly_remove(recordT);
continue;
}
if(next != NULL){
psly_remove(recordT);
__sync_bool_compare_and_swap(&Tail, t, next);
continue;
}
if(__sync_bool_compare_and_swap(&t->next, NULL, node)) {
psly_remove(recordT);
break;
}
psly_remove(recordT);
}
__sync_bool_compare_and_swap(&Tail, t, node);
}
這種場景下,假如我們系統共有N個線程,那麼對於一個內存,在它的整個生命週期裏,需要查詢maplist的次數上限爲N+1次!
這種方式極大地減少了查詢的次數,從而爲設計某些高效的共享數據結構提供了可能。
**鏈表遍歷保留前驅**:
當我們查詢maplist時候,有可能會因爲前驅節點的失效,而要重新在該list的head開始遍歷,假如鏈表過長會代價較大,所以我們在遍歷過程中維護些前驅節點可能會好點。
示例代碼如下:
保留:
if(currKey != key) {
int bucket;
if((steps & STEPS_) == 0 && (bucket = (steps >> STEPBIT)) < MAXPREV) {
Prevs* step = &prevs_[bucket];
step->r = prev;
step->rNext = prevNext;
}
++steps;
prev = curr;
prevNext = currNext;
}
失效之後啓用保留的前驅: --steps;
for(;;) {
int bucket = steps >> STEPBIT;
bucket = bucket < MAXPREV ? bucket: (MAXPREV - 1);
Prevs* prevs = &prevs_[bucket];
prev = prevs->r;
prevNext = prev->nextRecord;
long prevNextKeep = prevs->rNext;
if((prevNextKeep & NODEBITS) != (prevNext & NODEBITS) || (prevNext & REFCBITS) == DELETED) {
steps -= STEPS;
} else {
prevs->rNext = prevNext;
curr = idx_Record(prevNext);
break;
}
}
steps = steps & (~STEPS_);
**這裏的steps記錄我們目前所在的位置,STEPS表達我們隔幾個節點記錄一次(極端情況下可以拷貝所有遍歷過的節點)。**
批量獲取Record
----------
批量獲取Record方式指的是,由於獲取Record的競爭過於激烈,我們不再每次獲取一個,而是每次獲取一批,剩餘的作爲線程私有之後使用,維護好數據的 未使用/使用 狀態,以及作爲整體返回給資源庫。從而極大地減少了線程間的競爭。
統一回收的大塊數據
---------
對於某些場景,許多小塊的共享數據同時產生,又可以同時回收。我們不再爲每個內存地址分配一個Record,我們嘗試將一塊大內存的分割爲許多小內存來使用,如此一來小內存統一映射到大內存首地址的Record,直接省去了插入鏈表的操作。我需要對Record進行改造,retireBit不再作爲一個元素使用,這裏可以換成short
struct Record{
//Record庫維護字段
int volatile next ;
int self;
//數據字段
void* pointer;
int refcount;
//retire
+++ short retireNum;
long nextRecord;
}
同時,我們的psy_record接口增加一個參數retireNum:
recordT = psly_record(void** ppointer, void* pointer, Record* record, long nextRecord, short retireNum);
1. 我們對於某個內存pointer,我們映射首地址的方式要依賴於如何分配內存。
2. retire操作現在要給retireNum減一。最後講一下,唯一需要注意的是,如果內存已經處於可回收狀態:
1. 所有分片內存都已經retire。
2. 觀察到一次refcount爲0.
那麼我們便可以立即回收內部,因爲對於企圖使用該內存的線程而言,要麼正在遞增引用計數,要麼已經完成訪問。完成訪問的沒關係,根據我們的設計,遞增引用計數的線程稍後會回退減一,從而不再訪問這塊內存。
帶參數的retire
----
假如我們在開發一個併發數據結構,它本身將會被共享/動態開闢/回收,數據本身帶有指針,指針指向的數據隨着程序的執行變得不滿足需求,從而我們要重新配置這一數據,並且回收原數據。
**這種情況下我們能不能正確回收所有數據呢?**
答案是可以的。
對於搜優內存,如果滿足
> 共享的 / 需要回收的 / 沒有明確的可以安全回收內存的邏輯點
**我們都採用3RE&S Protocol提供的語義來回收內存。**
因爲
1. 只要採用這種方式,內部內存同樣能夠被record/remove/retire/scan維護好,而新開闢的內存將正確地作爲內部內存。
2. 任何時候,數據結構本身都是完整的。同時,當整體可被回收時,不會存在線程讀寫內部數據。安全性得到保證
**最後,我們必須自己提供freeMemory函數用於先回收內部內存,再回收外部對象的內存。**
[1]: /img/bVYqVm
[2]: https://erdani.com/publications/cuj-2004-12.pdf
[3]: https://github.com/pramalhe/ConcurrencyFreaks/blob/master/papers/hazarderas-2017.pdf
[4]: http://web.cecs.pdx.edu/~walpole/class/cs510/fall2011/slides/07.pdf
[5]: https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-579.pdf
[6]: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.135.9418&rep=rep1&type=pdf
[7]: http://blog.jobbole.com/107955/
[8]: http://www.non-blocking.com/download/GidPST05_LockFreeGC_TR.pdf
[9]: /img/bVYBoJ
[10]: /img/bVYBqU
[11]: https://www.microsoft.com/en-us/research/wp-content/uploads/2001/10/2001-disc.pdf
[12]: http://www.cs.rochester.edu/~scott/papers/1996_PODC_queues.pdf
[13]: http://www.cs.tau.ac.il/~mad/publications/ppopp2013-x86queues.pdf