CAS与互斥锁

最近在调试程序的时候遇到一个关于共享变量的问题,程序大致模型是有两个线程共享一个全局变量,A线程在一定条件触发后设置全局变量,B线程检测到全局变量的值被设置后先清除全局变量再作相应的操作,伪代码如下:

线程A:
if(condition is true)
{
    set flag = true;
}

线程B:
if(flag == true)
{
    set flag = false;
    do something
}

程序逻辑很简单,但是在测试的时候发现有几率出现A线程设置了全局变量后,但是B线程却没有作出相应的操作,也就导致事件丢失了,因为程序是从单片机移植过去的,单片机中虽然跑了freeRTOS系统但是程序运行结果是完全正确的,所以开始并没有想过要对部分操作加锁,尝试各种解决方案后(例如使用volatile关键字修饰、禁用CPU多核运行)问题依旧,经过加打印信息反复排查,找到了出问题的地方,因为变量在B线程被读取后还需要清除,但是读取和清除操作并不是原子的,这就可能出现B线程刚好读取了全局变量值后,A线程的时间片到了,开始执行A线程,A线程设置全局变量然后A线程退出,B线程继续恢复执行然后清除全局变量,这就导致刚刚A线程设置的全局变量又被清除了。

找到了问题,那就来解决,首先采用一种方法就是我们把清除操作放到B线程的末尾来做,但这只能缓解并不能从根本解决问题,就好比单片机中的中断服务程序,进入中断服务程序后首先要做的就是清除中断标志,否则可能就无法产生下次中断,这里是一样的;那就尝试加锁吧,使用互斥锁将A线程的设置全局变量这一步与B线程的读取并清除全局变量这一步通过锁保护起来,再测试发现问题解决了,没有再出现事件丢失。

虽然问题解决了,但这不一定是最好的方案,刚好之前了解了一下java里面的各种锁,基本都使用了一种CAS机制,CAS是Compare And Swap的缩写,即比较与替换。比锁更轻量级效率更高,其使用了三个操作值:内存地址V,旧值O,新值N,当且仅当内存地址V里面的值与旧值相同时,才会将新值N写入到内存地址V,这里表述可能不容易看懂,这个过程也不算复杂,想了解的可以自行查阅资料,限于篇幅不进行讲解。

CAS整个过程虽然分为比较和替换,但是对于上层来说,这个操作是原子的,在底层其实现是一条汇编指令cmpxchg,对于单核CPU来说,其是不可被打断的,而对于多核CPU则需要通过LOCK_IF_MP()来判断是否为多核CPU,如果是则需要加上lock前缀通过锁总线或者缓存锁定的方式来达到原子性。

有效率更高的方式当然要试一下,查阅了一些资料,发现我使用的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, ...)

第一种返回值是bool类型,即成功将新值写入内存后返回true,否则会不断重试,第二种会返回内存里面之前的操作值,使用第一种刚好可以满足需求,我们要的就是读取和清除是原子的就行了。

下面是我的测试代码:

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <pthread.h>
#include <unistd.h>
#include <time.h>

//编译
//gcc cas_test.c -lpthread -lm

#define IF_USE_MUTEX 1

#define COMPUTE_TIMES (1000 * 1000)

volatile uint64_t val = 0;

uint32_t get_tick_count_ms(void)
{
    struct timespec tp;

    clock_gettime(CLOCK_MONOTONIC,&tp);

    return (tp.tv_sec * 1000 + tp.tv_nsec / 1000000);
}

#if IF_USE_MUTEX

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//互斥锁方式
void *add(void *para)
{
    (void)para;

    for(volatile uint64_t i = 0;i < COMPUTE_TIMES;i++)
    {
        pthread_mutex_lock(&mutex);
        val++;
        pthread_mutex_unlock(&mutex);
    }

    return (void *)1;
}

#else

//CAS方式
void *add(void *para)
{
    (void)para;
    register uint64_t back,expect;

    for(volatile uint64_t i = 0;i < COMPUTE_TIMES;i++)
    {
        do{
            back = val;
            expect = val + 1;
        }while(!__sync_bool_compare_and_swap(&val,back,expect));
    }

    return (void *)1;
}

#endif/*IF_USE_MUTEX*/

int main(int argc,char *argv[])
{
    pthread_t thd;
    uint32_t tick_start = 0,tick_end = 0;

    #if IF_USE_MUTEX
    pthread_mutex_init(&mutex,NULL);
    #endif

    tick_start = get_tick_count_ms();
    printf("tick_start:%u\r\n",tick_start);

    pthread_create(&thd,NULL,add,NULL);

    add(NULL);

    //确保线程已经执行完
    pthread_join(thd,NULL);

    tick_end = get_tick_count_ms();
    printf("tick_end:%u\r\n",tick_end);

    printf("time:%ums\r\n",tick_end - tick_start);

    printf("%lu\r\n",val);

    return 0;
}

情况1:使用CAS

tick_start:10283903

tick_end:10283966

time:63ms

2000000

情况2:使用互斥锁

tick_start:10292230

tick_end:10292362

time:132ms

2000000

从上面的数据可以看到CAS方式效率确实比互斥锁方式更高,但CAS也是有一些缺点的,只不过我这里不会出现,所以未提及。

前面提到过在单片机中运行是完全正常的,其原因是在单片机中freeRTOS是抢占式的,而我刚好把线程B对应在单片机程序中的任务优先级设置得比其它任务都高,所以不会出现被打断的情况,而在linux中使用线程的时候却不是这样的,这也就是为什么在单片机中正常而在linux中运行的时候却会丢失事件了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值