LinkBlockedQueue的c++實現

封fenghl發表於2020-10-25

c++連結串列實現的阻塞佇列

最近從java原始碼裡發現了阻塞佇列的實現,覺得非常有趣。

首先,介紹下什麼是阻塞佇列。阻塞佇列代表著一個佇列可以執行緒安全的往該佇列中寫資料和從該佇列中讀資料。也就是說,我們可以在多個執行緒之間併發的進行寫資料和讀資料,而不會引發任何併發問題。

下面我們就說說如何實現一個阻塞佇列。

而實現一個阻塞佇列的前提:

  1. 需要能夠使用連結串列實現一個佇列
  2. 能夠使用c++的鎖機制,去給佇列的寫和讀操作加鎖。

為了效能,這裡的讀和寫的鎖不能是同一把鎖。而對於一個連結串列佇列來說,讀取操作肯定需要涉及頭指標,寫操作肯定涉及尾指標。既然要實現讀操作一把鎖和寫操作一把鎖。那麼就要求讀操作只能更改頭指標而不能更改尾指標,寫操作只能更改尾指標而不能更改頭指標。不滿足這個要求,那麼讀寫操作就不可能實現用兩把鎖分別對讀寫進行加鎖。

基本佇列的實現

現在我們先說說如何實現這個佇列。

要求:入隊操作(enqueue)只能操作尾指標(last), 出隊操作(dequeue)只能操作頭指標(head)。

對於佇列的初始化,這裡不能單純的設定為空指標,需要將頭尾指標同一節點。

下面我們來看入隊操作如何實現

從這個入隊操作來看,該操作只更改了尾指標last, 而沒有更改頭指標head。

其程式碼實現為:

    void enqueue(int item) {
        Node *node = new Node(item);
        last = last->next = node;
    }

接下來我們來看出隊操作如何實現

從出隊操作來看就有趣的多,它拋棄了head所指向的節點,而這個節點有可能是那些節點呢?

  1. 初始化時所賦值的那個節點
  2. 出隊後的節點

也就是說,head所指向的節點中的val值沒有任何實際含義。當需要出隊時,出隊head指向的下一個節點first中val的值,然後拋棄head本身指向的值,讓head指向head的下一個節點first,此時head原來所指向的節點將被刪除。現在我們可以看出出隊操作也只改變了頭指標head的值。

其程式碼實現為:

    int dequeue() {
        Node *h = head;
        Node *first = head->next;
        delete h;
        head = first;
        int x = first->item;
        return x;
    }

現在佇列已經實現,下面就看看阻塞佇列如何實現。

阻塞佇列的實現

既然是阻塞佇列,那就意味這加鎖和等待。那就需要對c++的一些鎖知識和條件變數有了解。

先來看看我們需要實現阻塞佇列的那些方法:

Special Value Blocks Times out
Insert offer(o) put(o) offer(o,timeout)
Remove poll() take() poll(timeout)

入隊

讓我們先來實現put這個方法。

先看其實現流程圖:

由於該佇列實現有最大值限制,故在插入資料之前需要先判斷該佇列是否已滿,已滿則需等待該佇列有可用空間。在該執行緒入隊操作完成後,可能有別的執行緒也在等待入隊,需要喚醒其他寫資料的執行緒,使其繼續執行後續操作。如果入隊前佇列為空,可能有出隊操作正在阻塞等待讀數,也需要去喚醒讀資料的執行緒。

在看程式碼實現之前,我們需要定義一些變數用於程式碼實現環節:

    /* The capacity bound*/
    int64_t capacity;
    /*Current number of elements */
    std::atomic<int64_t> count;    
	/** Lock held by take, pool, etc */
    std::mutex takeLock;
    /** Wait queue for waiting takes */
    std::condition_variable notEmpty;
    /** Lock held by put, offer, etc */
    std::mutex putLock;
    /** Wait queue for waiting puts */
    std::condition_variable notFull;

put函式的程式碼實現:

void LinkedBlockingQueue::put(const int item){
    int c;
    {
        std::unique_lock<std::mutex> lck{putLock};
        if( count == capacity) {
            notFull.wait(lck, [this](){return count < capacity;}); //(1)
        }
        enqueue(item);  //不應該把申請空間放在鎖裡面,耗時有點大
        c = count.fetch_add(1);
    }
    if(c + 1 < capacity) {
        notFull.notify_one();
    }

    if(0 == c) {
        notEmpty.notify_one();
    }
}

對於offer(o)的實現,主要更改是對上述程式碼(1)中的等待改為直接返回false, 表示,當前沒有可用空間插入資料。正常插入就返回true.

對於offer(o,timeout)的實現,就需要在上述程式碼(1)中的wait函式新增上時間引數,使其可以在timeout時間內返回,如果是正常喚醒,正常入隊,則返回ture,否則返回false.

該更改為:

            if(!notFull.wait_for(lck, rel_time, [this](){return count < capacity;})){
                return false;
            }

出隊

對於出隊,其實現和入隊基本相同,基本上只需要更改其中的關鍵判斷和通知。

take函式的程式碼實現為:

void LinkedBlockingQueue::take(int & returnVal) {
    int c;
    {
        std::unique_lock<std::mutex> lck{takeLock};
        if( 0 == count) {
            notEmpty.wait(lck, [this](){return count > 0 ;});
        }
        returnVal = dequeue();
        c = count.fetch_sub(1);
    }

    if( c > 1 ) {
        notEmpty.notify_one();
    }

    if(c == capacity) {
        notFull.notify_one();
    }
}

剩下的實現細節可以參考我的實現

相關文章