Michael-Scott 非阻塞队列算法中的插入

2024-02-09 22:40

本文主要是介绍Michael-Scott 非阻塞队列算法中的插入,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

CAS的基本使用模式:在更新某个值时存在不确定性,以及在更新失败时重新尝试。构建非阻塞算法的技巧在于:将执行原子修改的范围缩小到单个变量上。

  链接队列比栈更为复杂,因为它必须支持对头节点和尾节点的快速访问。因此,它需要单独维护的头指针和尾指针。有两个指针指向尾部的节点:当前最后一个元素的next指针,以及尾节点。当成功地插入一个新元素时,这两个指针都需要采用原子操作来更新。

  这里需要一些技巧来完成,第一个技巧是,即使在一个包含多个步骤的更新操作中,也要确保数据结构总是处于一致的状态。这样,当线程B到达时,如果发现线程A正在执行更新,那么线程B就可以知道有一个操作已部分完成,并且不能立即开始执行自己的更新操作。然后,B可以等待(通过反复检查队列的状态)并直到A完成更新,从而使两个线程不会相互干扰。

  虽然这种方法能够使不同的线程“轮流”访问呢数据结构,并且不会造成破坏,但如果一个线程在更新操作中失败了,那么其他的线程都无法在访问队列。要使得该算法成为一个非阻塞算法,必须确保当一个线程失败时不会妨碍其他线程继续执行下去。因此,第二个技巧是,如果当B到达时发现A正在修改数据结构,那么在数据结构中应该有足够多的信息,使得B能完成A的更新操作。如果B“帮助”A完成了更新操作,那么B可以执行自己的操作,而不用等到A的操作完成。当A恢复后再试图完成其操作时,会发现B已经替它完成了。

  在下面的程序中,给出了 Michael-Scott 提出的非阻塞连界队列算法中的插入部分,它是由 ConcurrentLinkedQueue 实现的。在许多队列算法中,空队列通常都包含一个“哨兵节点”或者“哑(Dummy)节点”,并且头节点和尾节点在初始化时都指向该哨兵节点。尾节点通常要么指向哨兵节点(如果队列为空),即队列的最后一个元素,要么(当有操作正在执行更新时)指向倒数第二个元素。下图1给出了一个处于正常状态(或者说稳定状态)的包含两个元素的队列。

Michael-Scott 非阻塞队列算法中的插入:
复制代码
 1 @ThreadSafe
 2 public class LinkedQueue<E> {
 3     private static class Node <E> {
 4         final E item;
 5         final AtomicReference<LinkedQueue.Node<E>> next;
 6 
 7         public Node(E item, LinkedQueue.Node<E> next) {
 8             this.item = item;
 9             this.next = new AtomicReference<LinkedQueue.Node<E>>(next);
10         }
11     }
12 
13     private final LinkedQueue.Node<E> dummy = new LinkedQueue.Node<E>(null, null);
14     private final AtomicReference<LinkedQueue.Node<E>> head
15             = new AtomicReference<LinkedQueue.Node<E>>(dummy);
16     private final AtomicReference<LinkedQueue.Node<E>> tail
17             = new AtomicReference<LinkedQueue.Node<E>>(dummy);
18 
19     public boolean put(E item) {
20         LinkedQueue.Node<E> newNode = new LinkedQueue.Node<E>(item, null);
21         while (true) {
22             LinkedQueue.Node<E> curTail = tail.get();
23             LinkedQueue.Node<E> tailNext = curTail.next.get();
24             if (curTail == tail.get()) {
25                 if (tailNext != null) {  // A
26                     // 队列处于中间状态,推进尾节点
27                     tail.compareAndSet(curTail, tailNext); // B
28                 } else {
29                     // 处于稳定状态,尝试插入新节点
30                     if (curTail.next.compareAndSet(null, newNode)) { // C
31                         // 插入操作成功,尝试推进尾节点
32                         tail.compareAndSet(curTail, newNode); // D
33                         return true;
34                     }
35                 }
36             }
37         }
38     }
39 }
复制代码

 

    图1 处于稳定状态并包含两个元素的对立

 

  当插入一个新的元素时,需要更新两个指针。首先更新当前最后一个元素的next 指针,将新节点链接到队列队尾,然后更新尾节点,将其指向这个新元素。在两个操作之间,队列处于一种中间状态,如图2。在等二次更新完成后,队列将再次处于稳定状态,如图3所示。

  实现这两个技巧的关键在于:当队列处于稳定状态时,尾节点的next域将为空,如果队列处于中间状态,那么tail.next 将为非空。因此,任何线程都能够通过检查tail.next 来获取队列当前的状态。而且,当队列处于中间状态时,可以通过将尾节点移动一个节点,从而结束其他线程正在执行的插入元素操作,并使得队列恢复为稳定状态

      图2  在插入过程中处于中间状态的对立

 

    图3 在插入操作完成后,队列再次处于稳定状态

  LinkedQueue.put 方法在插入新元素之前,将首先检查队列是否处于中间状态(步骤A)。如果是,那么有另一个线程正在插入元素(在步骤C和D之间)。此时当前线程不会等待其他线程执行完成,而是帮助它完成操作,并将尾节点向前推进一个节点(步骤B)。然后,它将重复执行这种检查,以免另一个线程已经开始插入新元素,并继续推进尾节点,直到它发现队列处于稳定状态之后,才会开始执行自己的插入操作。

  由于步骤C中的CAS将把新节点链接到队列尾部,因此如果两个线程同时插入元素,那么这个CAS将失败。在这样的情况下,并不会造成破坏:不会发生任何变化,并且当前的线程只需要重新读取尾节点并再次重试。如果步骤C成功了,那么插入操作将生效,第二个CAS(步骤D)被认为是一个“清理操作”,因为它既可以由执行插入操作的线程来执行,也可以由其他任何线程来执行。如果步骤D失败,那么执行插入操作的线程将返回,而不是重新执行CAS,因为不再需要重试——另一个线程已经在步骤B中完成了这个工作。

  这种方式能够工作,因为在任何线程尝试将一个新节点插入到队列之前,都会首先通过检查tail.next是否非空来判断是否需要清理队列。如果是,它首先会推荐尾节点(可能需要执行多次),直到队列处于稳定状态。

这篇关于Michael-Scott 非阻塞队列算法中的插入的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/695478

相关文章

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

hdu1180(广搜+优先队列)

此题要求最少到达目标点T的最短时间,所以我选择了广度优先搜索,并且要用到优先队列。 另外此题注意点较多,比如说可以在某个点停留,我wa了好多两次,就是因为忽略了这一点,然后参考了大神的思想,然后经过反复修改才AC的 这是我的代码 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

poj 3974 and hdu 3068 最长回文串的O(n)解法(Manacher算法)

求一段字符串中的最长回文串。 因为数据量比较大,用原来的O(n^2)会爆。 小白上的O(n^2)解法代码:TLE啦~ #include<stdio.h>#include<string.h>const int Maxn = 1000000;char s[Maxn];int main(){char e[] = {"END"};while(scanf("%s", s) != EO

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

poj 3190 优先队列+贪心

题意: 有n头牛,分别给他们挤奶的时间。 然后每头牛挤奶的时候都要在一个stall里面,并且每个stall每次只能占用一头牛。 问最少需要多少个stall,并输出每头牛所在的stall。 e.g 样例: INPUT: 51 102 43 65 84 7 OUTPUT: 412324 HINT: Explanation of the s

poj 2431 poj 3253 优先队列的运用

poj 2431: 题意: 一条路起点为0, 终点为l。 卡车初始时在0点,并且有p升油,假设油箱无限大。 给n个加油站,每个加油站距离终点 l 距离为 x[i],可以加的油量为fuel[i]。 问最少加几次油可以到达终点,若不能到达,输出-1。 解析: 《挑战程序设计竞赛》: “在卡车开往终点的途中,只有在加油站才可以加油。但是,如果认为“在到达加油站i时,就获得了一