作者:曾志優
出處: http://www.cnblogs.com/zengzy
1、環形緩沖區
緩沖區的好處,就是空間換時間和協調快慢線程。緩沖區可以用很多設計法,這里說一下環形緩沖區的幾種設計方案,可以看成是幾種環形緩沖區的模式。設 計環形緩沖區涉及到幾個點,一是超出緩沖區大小的的索引如何處理,二是如何表示緩沖區滿和緩沖區空,三是如何入隊、出隊,四是緩沖區中數據長度如何計算。
ps.規定以下所有方案,在緩沖區滿時不可再寫入數據,緩沖區空時不能讀數據
1.1、常規數組環形緩沖區
設緩沖區大小為N,隊頭out,隊尾in,out、in均是下標表示:
- 初始時,in=out=0
- 隊頭隊尾的更新用取模操作,out=(out+1)%N,in=(in+1)%N
- out==in表示緩沖區空,(in+1)%N==out表示緩沖區滿
- 入隊que[in]=value;in=(in+1)%N;
- 出隊ret =que[out];out=(out+1)%N;
- 數據長度 len =( in - out + N) % N
1.2、改進版數組環形緩沖區
同樣假設緩沖區大小為N,隊頭out,隊尾in,out、in為數組下標,但數據類型為unsigned int。
- 初始時,in=out=0
- 上調緩沖區大小N為2的冪,假設為M
- 隊頭隊尾更新不再取模,直接++out,++in
- out==in表示緩沖區空,(in-out)==M表示緩沖區滿
- 入隊que[in&(M-1)] = value ; ++in;
- 出隊ret = que[out&(M-1)] ; ++out;
- in-out表示數據長度
這個改進的思想來自linux內核循環隊列kfifo,這里解釋一下幾個行為的含義及原理
⑴上調緩沖區大小至2的冪
這是方便取模,x%M == x&(M-1) 為真,位運算的效率比取模要高。用一個例子來分析一下為什么等式成立的:
假設M=8=2³,那么M-1=7,二進制為0000 0111
①若 x<8 ----> x&7=x , x%8 = x,等式成立
②若 x>8 ----> x = 2^a+2^b+2^c+... 比如,51 = 1+2+16+32 = 2^0+2^1+2^4+2^5 ,求 51&7時,由於7的二進制0000 0111,所以2的冪只要大於等於2³的數,與上7結果都是0,所以2^4 & 7 = 0 , 2^5 & 7 = 0, (2^0+2^1+2^4+2^5) & (7) = 2^0+2^1=3。而根據①,(2^0+2^1)&7 = (2^0+2^1)%8 ,所以51&7=51%8
綜上得證。
⑵out、in類型設計為unsigned int
無符號整形的溢出之后,又從0開始計數:MAX_UNSIGNED_INT + 1 = 0 ,MAX_UNSIGNED_INT + 2 = 1 ,... 。
in、out溢出之前,都能通過&把in、out映射到正確的位置上,那溢出之后呢?可以舉個例子來:
假設現在in=MAX_UNSIGNED_INT,那么in & (M-1) = M-1 ,也就是最后一個位置,再入隊時,應該從頭開始入隊,也就是0,而in+1也為0,所以即使溢出了,(in+1)&(M-1)仍然能映射到正確的 位置。這就是為什么我們入隊出隊只要做個與映射和++操作就能保證正確的原因。
而,根據入隊和出隊的操作,隊列中的元素總是維持在[out,in)這個區間中,由於溢出可能存在,這個區間有三種情況:
- out沒溢出,in沒溢出,in-out就是這個緩沖區中數據的長度。
- out沒溢出,in溢出,此時數據長度應該是MAX_UNSIGNED_INT - out +1 + in = in - out + MAX_UNSIGNED_INT +1 = in-out。
- out溢出,in溢出,此時數據長度也是in-out。
根據上面三種情況,in-out總是表示環形隊列中數據的長度
不得不驚嘆,linux內核中的kfifo實現實在是太精妙了。相比前面的版本,所有的取余操作都改成了與運算,入隊出隊,求緩沖區數據長度都變得非常簡單。
1.3、鏈表實現的環形緩沖區
環形緩沖區的鏈表實現比數組實現要簡單一些,可以用下圖的這種設計方案:
假設要求環形緩沖區大小為N
- 隊列長度:可以設計一個size的成員,每次O(1)取size,也可以O(N)遍歷隊列求size
- 隊列空:head->next == NULL
- 隊列滿:size == N
- 出隊核心
ret = out;
out = out->next;
head->next = out; - 入隊核心new_node表示新申請的結點
new_node->next = in->next;
in->next = in_node;
++size;
當然,鏈表結點的設定是自由的,鏈表結點本身可以內含數組、鏈表、哈希表等等,例如下面這樣,內含一個數組
這時,可以增設兩個變量out_pos,in_pos。假設結點內數組的大小為N_ELEMS,整個鏈表結點的數量為node_nums
- 隊列長度:(nodes_nums-2)*N+N-out_pos+in_pos
- 隊列空:head->next == NULL
- 隊列滿:隊列長度 == N
- 出隊核心
out_pos == N_ELEMS;
delete_node = out;
free(delete_node);
out = out->next;
out_pos = 0;
head->next = out;
ret = out[out_pos++]; - 入隊核心,new_node表示新申請的核心
in_pos == N_ELEMS;
new_node->next = in->next;
in = new_node;
in_pos = 0;
in[in_pos++] = value;
1.4、改進鏈表環形緩沖區
上面鏈表環形隊列出隊列可能釋放內存,入隊列可能申請內存,所以,可以用個空閑鏈表把該釋放的內存管理起來,入隊列時,如果要增加結點,先從空閑鏈表中取結點,取不到再去申請內存,這樣就可以避免多次分配釋放內存了,至於其他的操作都是一樣的。
上邊只是簡單的說了下入隊出隊等操作,事實上,緩沖區往往是和讀寫線程伴隨出現 的,緩沖區中的每一個資源,對於同類線程可能需要互斥訪問,也可能可以共享使用,而不同類線程間(讀寫線程)往往需要做同步操作。比如,讀線程之間可能共 享緩沖區的每一個資源,也可能互斥使用每個資源,通常,在緩沖區滿時寫線程不能寫,緩沖區空時讀線程不能讀,也就是讀寫線程要求同步。這其實就是操作系統 課程上PV操作的幾個經典模式,如果讀讀之間、寫寫之間要求互斥使用資源,並且讀寫線程間不要求互斥,就是生產者消費者問題,如果讀讀之間不要求互斥(每 個資源可供多個讀線程共同使用),寫寫之間要求互斥(每個資源僅供一個寫線程使用),並且讀寫線程也要求互斥(讀的時候不能寫,寫的時候不能讀),就是讀 寫者問題。
下面會以生產者消費者模式和1.2節改進版的循環緩沖區為例,來說說並發循環隊列有鎖實現,下一篇說無鎖實現。關於讀寫者的問題,以后有時間再詳談。
2、生產者消費者
先提一嘴生產者消費者的優點吧
- 並發,若緩沖區中數據處理方式一致,可以開多個線程或進程處理數據或生產數據
- 異步,生產者無需干等着消費者消費數據,消費者也無需干等着生產者生產數據,只需根據緩沖區的狀態做出相應反應,如果結合io多用復用技術,也就是所謂的反應器模式,可以設計很好的異步通信架構,像zeromq底層的線程通信就是使用這種方案來做的。
- 解耦,解耦可以說是一個附帶作用,由於生產者和消費者無直接關聯,也就是生產者中不會去調用任何消費者的方法或者反過來,所以任何一方的變動不影響另一方。
- 緩沖,主要是保持各自的性能,比如生產者很快,那沒關系,消費者雖然消費不過來,但可以把數據放緩沖區里。
現在正式開工,根據生產者和消費者的數量,可以把生產者消費者划分為四種類型,1:1,1:N,M:1,M:N。
然后再做個規定,規定環形緩沖區的大小為M,M為2的冪次方,in、out統一稱為slot。
2.1、單生產者單消費者
一個生產者,一個消費者,緩沖區可用資源數為M。
這種情況只要同步生產者和消費者,同步的方法是用兩個信號量available_in_slots,available_out_slots分別表示生產者有多個可用資源、消費者有多個可用資源, 每生產一個產品,生產者可用資源減1,消費者可用資源加1,這點可用PV操作來實現,用P操作可以消耗1個資源,P操作結束資源數減1,V操作可以生產1 個資源,V操作結束后資源數加1。初始時,available_in_slots=M,表示生產者有M個空間可放產 品,available_out_slots=0,表示消費者還沒有可用資源:
available_in_slots = M; available_out_slots = 0; in=out=0; void producer() { while(true){ P(available_in_slots); queue[(in++)&(M-1)] = data; V(available_out_slots) } } void consumer() { while(true){ P(available_out_slots); queue[(out++)&(M-1)] = data; V(available_in_slots) } }
2.2、單生產者多消費者
一個生產者,多個消費者,緩沖區可用資源數位M。
這種情況下,消費者有多個,消費者之間對out slot要互斥訪問,用out_slot_mutex來實現消費者間的互斥,拿到out_slot_mutex的消費者線程才得以繼續執行,沒拿到的只能 阻塞。生產者消費者要同步,用available_in_slots,available_out_slots來實現生產者消費者的同步。
詳細內容看原文連接。