無鎖隊列的實現

來源:互聯網
上載者:User

http://coolshell.cn/articles/8239.html

無鎖隊列的鏈表實現

EnQueue(x) //進隊列{    //準備新加入的結點資料    q = new record();    q->value = x;    q->next = NULL;     do {        p = tail; //取鏈表尾指標的快照    } while( CAS(p->next, NULL, q) != TRUE); //如果沒有把結點鏈在尾指標上,再試     CAS(tail, p, q); //置尾結點}

我們可以看到,程式中的那個 do- while 的 Re-Try-Loop。就是說,很有可能我在準備在隊列尾加入結點時,別的線程已經加成功了,於是tail指標就變了,於是我的CAS返回了false,於是程式再試,直到試成功為止。這個很像我們的搶電話熱線的不停重播的情況。

你會看到,為什麼我們的“置尾結點”的操作(第12行)不判斷是否成功,因為:

  1. 如果有一個線程T1,它的while中的CAS如果成功的話,那麼其它所有的 隨後線程的CAS都會失敗,然後就會再迴圈,
  2. 此時,如果T1 線程還沒有更新tail指標,其它的線程繼續失敗,因為tail->next不是NULL了。
  3. 直到T1線程更新完tail指標,於是其它的線程中的某個線程就可以得到新的tail指標,繼續往下走了。

這裡有一個潛在的問題——如果T1線程在用CAS更新tail指標的之前,線程停掉或是掛掉了,那麼其它線程就進入死迴圈了。下面是改良版的EnQueue()

EnQueue(x) //進隊列改良版{    q = new record();    q->value = x;    q->next = NULL;     p = tail;    oldp = p    do {        while (p->next != NULL)            p = p->next;    } while( CAS(p.next, NULL, q) != TRUE); //如果沒有把結點鏈在尾上,再試     CAS(tail, oldp, q); //置尾結點}

我們讓每個線程,自己fetch 指標 p 到鏈表尾。但是這樣的fetch會很影響效能。而通實際情況看下來,99.9%的情況不會有線程停轉的情況,所以,更好的做法是,你可以接合上述的這兩個版本,如果retry的次數超了一個值的話(比如說3次),那麼,就自己fetch指標。

好了,我們解決了EnQueue,我們再來看看DeQueue的代碼:(很簡單,我就不解釋了)

DeQueue() //出隊列{    do{        p = head;        if (p->next == NULL){            return ERR_EMPTY_QUEUE;        }    while( CAS(head, p, p->next) != TRUE );    return p->next->value;}

我們可以看到,DeQueue的代碼操作的是 head->next,而不是head本身。這樣考慮是因為一個邊界條件,我們需要一個dummy的頭指標來解決鏈表中如果只有一個元素,head和tail都指向同一個結點的問題,這樣EnQueue和DeQueue要互斥了

CAS的ABA問題

所謂ABA(見維基百科的ABA詞條),問題基本是這個樣子:

  1. 進程P1在共用變數中讀到值為A
  2. P1被搶佔了,進程P2執行
  3. P2把共用變數裡的值從A改成了B,再改回到A,此時被P1搶佔。
  4. P1回來看到共用變數裡的值沒有被改變,於是繼續執行。

雖然P1以為變數值沒有改變,繼續執行了,但是這個會引發一些潛在的問題。ABA問題最容易發生在lock free 的演算法中的,CAS首當其衝,因為CAS判斷的是指標的地址。如果這個地址被重用了呢,問題就很大了。(地址被重用是很經常發生的,一個記憶體配置後釋放了,再分配,很有可能還是原來的地址)

比如上述的DeQueue()函數,因為我們要讓head和tail分開,所以我們引入了一個dummy指標給head,當我們做CAS的之前,如果head的那塊記憶體被回收並被重用了,而重用的記憶體又被EnQueue()進來了,這會有很大的問題。(記憶體管理中重用記憶體基本上是一種很常見的行為

這個例子你可能沒有看懂,維基百科上給了一個活生生的例子——

你拿著一個裝滿錢的手提箱在飛機場,此時過來了一個火辣性感的美女,然後她很暖昧地挑逗著你,並趁你不注意的時候,把用一個一模一樣的手提箱和你那裝滿錢的箱子調了個包,然後就離開了,你看到你的手提箱還在那,於是就提著手提箱去趕飛機去了。

這就是ABA的問題。

解決ABA的問題

維基百科上給了一個解——使用double-CAS(雙保險的CAS),例如,在32位系統上,我們要檢查64位的內容

1)一次用CAS檢查雙倍長度的值,前半部是指標,後半部分是一個計數器。

2)只有這兩個都一樣,才算通過檢查,要吧賦新的值。並把計數器累加1。

這樣一來,ABA發生時,雖然值一樣,但是計數器就不一樣(但是在32位的系統上,這個計數器會溢出回來又從1開始的,這還是會有ABA的問題)

當然,我們這個隊列的問題就是不想讓那個記憶體重用,這樣明確的業務問題比較好解決,論文《Implementing Lock-Free Queues》給出一這麼一個方法——使用結點記憶體引用計數refcnt

SafeRead(q){    loop:        p = q->next;        if (p == NULL){            return p;        }         Fetch&Add(p->refcnt, 1);         if (p == q->next){            return p;        }else{            Release(p);        }    goto loop;}

其中的 Fetch&Add和Release分是是加引用計數和減引用計數,都是原子操作,這樣就可以阻止記憶體被回收了。

用數組實現無鎖隊列

本實現來自論文《Implementing Lock-Free Queues》

使用數組來實現隊列是很常見的方法,因為沒有記憶體的分部和釋放,一切都會變得簡單,實現的思路如下:

1)數組隊列應該是一個ring buffer形式的數組(環形數組)

2)數組的元素應該有三個可能的值:HEAD,TAIL,EMPTY(當然,還有實際的資料)

3)數組一開始全部初始化成EMPTY,有兩個相鄰的元素要初始化成HEAD和TAIL,這代表空隊列。

4)EnQueue操作。假設資料x要入隊列,定位TAIL的位置,使用double-CAS方法把(TAIL, EMPTY) 更新成 (x, TAIL)。需要注意,如果找不到(TAIL, EMPTY),則說明隊列滿了。

5)DeQueue操作。定位HEAD的位置,把(HEAD, x)更新成(EMPTY, HEAD),並把x返回。同樣需要注意,如果x是TAIL,則說明隊列為空白。

演算法的一個關鍵是——如何定位HEAD或TAIL?

1)我們可以聲明兩個計數器,一個用來計數EnQueue的次數,一個用來計數DeQueue的次數。

2)這兩個計算機使用使用Fetch&ADD來進行原子累加,在EnQueue或DeQueue完成的時候累加就好了。

3)累加後求個模什麼的就可以知道TAIL和HEAD的位置了。

如所示:

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.