锁、CAS操作和无锁队列的实现_c 语言cas-程序员宅基地

技术标签: Linux  

锁的机制

锁和人很像,有的人乐观,总会想到好的一方面,所以只要越努力,就会越幸运;有的人悲观,总会想到不好的一方面,患得患失,所以经常会做不好事。我一直把前一个当作为我前进的动力和方向,快乐充实的过好每一天。

常用的锁机制也有两种:
1、乐观锁:假设不会发生并发冲突,每次不加锁而去完成某项操作,只在提交操作时,检查是否违反数据完整性。如果因为冲突失败就继续重试,直到成功为止。而乐观锁用到的机制就是CAS。

乐观锁大多是基于数据版本记录机制实现。为数据增加一个版本标识,比如在基于数据库表的版本解决方案中,一般是通过微数据库表增加一个“version”字段来实现。读取数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。*乐观锁的缺点是不能解决脏读的问题*。

注意:在实际生产环境里边,如果并发量不大且不允许脏读,可以使用悲观锁解决并发问题。但如果系统的并发非常大的话,悲观锁定会带来非常大的性能问题,所以我们就要选择乐观锁。

2、悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。悲观锁的实现,往往依靠底层提供的锁机制。悲观锁会导致其他所有需要锁的线程挂起,等待持有锁的线程释放锁。如果所有线程都在等待其他线程释放锁,而不能主动释放锁资源,那么也会造成死锁问题。

锁的机制存在以下问题:
(1)在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
(2)一个线程持有锁会导致其他所有需要次所的线程挂起。
(3)如果一个优先级搞得线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

CAS操作

Compare And Set(或Compare And Swap),CAS是解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)、新值(B)。如果内存位置的值与预期原值相同,那么处理器会自动将内存的值更新为新值。否则,处理器不做任何操作。无论哪种情况,处理器都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更新该位置,只告诉我这个位置现在的值即可。”
现在几乎所有的CPU指令都支持CAS的原子操作,X86下对应的是CMPXCHG汇编指令。有了这个操作,我们就可以用其来实现各种无锁的数据结构。

这个操作可以用以下的例子来描述:
意思是,看一看内存*reg里的值是不是oldval,如果是的话,则对其赋值newval,并返回true,表示更新成功,如果返回false,则表示修改失败。

bool compare_and_swap(int *reg,int oldval,int newval)
{
    int reg_val = *reg;
    if(reg_val == oldval)
    {
        *reg = newval;
        return true;
    }
    return false;
}

CAS操作无锁队列的实现(参考)

Q:CAS的实现
A:gcc提供了两个函数

bool __sync_bool_compare_and_swap (type *ptr, type oldval, 
                                    type newval, ...);
type __sync_val_compare_and_swap (type *ptr, type oldval, 
                            type newval, ...);

这两个函数提供原子的比较和交换,如果*ptr == oldval,就将newval写入*ptr,
第一个函数在相等并写入的情况下返回true,这个函数比第二个好在,返回bool值可以知道有没有更新成功。
第二个函数在返回操作之前的值。

第二个函数用c语言描述:

type __sync_val_compare_and_swap (type *ptr, type oldval, 
                            type newval, ...)
{
    type cur = *ptr;
    if (cur == oldval)
    {
        *ptr = newval;
    }
    return cur;// 返回操作之前的值
}

type只能是1,2,4或8字节长度的int类型,否则会发生下面的错误
cas1
Q: 操作系统级别是如何实现的
A: X86中有一个CMPXCHG的汇编指令

Q: CAS指令有什么缺点
A:
1.存在ABA问题因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
2.循环时间长开销大自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
3.只能保证一个共享变量的原子操作对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。

gcc从4.1.2提供了__sync_*系列的built-in函数,用于提供加减和逻辑运算的原子操作。

其声明如下:

原子操作的后置加加type __sync_fetch_and_add (type *ptr, type value, …)
原子操作的前置加加type __sync_add_and_fetch (type *ptr, type value, …)
其他类比
type __sync_fetch_and_sub (type *ptr, type value, …)
type __sync_fetch_and_or (type *ptr, type value, …)
type __sync_fetch_and_and (type *ptr, type value, …)
type __sync_fetch_and_xor (type *ptr, type value, …)
type __sync_fetch_and_nand (type *ptr, type value, …)

type __sync_sub_and_fetch (type *ptr, type value, …)
type __sync_or_and_fetch (type *ptr, type value, …)
type __sync_and_and_fetch (type *ptr, type value, …)
type __sync_xor_and_fetch (type *ptr, type value, …)
type __sync_nand_and_fetch (type *ptr, type value, …)
这两组函数的区别在于第一组返回更新前的值,第二组返回更新后的值。

关于CAS函数,参考: http://blog.csdn.net/youfuchen/article/details/23179799

在多线程环境下有以下情景:

比如对同一链队列进行入队操作时一个线程正在将新的队列节点 挂载到 队尾节点的next上,可是还没来的及更新队尾节点 但同一时刻另一个线程也在进行入队操作将新的队列节点也挂在到了没更新的队尾节点那么先挂载的节点就丢失了。

为了解决多线程环境下的这些问题,我们第一时间肯定想到了加上互斥锁控制同一时刻只能有一个线程可以对队列进行写操作,但是加锁的操作太消耗系统资源了很繁重 。

因为对临界区的操作只有一步 就是对队列的尾节点进行更新,只要让这一步进行的是原子操作就可以了,所以使用到了CAS操作。

为了有一个对比 写了一份thread_queue.c是用锁对临界区进行控制访问的
另一份是lock_free_queue.c是用CAS确保对临界区的操作是原子操作

queue.h”

#ifndef QUEUE_H_
#define QUEUE_H_

#include <stdio.h>
#include <stdlib.h>

/*
普通的
链式队列
*/
typedef struct QNode
{
    int data;
    struct QNode *next;
}QNode, *QueuePtr;

typedef struct LinkQueue
{
    QueuePtr front;
    QueuePtr rear;
}LinkQueue;

void init_Queue(LinkQueue *q);//初始化队列
void push_Queue(LinkQueue *q, int e);//队尾入队
int pop_Queue(LinkQueue *q, int *e);//队头出队
int is_Empty(LinkQueue *q);
void show(LinkQueue *q);

#endif /* QUEUE_H_ */queue.c”

#include "queue.h"

/*
初始化
为队列构建一个头结点
让front和rear都指向这个头结点
*/
void init_Queue(LinkQueue *q)
{
    q->front = q->rear = (QNode *)malloc(sizeof(QNode));
    q->front->next = NULL;
}

/*
普通的入队操作
*/
void push_Queue(LinkQueue *q, int e)
{
    QueuePtr newNode = (QueuePtr)malloc(sizeof(QNode));
    newNode->data = e;
    newNode->next = NULL;
    q->rear->next = newNode;
    q->rear = newNode;
}

/*
cas的入队操作
和普通的入队操作一样
新建节点后
要将新节点挂在队尾时需要进行cas操作
因为官方文档:The definition given in the Intel documentation allows only for the use of the types int, long, long long as well as their unsigned counterparts
只能用 int, long, long long
所以要把指针类型 QueuePtr 变成 long
用long的另一个原因就是:屏蔽32位和64位的差异 long在32位是4字节 64位是8字节
*/
void cas_push(LinkQueue *q, int e)
{
    QueuePtr newNode = (QueuePtr)malloc(sizeof(QNode));
    newNode->data = e;
    newNode->next = NULL;

    QueuePtr tmp;
    do
    {
        tmp = q->rear;
    }while (!__sync_bool_compare_and_swap((long *)(&(tmp->next)), NULL, (long)newNode));

    q->rear = newNode;
}

/*
以前的判空是 q->front == q->rear
但是这样子会增加出队的操作 当出的是最后一个元素时, q->rear需要指向 q->front
我把这一步省了 暂时没有发现有什么副作用
所以我改成了 q->front->next == NULL
*/
int is_Empty(LinkQueue *q)
{
    if (q->front->next == NULL)
    {
        return(1);
    }
    return(0);
}

/*
普通的出队操作
如果队空 返回0 也就是false
e作为接受元素的缓冲
*/
int pop_Queue(LinkQueue *q, int *e)
{
    if (is_Empty(q))
    {
        return(0);
    }
    QueuePtr tmp;
    tmp = q->front->next;
    q->front->next = tmp->next;

    *e = tmp->data;
    free(tmp);
    return(1);
}

/*
cas的出队操作
每一次都要判断这个队列是不是空
然后执行cas的出队操作:
(1)tmp = q->rear 把旧的队头存起来
(2)执行原子操作:看 旧的队头 是否等于 现在的队头 tmp == *(&(q->front)) 如果相等执行 *(&(q->front)) = tmp->next 返回true 
    否则,即执行这一步原子操作的时候,别的线程修改了队列,导致队尾指向改变了,返回false ,while(!false)回到第一步重新执行
*/
int cas_pop(LinkQueue *q, int *e)
{
    QueuePtr tmp;
    do {
        if (is_Empty(q))
        {
            return(0);
        }
        //printf("cas_pop...\n");
        tmp = q->front->next;
    } while (!__sync_bool_compare_and_swap((long *)(&(q->front->next)), (long)tmp, (long)tmp->next));

    *e = tmp->data;
    free(tmp);
    return(1);
}

/*
遍历队列 打印里面的元素 为了求证队列里面的元素
*/
void show(LinkQueue *q)
{
    printf("void show(LinkQueue *q)\n");
    QueuePtr tmp = q->front->next;
    while (tmp)
    {
        printf("%d ", tmp->data);
        tmp = tmp->next;
    }
    printf("\n");
}

“thread_queue.c”

#include "queue.h"
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <assert.h>

#define THREAD_NUMBER 4//开启的线程数,电脑是4核,所以用4

//sem_t queue_sem;//信号量
pthread_mutex_t mutex;//互斥锁

void *thread_push(void *arg);
void *thread_pop(void *arg);

int main()
{
    LinkQueue que;
    init_Queue(&que);

    /*初始化二进制信号量 初始值为1 代表每一次只有1个线程可以访问 
    本来更加应该用互斥量 比较贴合情景 但是不太熟 就用了信号量
    */
    //int res = sem_init(&queue_sem, 0, 1);
    //assert(res != -1);

    int i;
    pthread_t threadArr[THREAD_NUMBER];
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_create(&threadArr[i], NULL, thread_push, (void *)&que);
    }

    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_join(threadArr[i], NULL);
    }

    show(&que);

    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_create(&threadArr[i], NULL, thread_pop, (void *)&que);
    }

    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_join(threadArr[i], NULL);
    }

    //sem_destroy(&queue_sem);

    exit(EXIT_SUCCESS);
}

void *thread_push(void *arg)
{
    printf("start push\n");
    LinkQueue * quePtr = (LinkQueue *)arg;
    int i;
    for (i = 0; i < 20; ++i)
    {
        //sem_wait(&queue_sem);
        pthread_mutex_lock(&mutex);
        push_Queue(quePtr, i);
        pthread_mutex_unlock(&mutex);
        //sem_post(&queue_sem);
    }
    printf("finish push\n");
    pthread_exit(NULL);
}

void *thread_pop(void *arg)
{
    printf("start pop\n");
    LinkQueue * quePtr = (LinkQueue *)arg;
    int tmp;
    int res;
    while (1)
    {
        //sem_wait(&queue_sem);
        pthread_mutex_lock(&mutex);
        res = pop_Queue(quePtr, &tmp);
        pthread_mutex_unlock(&mutex);
        //sem_post(&queue_sem);
        if (!res)
        {
            break;
        }
        printf("%d ", tmp);
    }
    printf("finish pop\n");
    pthread_exit(NULL);
}

“lock_free_queue.c”

#include "queue.h"
#include <pthread.h>
#include <unistd.h>
#include <assert.h>

#define THREAD_NUMBER 4//开启的线程数,电脑是4核,所以用4

void *thread_push(void *arg);
void *thread_pop(void *arg);

/*
初始化空队列

为了模拟线程对资源的抢占
开启4个线程 每个线程push 20个元素 0~19
等待4个线程结束
打印队列元素 验证push
开启四个线程 每个线程都对队列进行 pop操作
*/
int main()
{
    LinkQueue que;
    init_Queue(&que);

    int i;
    /*
    创造四个新线程 每个线程都执行 thread_push(&que)
    */
    pthread_t threadArr[THREAD_NUMBER];
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_create(&threadArr[i], NULL, thread_push, (void *)&que);
    }

    /*
    等待四个线程都执行完
    要不然主线程一下子就跑完了 程序就结束了
    还有就是 为了show函数 可以验证元素是不是都push进去了
    */
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_join(threadArr[i], NULL);
    }

    show(&que);

    /*
    创造四个新线程 每个线程都执行 thread_pop(&que)
    */
    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_create(&threadArr[i], NULL, thread_pop, (void *)&que);
    }

    for (i = 0; i < THREAD_NUMBER; ++i)
    {
        pthread_join(threadArr[i], NULL);
    }

    exit(EXIT_SUCCESS);
}

void *thread_push(void *arg)
{
    printf("start push\n");
    LinkQueue * quePtr = (LinkQueue *)arg;
    int i;
    for (i = 0; i < 20; ++i)
    {
        cas_push(quePtr, i);
    }
    printf("finish push\n");
    pthread_exit(NULL);
}

void *thread_pop(void *arg)
{
    printf("start pop\n");
    LinkQueue * quePtr = (LinkQueue *)arg;
    int tmp;
    int res;
    while (1)
    {
        res = cas_pop(quePtr, &tmp);
        if (!res)
        {
            break;
        }
        printf("%d ", tmp);
        //sleep(1);
    }
    printf("finish pop\n");
    pthread_exit(NULL);
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/yishizuofei/article/details/78353722

智能推荐

2022黑龙江最新建筑八大员(材料员)模拟考试试题及答案_料账的试题-程序员宅基地

文章浏览阅读529次。百分百题库提供建筑八大员(材料员)考试试题、建筑八大员(材料员)考试预测题、建筑八大员(材料员)考试真题、建筑八大员(材料员)证考试题库等,提供在线做题刷题,在线模拟考试,助你考试轻松过关。310项目经理部应编制机械设备使用计划并报()审批。A监理单位B企业C建设单位D租赁单位答案:B311对技术开发、新技术和新工艺应用等情况进行的分析和评价属于()。A人力资源管理考核B材料管理考核C机械设备管理考核D技术管理考核答案:D312建筑垃圾和渣土._料账的试题

chatgpt赋能python:Python自动打开浏览器的技巧-程序员宅基地

文章浏览阅读614次。本文由chatgpt生成,文章没有在chatgpt生成的基础上进行任何的修改。以上只是chatgpt能力的冰山一角。作为通用的Aigc大模型,只是展现它原本的实力。对于颠覆工作方式的ChatGPT,应该选择拥抱而不是抗拒,未来属于“会用”AI的人。AI职场汇报智能办公文案写作效率提升教程 专注于AI+职场+办公方向。下图是课程的整体大纲下图是AI职场汇报智能办公文案写作效率提升教程中用到的ai工具。_python自动打开浏览器

Linux中安装JDK-RPM_linux 安装jdk rpm-程序员宅基地

文章浏览阅读545次。Linux中安装JDK-RPM方式_linux 安装jdk rpm

net高校志愿者管理系统-73371,计算机毕业设计(上万套实战教程,赠送源码)-程序员宅基地

文章浏览阅读25次。免费领取项目源码,请关注赞收藏并私信博主,谢谢-高校志愿者管理系统主要功能模块包括页、个人资料(个人信息。修改密码)、公共管理(轮播图、系统公告)、用户管理(管理员、志愿用户)、信息管理(志愿资讯、资讯分类)、活动分类、志愿活动、报名信息、活动心得、留言反馈,采取面对对象的开发模式进行软件的开发和硬体的架设,能很好的满足实际使用的需求,完善了对应的软体架设以及程序编码的工作,采取SQL Server 作为后台数据的主要存储单元,采用Asp.Net技术进行业务系统的编码及其开发,实现了本系统的全部功能。

小米宣布用鸿蒙了吗,小米OV对于是否采用鸿蒙保持沉默,原因是中国制造需要它们...-程序员宅基地

文章浏览阅读122次。原标题:小米OV对于是否采用鸿蒙保持沉默,原因是中国制造需要它们目前华为已开始对鸿蒙系统大规模宣传,不过中国手机四强中的另外三家小米、OPPO、vivo对于是否采用鸿蒙系统保持沉默,甚至OPPO还因此而闹出了一些风波,对此柏铭科技认为这是因为中国制造当下需要小米OV几家继续将手机出口至海外市场。 2020年中国制造支持中国经济渡过了艰难的一年,这一年中国进出口贸易额保持稳步增长的势头,成为全球唯一..._小米宣布用鸿蒙系统

Kafka Eagle_kafka eagle git-程序员宅基地

文章浏览阅读1.3k次。1.Kafka Eagle实现kafka消息监控的代码细节是什么?2.Kafka owner的组成规则是什么?3.怎样使用SQL进行kafka数据预览?4.Kafka Eagle是否支持多集群监控?1.概述在《Kafka 消息监控 - Kafka Eagle》一文中,简单的介绍了 Kafka Eagle这款监控工具的作用,截图预览,以及使用详情。今天_kafka eagle git

随便推点

Eva.js是什么(互动小游戏开发)-程序员宅基地

文章浏览阅读1.1k次,点赞29次,收藏19次。Eva.js 是一个专注于开发互动游戏项目的前端游戏引擎。:Eva.js 提供开箱即用的游戏组件供开发人员立即使用。是的,它简单而优雅!:Eva.js 由高效的运行时和渲染管道 (Pixi.JS) 提供支持,这使得释放设备的全部潜力成为可能。:得益于 ECS(实体-组件-系统)架构,你可以通过高度可定制的 API 扩展您的需求。唯一的限制是你的想象力!_eva.js

OC学习笔记-Objective-C概述和特点_objective-c特点及应用领域-程序员宅基地

文章浏览阅读1k次。Objective-C概述Objective-C是一种面向对象的计算机语言,1980年代初布莱德.考斯特在其公司Stepstone发明Objective-C,该语言是基于SmallTalk-80。1988年NeXT公司发布了OC,他的开发环境和类库叫NEXTSTEP, 1994年NExt与Sun公司发布了标准的NEXTSTEP系统,取名openStep。1996_objective-c特点及应用领域

STM32学习笔记6:TIM基本介绍_stm32 tim寄存器详解-程序员宅基地

文章浏览阅读955次,点赞20次,收藏16次。TIM(Timer)定时器定时器可以对输入的时钟进行计数,并在计数值达到设定值时触发中断16位计数器、预分频器、自动重装寄存器的时基单元,在 72MHz 计数时钟下可以实现最大 59.65s 的定时,59.65s65536×65536×172MHz59.65s65536×65536×721​MHz不仅具备基本的定时中断功能,而且还包含内外时钟源选择、输入捕获、输出比较、编码器接口、主从触发模式等多种功能。_stm32 tim寄存器详解

前端基础语言HTML、CSS 和 JavaScript 学习指南_艾编程学习资料-程序员宅基地

文章浏览阅读1.5k次。对于任何有兴趣学习前端 Web 开发的人来说,了解 HTML、CSS 和JavaScript 之间的区别至关重要。这三种前端语言都是您访问过的每个网站的用户界面构建块。而且,虽然每种语言都有不同的功能重点,但它们都可以共同创建令人兴奋的交互式网站,让用户保持参与。因此,您会发现学习所有三种语言都很重要。如果您有兴趣从事前端开发工作,可以通过多种方式学习这些语言——在艾编程就可以参与到学习当中来。在本文中,我们将回顾每种语言的特征、它们如何协同工作以及您可以在哪里学习它们。HTML vs C._艾编程学习资料

三维重构(10):PCL点云配准_局部点云与全局点云配准-程序员宅基地

文章浏览阅读2.8k次。点云配准主要针对点云的:不完整、旋转错位、平移错位。因此要得到完整点云就需要对局部点云进行配准。为了得到被测物体的完整数据模型,需要确定一个合适的坐标系变换,将从各个视角得到的点集合并到一个统一的坐标系下形成一个完整的数据点云,然后就可以方便地进行可视化,这就是点云数据的配准。点云配准技术通过计算机技术和统计学规律,通过计算机计算两个点云之间的错位,也就是把在不同的坐标系下的得到的点云进行坐标变..._局部点云与全局点云配准

python零基础学习书-Python零基础到进阶必读的书藉:Python学习手册pdf免费下载-程序员宅基地

文章浏览阅读273次。提取码:0oorGoogle和YouTube由于Python的高可适应性、易于维护以及适合于快速开发而采用它。如果你想要编写高质量、高效的并且易于与其他语言和工具集成的代码,《Python学习手册:第4 版》将帮助你使用Python快速实现这一点,不管你是编程新手还是Python初学者。本书是易于掌握和自学的教程,根据作者Python专家Mark Lutz的著名培训课程编写而成。《Python学习..._零基础学pythonpdf电子书