The correct posture of the lock-free queue based on a doubly linked list (fixed errors in previous blogs)

Source: Internet
Author: User
Tags cas lock queue volatile

Directory

    • 1. Preface
    • 2. Lock-free queue based on bidirectional linked list
      • 2.1 Queue Method
      • 2.2 Team-out method
    • 3. Performance Testing
    • 4. Summary
1. Preface

If you've read this blog I wrote a few days ago to build a queue of unlocked concurrent containers (stacks and queues), I'd like to apologize to you. Because I made a low-level error when implementing the queue's Outbound method: The queue's outbound direction is in the queue header, and my implementation is at the tail of the queue. Although the code executes correctly, it is clearly not compliant with the queue specification. So that part of the code writing "no lock queue based on a doubly linked list" actually reads "No lock stack based on a doubly linked list". Of course, "the queue is queued from one end and out of the other end of the team, on the one side in and out of that is the stack" the common sense I certainly have, as to why this kind of low-level error reasoning can only be attributed to the burnout caused by continuous high temperature. Some time ago I, like a trapped in the soil of the African lung fish, the whole meaning of life is waiting for the onset of the rainy season. Recently, the long-lost rain brought a little coolness, also washed away this mental fatigue, take advantage of this opportunity to correct the mistakes of the past. Code See beautiful-concurrent on GitHub

2. Lock-free queue based on bidirectional linked list

The linked list node is defined as follows

/** * 链表节点的定义 * @param <E> */private static class Node<E> {    //指向前一个节点的指针    public volatile Node pre;    //指向后一个结点的指针    public volatile Node next;    //真正要存储在队列中的值    public E item;    public Node(E item) {        this.item = item;    }    @Override    public String toString() {        return "Node{" +                "item=" + item +                '}';    }}

When a lock-free queue is implemented based on a doubly linked list, the node pointer does not need to be updated by the atom, only with volatile modifiers to ensure visibility.

2.1 Queue Method

The first step is to look at the queue method, this part of the code refers to Doug Lea in AQS to join the thread synchronization queue this part of the implementation of the logic, so correctness is no problem

/** * 将元素加入队列尾部 * * @param e 要入队的元素 * @return true:入队成功 false:入队失败 */public boolean enqueue(E e) {    //创建一个包含入队元素的新结点    Node<E> newNode = new Node<>(e);    //死循环    for (; ; ) {        //记录当前尾结点        Node<E> taild = tail.get();        //当前尾结点为null,说明队列为空        if (taild == null) {            //CAS方式更新队列头指针            if (head.compareAndSet(null, newNode)) {                //非同步方式更新尾指针                tail.set(newNode);                return true;            }        } else {            //新结点的pre指针指向原尾结点            newNode.pre = taild;            //CAS方式将尾指针指向新的结点            if (tail.compareAndSet(taild, newNode)) {                //非同步方式使原尾结点的next指针指向新加入结点                taild.next = newNode;                return true;            }        }    }}

There are two scenarios in which the queue is empty and the queue is not empty and is judged by the element pointed to by the tail pointer of the queue:

    • 1. The queue is empty: the node to which the queue tail pointer points is null, which is part of the logic in the IF clause
      First, the CAS update the queue head pointer to the newly inserted node, and if the execution succeeds, the tail pointer is also pointed to the node in an unsynchronized manner, and the node is successfully queued; if the CAS update head pointer fails, you re-execute the For loop, as shown in

    • 2. The queue is not empty: the node to which the queue tail pointer points is not NULL. The queue logic is implemented in three steps, as shown in the process

Considering only the queue, the entire process is thread-safe, although some steps are not synchronized. Our units are listed as empty and not empty in two cases to demonstrate:

    • 1. When the queue is empty, the execution process enters the IF clause, assuming that a thread executes the head.compareAndSet(null, newNode) update header pointer successfully, and tail.set(newNode) that the other thread will only enter the IF clause because tail is NULL, and the CAS operation that updates the head pointer must fail, regardless of when it executes. Because the head is not NULL at this time. So only in the case of the queue, the operation is thread-safe when the queues are empty.
    • 2. When the queue is not empty, as long as the CAS operation that updates the tail pointer tail.compareAndSet(taild, newNode) succeeds, then the node has successfully joined the queue, and taild.next = newNode; when execution is not related to the queued case only (but will affect the logical implementation of the team, first sell a Xiaoguanzi).
2.2 Team-out method
 /** * Removes the first element of the queue from the queue and returns the element, and returns null if the queue is empty * * @return */public E dequeue () {///Dead loop for (;;            ) {//Current head node node<e> tailed = Tail.get ();            The current tail node node<e> headed = Head.get ();            if (tailed = = null) {//tail node is NULL, indicating that the queue is empty and returns null return null directly; The else if (headed = = tailed) {///tail node is the same as the head node, which indicates that there is only one element in the queue, and that the tail pointer is updated with a null if (tail.//cas method).                    Compareandset (tailed, NULL)) {//head pointer updated to NULL head.set (NULL);                return headed.item; }} else {//go to this step to indicate that the number of element nodes in the queue is greater than 1, as long as the queue header pointer is updated to point to the next node of the original head node on the line//But note that the next node of the head node may be null.                , first make sure that the new Queue header node is not NULL//The next node of the queue header node Headednext = Headed.next;  if (headednext! = null && head.compareandset (headed, Headednext)) Headednext.pre=null;            Help GC        return headed.item; }        }    }

The logical implementation of the team is divided into three scenarios: the queue is empty, and the queue has exactly one element node and the number of element nodes in the queue is greater than 1.
In fact, the last error in the code is mainly the number of nodes in the queue is greater than 1, and the other two cases, regardless of the side of the team operation is the same. Below is a discussion of the points that need attention in the implementation of the team

    • 1. The queue is empty, the criterion is tail that is the tail pointer to null, because the queue is the tail pointer to determine the status of the queues, so here to maintain consistency, even if the empty queue in the process of the head pointer has successfully pointed to the new node but did not have time to update the tail pointer, The queue will also return null at this point.

    • 2. There is just one element in the queue: the tail-end pointer just points to the same node. First update the tail pointer to NULL in CAS mode, execute successfully and then set the head pointer to null in the normal way, does this have concurrency problems? Consider this extreme scenario: just the CAS update tail pointer is null and then loses CPU execution, as shown in:

There are two kinds of situations to discuss:
1. Team-out situation
Because the tail is already null, the program will determine that the queue is empty, so the thread that executes the out team will return null
2. Queue situation
Because tail is null, the thread that executes the queued logic enters the IF clause because the head is not null at this point, so the CAS operation that performs the diagram fails and does not spin

As you can see, the out-of-line logic that happens to have exactly one element node in the queue is thread-safe.

    • 3. The number of element nodes in the queue is greater than 1
      This is the time to update the head pointer to the next node of the head node as a CAs, but be aware that before you do
      headedNext != nullMake sure that the next node of the head node is not NULL. You may ask, "Wait, this part of the code is executed if the number of element nodes in the queue is at least 2, then the next node of the head node must not be null." If you only consider the situation of the team, this is true, but may be in the team in the middle of the team, as shown in

As shown, there are 3 element nodes in the queue, but the thread responsible for the second node has successfully performed the update operation of the tail pointer but has not been able to update the previous node's next pointer to lose the CPU execution, recall the next queue process, in fact, this situation may exist and allow. If you update the head headedNext != null pointer to the next node of the header node without making a judgment at this point, the following situation occurs

At this point the outbound thread still executes the last ELSE clause, although the queue is not empty at this point, but head points to null, the CAS update operation will throw a null pointer exception, it seems that our last update of the head pointer is too hasty, There is a special case where the next pointer to the head node may be null, which is caused by the queued operation. So before the CAS update to the head pointer, get the next node of the recorded head node Headednext, and by headedNext !=null ensuring that the updated head node is not NULL. If this happens, the out-of-line thread will wait through spin until the queued thread that caused the situation successfully executes
taild.next = newNode;, the current outbound thread's exit process is successful and the head pointer is correctly set to the next node of the original queue header node.

Complete code See githubbeautiful-concurrent.

3. Performance Testing

200 threads are opened, each thread is mixed for 10,000 enqueue and outbound operations, the above process is repeated 100 times the average time to execute (in milliseconds), the complete test code has been placed on GitHub beautiful-concurrent. The test results are as shown

The final test result was a surprise. Fixed the original queue at one end of the bug, the performance has also been greatly improved. The non-lock queue lockfreelinkedqueue based on the bidirectional linked list in the concurrency environment performance in the second place, beyond our own implementation of the one-way chain-based lock-free queue Lockfreesinglelinkedqueue many, Even close to the performance of Concurrentlinkedqueue, to know that the latter implementation is much more complicated than ours, after a lot of optimization. The original error implementation because the team and the queue at one end, so no need to increase the unnecessary CSA competition, resulting in lower concurrency performance of the good understanding; that's better than a one-way list-based queue performance. After all, the latter does not have a prev pointer and a lot less pointer operation. This may be due to the fact that the one-way list is more competitive than the CAs, resulting in a low efficiency of CPU utilization. However, the two-way linked list reduces CAS competition to a certain extent because of the particularity of its structure. So this is also a lesson, if you can ensure thread safety, try not to use any synchronous operation, if you have to synchronize, then the more lightweight the better, volatile than CAS "light" much. In broadening the idea, if we are similar to Concurrentlinkedqueue optimization, such as do not need to update the queue tail pointer every time, performance will have a leap, or even beyond the concurrentlinkedqueue itself? This may be an interesting attempt to dig a hole first and then have time to fill it out later.

4. Summary

This article is a correction to the previous article error, the reason for the independence of the article is also hoped that those who have been "misleading" students have more chance to see. This time the queue of the process carried out detailed graphic analysis, and not as lazy as the last time, only to speak of a general, otherwise it will not appear "queue at one end in and out of the" this low-level error, do not know the previous article was trampled on a foot is not the reason, if you can find the wrong time in the following message to me After all, the advantage of writing a technical blog is not only the process of the system combing technical knowledge self-improvement, but also a process of sharing discussions with others. This process requires not only the author's own efforts, but also the participation of the reader.

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

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.