最近在调试程序的时候遇到一个关于共享变量的问题,程序大致模型是有两个线程共享一个全局变量,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中运行的时候却会丢失事件了。