Http://coolshell.cn/articles/8239.html
Implementation of a linked list with no lock queue
Enqueue (x) // enter the queue {// prepare the newly added node data q = New Record (); q-> value = x; q-> next = NULL; do {P = tail; // take the snapshot of the end pointer of the linked list} while (CAS (p-> next, null, q )! = True); // If the node chain is not on the tail pointer, try CAS (tail, p, q) again; // set the end node}
We can see that the do-while re-try-loop in the program. That is to say, it is very likely that when I prepare to add a knot at the end of the queue, other threads have been successfully added, so the tail pointer has changed, so my CAS returns false, so the program tries again, until the test is successful. This is very similar to the replaying of our hotline.
You will see why our "tail node" Operation (row 12th) does not determine whether the operation is successful, because:
- If there is a thread T1, if the CAS in its while is successful, the CAS of all other subsequent threads will fail and then be recycled,
- At this time, if the T1 thread has not updated the tail pointer, other threads continue to fail because tail-> next is not null.
- Until thread T1 updates the tail pointer, a thread in other threads can get a new tail pointer and continue.
There is a potential problem --If the T1 thread stops or fails before using CAS to update the tail pointer, other threads will enter an endless loop.. Below is the improved version of enqueue ()
Enqueue (x) // improved queue version {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); // If the node chain is not at the end, try CAS (tail, oldp, q) again; // set the end node}
We let each thread fetch the pointer P to the end of the linked list. However, this fetch will affect the performance. However, we can see from the actual situation that there will be no thread stop in the case of 99.9%. Therefore, it is better that you can join the above two versions, if the number of retries exceeds a value (for example, three times), fetch the pointer.
Okay, we have solved enqueue. Let's take a look at the dequeue Code: (very simple, I will not explain it)
Dequeue () // output queue {do {P = head; If (p-> next = NULL) {return err_empty_queue;} while (CAS (Head, P, p-> next )! = True); Return p-> next-> value ;}
We can see that the dequeue code operates on the head-> next, rather than the head itself. This is because of a boundary condition. We need a dummy header pointer to solve the problem that if there is only one element in the linked list, head and tail both point to the same node, in this way, enqueue and dequeue will be mutually excluded..
Cas aba Problems
The so-called ABA (see the ABA entry in Wikipedia) is basically like this:
- Process P1 reads the value A in the shared variable
- P1 is preemptible, And the P2 process is executed.
- P2 changes the value in the shared variable from A to B, and then returns to A, which is preemptible by P1.
- When P1 is returned, the value in the shared variable is not changed, and the execution continues.
Although P1 assumes that the variable value has not changed and the execution continues, this may cause some potential problems.The ABA problem is most likely to occur in the lock free algorithm. CAS bears the brunt of this problem because CAS judges the pointer address. If this address is reused, the problem will be very high.(Address reuse often occurs. After a memory is allocated, it is released and distributed. It is likely that the original address is still used)
For example, the preceding dequeue () function, because we want to separate head and tail, we introduced a dummy pointer to head. Before we made cas, if the head memory is recycled and reused, And the reused memory is re-used by enqueue (), this will cause a big problem. (Memory reuse in memory management is basically a common behavior.)
You may not understand this example. Wikipedia provides a living example --
You are carrying a suitcase full of money at the airport. At this time, you come to a hot and sexy girl, and then she teased you very warmly, and when you don't pay attention to it, I made a bag with the same suitcase and your suitcase full of money, and then I left. You saw that your suitcase was still there, so I took my suitcase to catch the plane.
This is the problem of ABA.
Solve ABA Problems
Wikipedia provides a solution-using double-cas (double-insurance CAS). For example, on a 32-bit system, we need to check the 64-bit content.
1) Check the double length value with CAS at a time. The first half is the pointer, and the second half is a counter.
2) only when both of them are the same can we pass the check. We need to add new values. Add 1 to the counter.
In this way, although the values of ABA are the same, the counters are different (but on a 32-bit system, this counter will overflow and start from 1, this still has the problem of ABA)
Of course, the problem with our queue is that we don't want to reuse that memory. This makes it easier to solve clear business problems. The paper implementing lock-free queues provides such a method --Use node memory reference count 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;}
The fetch & add and release are both reference count and reference count, which are atomic operations to prevent memory from being recycled.
Implement lock-free queue using Arrays
This implementation comes from implementing lock-free queues.
It is very common to use arrays to implement queues. Because there is no memory division or release, everything will become simple. The implementation idea is as follows:
1) The array queue should be an array in the form of ring buffer (ring array)
2) The array element should have three possible values: Head, tail, and empty (of course, there are actual data)
3) The array is initialized to empty at the beginning, and two adjacent elements must be initialized to head and tail, which indicates an empty queue.
4) enqueue operation. Assume that data X is to be added to the queue, locate the tail location, and use the double-CAS method to update (tail, empty) to (x, tail ). Note that if you cannot find (tail, empty), the queue is full.
5) dequeue operation. Locate the head, update (Head, x) to (empty, head), and return X. Also note that if X is tail, the queue is empty.
One key to algorithms is-how to locate head or tail?
1) We can declare two counters, one for counting the number of enqueue and the other for counting the number of dequeue.
2) The two calculators use fetch & add to accumulate atoms. It is good to accumulate them when enqueue or dequeue is complete.
3) After accumulation, find a modulo or something to know the position of tail and head.
As shown in: