查看原文
其他

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

Linux爱好者 2021-07-19

The following article is from 后台服务器开发 Author CPP后台服务器开发

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


问题引入:

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

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


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)本身编程就不是很友好,如果没有彻底掌握,最好还是使用锁去编写。

CAS 更多的是一种思想,也是实现高性能编程的一种途径,目前已经有一些开源级别的无锁库可以提供我们使用,也许这些才是我们最好的选择。


- EOF -

推荐阅读  点击标题可跳转

1、无锁队列的实现

2、TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

3、Linux 内核 tasklet 机制和工作队列


看完本文有收获?请分享给更多人

推荐关注「Linux 爱好者」,提升Linux技能

点赞和在看就是最大的支持❤️

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存