轉自 http://home.eeworld.com.cn/my/space-uid-346593-blogid-239256.html
圓形緩沖區(circular buffer),也稱作圓形隊列(circular queue),循環緩沖區(cyclic buffer),環形緩沖區(ring buffer),是一種數據結構用於表示一個固定尺寸、頭尾相連的緩沖區,適合緩存數據流。
目錄
用法
圓形緩沖區的一個有用特性是:當一個數據元素被用掉后,其余數據元素不需要移動其存儲位置。相反,一個非圓形緩沖區(例如一個普通的隊列)在用掉一個數據元素后,其余數據元素需要向前搬移。換句話說,圓形緩沖區適合實現先進先出緩沖區,而非圓形緩沖區適合后進先出緩沖區。
圓形緩沖區適合於事先明確了緩沖區的最大容量的情形。擴展一個圓形緩沖區的容量,需要搬移其中的數據。因此一個緩沖區如果需要經常調整其容量,用鏈表實現更為合適。
寫操作覆蓋圓形緩沖區中未被處理的數據在某些情況下是允許的。特別是在多媒體處理時。例如,音頻的生產者可以覆蓋掉聲卡尚未來得及處理的音頻數據。
工作過程
一個圓形緩沖區最初為空並有預定的長度。例如,這是一個具有七個元素空間的圓形緩沖區,其中底部的單線與箭頭表示“頭尾相接”形成一個圓形地址空間:
假定1被寫入緩沖區中部(對於圓形緩沖區來說,最初的寫入位置在哪里是無關緊要的):
再寫入2個元素,分別是2 & 3 — 被追加在1之后:
如果兩個元素被處理,那么是緩沖區中最老的兩個元素被卸載。在本例中,1 & 2被卸載,緩沖區中只剩下3:
如果緩沖區中有7個元素,則是滿的:
如果緩沖區是滿的,又要寫入新的數據,一種策略是覆蓋掉最老的數據。此例中,2個新數據— A & B — 寫入,覆蓋了3 & 4:
也可以采取其他策略,禁止覆蓋緩沖區的數據,采取返回一個錯誤碼或者拋出異常。
最終,如果從緩沖區中卸載2個數據,不是3 & 4 而是 5 & 6 。因為 A & B 已經覆蓋了3 & 4:
圓形緩沖區工作機制
由於計算機內存是線性地址空間,因此圓形緩沖區需要特別的設計才可以從邏輯上實現。
讀指針與寫指針
一般的,圓形緩沖區需要4個指針:
- 在內存中實際開始位置;
- 在內存中實際結束位置,也可以用緩沖區長度代替;
- 存儲在緩沖區中的有效數據的開始位置(讀指針);
- 存儲在緩沖區中的有效數據的結尾位置(寫指針)。
讀指針、寫指針可以用整型值來表示。
下例為一個未滿的緩沖區的讀寫指針:
下例為一個滿的緩沖區的讀寫指針:
區分緩沖區滿或者空
緩沖區是滿、或是空,都有可能出現讀指針與寫指針指向同一位置:
有多種策略用於檢測緩沖區是滿、或是空.
總是保持一個存儲單元為空
緩沖區中總是有一個存儲單元保持未使用狀態。緩沖區最多存入個數據。如果讀寫指針指向同一位置,則緩沖區為空。如果寫指針位於讀指針的相鄰后一個位置,則緩沖區為滿。這種策略的優點是簡單、魯棒;缺點是語義上實際可存數據量與緩沖區容量不一致,測試緩沖區是否滿需要做取余數計算。
使用數據計數
這種策略不使用顯式的寫指針,而是保持着緩沖區內存儲的數據的計數。因此測試緩沖區是空是滿非常簡單;對性能影響可以忽略。缺點是讀寫操作都需要修改這個存儲數據計數,對於多線程訪問緩沖區需要並發控制。
鏡像指示位
緩沖區的長度如果是n,邏輯地址空間則為0至n-1;那么,規定n至2n-1為鏡像邏輯地址空間。本策略規定讀寫指針的地址空間為0至2n-1,其 中低半部分對應於常規的邏輯地址空間,高半部分對應於鏡像邏輯地址空間。當指針值大於等於2n時,使其折返(wrapped)到ptr-2n。使用一位表 示寫指針或讀指針是否進入了虛擬的鏡像存儲區:置位表示進入,不置位表示沒進入還在基本存儲區。
在讀寫指針的值相同情況下,如果二者的指示位相同,說明緩沖區為空;如果二者的指示位不同,說明緩沖區為滿。這種方法優點是測試緩沖區滿/空很簡 單;不需要做取余數操作;讀寫線程可以分別設計專用算法策略,能實現精致的並發控制。 缺點是讀寫指針各需要額外的一位作為指示位。
如果緩沖區長度是2的冪,則本方法可以省略鏡像指示位。如果讀寫指針的值相等,則緩沖區為空;如果讀寫指針相差n,則緩沖區為滿,這可以用條件表達式(寫指針 == (讀指針 異或 緩沖區長度))來判斷。
/* This approach adds one bit to end and start pointers */
/* Circular buffer object */
typedef struct { int size; /* maximum number of elements */ int start; /* index of oldest element */ int end; /* index at which to write new element */ ElemType *elems; /* vector of elements */ } CircularBuffer; void cbInit(CircularBuffer *cb, int size) { cb->size = size; cb->start = 0; cb->end = 0; cb->elems = (ElemType *)calloc(cb->size, sizeof(ElemType)); } void cbPrint(CircularBuffer *cb) { printf("size=0x%x, start=%d, end=%d\n", cb->size, cb->start, cb->end); } int cbIsFull(CircularBuffer *cb) { return cb->end == (cb->start ^ cb->size); /* This inverts the most significant bit of start before comparison */ } int cbIsEmpty(CircularBuffer *cb) { return cb->end == cb->start; } int cbIncr(CircularBuffer *cb, int p) { return (p + 1)&(2*cb->size-1); /* start and end pointers incrementation is done modulo 2*size */ } void cbWrite(CircularBuffer *cb, ElemType *elem) { cb->elems[cb->end&(cb->size-1)] = *elem; if (cbIsFull(cb)) /* full, overwrite moves start pointer */ cb->start = cbIncr(cb, cb->start); cb->end = cbIncr(cb, cb->end); } void cbRead(CircularBuffer *cb, ElemType *elem) { *elem = cb->elems[cb->start&(cb->size-1)]; cb->start = cbIncr(cb, cb->start); }
讀/寫 計數
用兩個有符號整型變量分別保存寫入、讀出緩沖區的數據數量。其差值就是緩沖區中尚未被處理的有效數據的數量。這種方法的優點是讀線程、寫線程互不干擾;缺點是需要額外兩個變量。
記錄最后的操作
使用一位記錄最后一次操作是讀還是寫。讀寫指針值相等情況下,如果最后一次操作為寫入,那么緩沖區是滿的;如果最后一次操作為讀出,那么緩沖區是空。 這種策略的缺點是讀寫操作共享一個標志位,多線程時需要並發控制。
POSIX優化實現