一文带你彻底理解高性能无锁队列

2024-04-21 16:48

本文主要是介绍一文带你彻底理解高性能无锁队列,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一文带你彻底理解高性能无锁队列

目前,大部分软件设计都在追求高性能,快速处理,耗时低,仿佛已经是行业中必不可少的一部分。作为互联网从业人员,我们也必须适应时代的潮流,彻底掌握这种高性能编程。


问题引入:

一个生产者,多个消费者的队列,如果是你,你回怎么设计?

想必拿到这个问题,更多的人脑海中已经浮现了一把锁;我也是的,那我们就从浅入深的来看看高性能的无锁队列是怎么一步一步的演化开来的。


1、低效的实现队列

编写多线程的时候,往往会发生资源竞争的现象,导致我们不得不加锁去保护变量,但在这个的同时也对性能造成了一定的损耗。

设计方案:(C++)

如图所示:

在这里插入图片描述

这种设计方式的话,我们是不可避免加锁操作的,因为其本身就不是线程安全的。

流程:

生产者放入队列中时,加锁,数据输入后完成加锁操作;然后剩余线程进行争锁操作,进行取队列数据操作;

简单实现:

template<class T>
class SimpleQueue
{
public:SimpleQueue() {}~SimpleQueue(){}void Push(T val){_mutex.lock();_q.push(move(val));_mutex.unlock();}T Get(){_mutex.lock();if (_q.empty()){_mutex.unlock();return 0;}T val = _q.front();_q.pop();_mutex.unlock();return val;}private:mutex _mutex;queue<T>_q;
};

这个就是比较简单,同事性能较差的一种方案;

既然我们提到了高性能,那么这种操作是不不符合我们需求的,那还有什么更好的方案提供跟高的性能吗?

很显然的一个操作:去锁化-----也就是常说的无锁队列


2、无锁队列

其实有锁和无锁就是我们平时所说的乐观锁和悲观锁:

   加锁是一种悲观的策略,它总是认为每次访问共享资源的时候,总会发生冲突,所以宁愿牺牲性能(时间)来保证数据安全。无锁是一种乐观的策略,它假设线程访问共享资源不会发生冲突,所以不需要加锁,因此线程将不断执行,不需要停止。一旦碰到冲突,就重试当前操作直到没有冲突为止。

无锁的策略使用一种叫做比较交换的技术(CAS Compare And Swap)来鉴别线程冲突,一旦检测到冲突产生,就重试当前操作直到没有冲突为止。

CAS是系统原语,CAS操作是一条CPU的原子指令,所以不会有线程安全问题。

CAS 的伪码:

template <class T>
bool CAS(T* addr, T expected, T value) 
{if (*addr == expected) {*addr = value;return true;}return false;
} 

CASexpected 与一个内存地址进行比较,如果比较成功,就将内存内容替换为 new 。当前大多数机器都在硬件级实现了这个操作,在 Inter 处理器上这个操作是 CMPXCHG ,因而 CAS 是一个最基础的原子操作。

GCC4.1+版本中支持CAS的原子操作,API接口如下:

**bool** __sync_bool_compare_and_swap (type *ptr, type oldval type newval, ...)type __sync_val_compare_and_swap (type *ptr, type oldval type newval, ...)
#include <algorithm>
#include <vector>
#include <string>
#include <unordered_map>
#include <queue>
#include <functional>
#include <stack>
#include <iostream>
#include <unistd.h>
#include <thread>
#include <list>using namespace std;/*
*   说明:基于CAS封装的无锁List。
*/
template <typename T>
class JzLockfreeList
{
private:std::list<T> list;private:int mutex;int lock;int unlock;
public:JzLockfreeList() :mutex(0), lock(0), unlock(1) {};~JzLockfreeList() {};void Lock(){while (!__sync_bool_compare_and_swap(&mutex, lock, 1)){usleep(100);}}void Unlock(){__sync_bool_compare_and_swap(&mutex, unlock, 0);}void Push(T data){Lock();list.push_back(data);Unlock();}T Front(){Lock();T data = list.front();Unlock();return data;}void PopFront(){Lock();list.pop_front();Unlock();}bool IsEmpty(){Lock();if (list.empty()){Unlock();return true;}else{Unlock();return false;}}bool Find(T data){typename std::list<T>::iterator it;Lock();for (it = list.begin(); it != list.end(); ++it){if (*it == data){Unlock();return true;}}Unlock();return false;}
};JzLockfreeList<int> LF;thread_local int num = 1;
//生产者
void Producer()
{while(true){num++;cout<<"num push:"<<num<<endl;LF.Push(num);sleep(2);}
}//消费者
void Customer()
{while(true){if (!LF.IsEmpty()){cout <<"num get " <<LF.Front() <<endl;LF.PopFront();}sleep(1);}
}int main()
{thread t1(Producer);thread t2(Customer);thread t3(Customer);t1.join();t2.join();t3.join();return 0;
}

在C++11 中出现了CAS的用法,也为我们提供了API;

/*
* @brief:compare & swap(CAS)。如果等于expect则swap,否则就返回--是否交换成功, 注意expect如果不相等,会把当前值写入到expected里面。
* 相比于strong,weak可能会出现[spurious wakeup](<http://en.wikipedia.org/wiki/Spurious_wakeup>).
* @param          若x等于expect,则设置为desired 返回true,
*                 否则最新值写入expect,返回false
*/
class atomic {
bool compare_exchange_strong(T& expect /*用来比较的值*/, T desired/*用来设置的值*/)
bool compare_exchange_weak(T& expect, T desired)
}

其实在CAS中,还有一种异常产生,也就是常说的ABA的现象。所谓ABA现象就是当前现象期望值是A,某个线程将A改为B,另外线程将B改为A,导致当前线程误以为还是原来的值,然后操作就会导致一些异常出现。

这里我们可以借用数据库乐观锁的方式,维护一个全局的版本号或者是标志,每次修改的时候需要期望值和内存值相等并且标识也没有发生改变的时候采取更新值。


无锁(CAS)本身编程就不是很友好,如果没有彻底掌握,最好还是使用锁去编写。

ompare_exchange_weak(T& expect, T desired)
}


****其实在CAS中,还有一种异常产生,也就是常说的`ABA`的现象。所谓ABA现象就是当前现象期望值是A,某个线程将A改为B,另外线程将B改为A,导致当前线程误以为还是原来的值,然后操作就会导致一些异常出现。这里我们可以借用数据库乐观锁的方式,维护一个全局的版本号或者是标志,每次修改的时候需要期望值和内存值相等并且标识也没有发生改变的时候采取更新值。****无锁(CAS)本身编程就不是很友好,如果没有彻底掌握,最好还是使用锁去编写。CAS 更多的是一种思想,也是实现高性能编程的一种途径,目前已经有一些开源级别的无锁库可以提供我们使用,也许这些才是我们最好的选择。

这篇关于一文带你彻底理解高性能无锁队列的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

如何通过Python实现一个消息队列

《如何通过Python实现一个消息队列》这篇文章主要为大家详细介绍了如何通过Python实现一个简单的消息队列,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录如何通过 python 实现消息队列如何把 http 请求放在队列中执行1. 使用 queue.Queue 和 reque

一文详解Java Condition的await和signal等待通知机制

《一文详解JavaCondition的await和signal等待通知机制》这篇文章主要为大家详细介绍了JavaCondition的await和signal等待通知机制的相关知识,文中的示例代码讲... 目录1. Condition的核心方法2. 使用场景与优势3. 使用流程与规范基本模板生产者-消费者示例

解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)

《解读Redis秒杀优化方案(阻塞队列+基于Stream流的消息队列)》该文章介绍了使用Redis的阻塞队列和Stream流的消息队列来优化秒杀系统的方案,通过将秒杀流程拆分为两条流水线,使用Redi... 目录Redis秒杀优化方案(阻塞队列+Stream流的消息队列)什么是消息队列?消费者组的工作方式每

深入理解Apache Airflow 调度器(最新推荐)

《深入理解ApacheAirflow调度器(最新推荐)》ApacheAirflow调度器是数据管道管理系统的关键组件,负责编排dag中任务的执行,通过理解调度器的角色和工作方式,正确配置调度器,并... 目录什么是Airflow 调度器?Airflow 调度器工作机制配置Airflow调度器调优及优化建议最

电脑密码怎么设置? 一文读懂电脑密码的详细指南

《电脑密码怎么设置?一文读懂电脑密码的详细指南》为了保护个人隐私和数据安全,设置电脑密码显得尤为重要,那么,如何在电脑上设置密码呢?详细请看下文介绍... 设置电脑密码是保护个人隐私、数据安全以及系统安全的重要措施,下面以Windows 11系统为例,跟大家分享一下设置电脑密码的具体办php法。Windo

一文详解Python中数据清洗与处理的常用方法

《一文详解Python中数据清洗与处理的常用方法》在数据处理与分析过程中,缺失值、重复值、异常值等问题是常见的挑战,本文总结了多种数据清洗与处理方法,文中的示例代码简洁易懂,有需要的小伙伴可以参考下... 目录缺失值处理重复值处理异常值处理数据类型转换文本清洗数据分组统计数据分箱数据标准化在数据处理与分析过

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

Redis延迟队列的实现示例

《Redis延迟队列的实现示例》Redis延迟队列是一种使用Redis实现的消息队列,本文主要介绍了Redis延迟队列的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习... 目录一、什么是 Redis 延迟队列二、实现原理三、Java 代码示例四、注意事项五、使用 Redi

深入理解C语言的void*

《深入理解C语言的void*》本文主要介绍了C语言的void*,包括它的任意性、编译器对void*的类型检查以及需要显式类型转换的规则,具有一定的参考价值,感兴趣的可以了解一下... 目录一、void* 的类型任意性二、编译器对 void* 的类型检查三、需要显式类型转换占用的字节四、总结一、void* 的