随手记——进程内共享全局变量需要加锁么?

本文探讨了在多线程环境中,如何高效处理共享全局变量的并发问题。作者通过实例揭示了问题根源在于一个被误解的代码片段,并介绍了加锁、合并函数及const+特权指针等解决方案,强调了正确使用这些技术的重要性。
摘要由CSDN通过智能技术生成

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的无效信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。


这个问题按道理来说是非常基础的知识了——最简单的答案就是要加锁。但在现实的场景中还是让人很纠结的,因为现实中的情况是一个源文件中有成百(甚至上千)个全局变量的(国内大部分公司都是饿汉型的——只关注功能不关注架构和规范这种“阳春白雪”的东西),都加上锁不成卖锁的了?所以,真实的场景是大部分都不加锁。

但是在关键点上也不加锁的话,就会引发偶现的并发竞态问题。下面我们就在一个真实场景中感受一下这个问题。

1 问题引入

先交代一下背景。同一个进程中有一个主线程(以下简称TA)和多个从线程(以下简称TBx),主线程只有一个,从线程可以根据业务动态增加和减少。TA和TBx的维护在不同部门(注意这点很重要,常常跨部门的问题都很难快速解决)。TA和TBx共享一个全局变量global,该全局变量的数值由TA进行维护,TBx原则上只对该变量进行读取。

问题出现的概率极低,在创建多个从线程,并且在特定业务下才会出现。这对我们定位问题提出了巨大的挑战。所以,第一步就是要能够找到问题触发的条件,稳定复现问题才是解决问题的第一步。我们跨过这步之后才来到了今天问题的主角——global的值偶现异常。

2 问题分析定位

按照常理来说global的值维护起来是很简单的,主线程每次收到一个中断(中断周期为500us)就自加1而已。但定位过程中就发现有时候自加1就会失败(保持原值或者其他一个异常值,保持原值的情况居多)。

这么简单的逻辑(其实没有逻辑。。。)居然还能出错?感觉CPU不受控制了。于是乎各种手段加上——防止乱序执行、内存屏障、原子操作。

然而,复现概率低了,但是仍然没有解决。。。


到这里思路有点受限了,走入了死胡同。那接下来就是 跳出来 ——我们的前提是否出现了问题?

Tips:注意这种没有思路后跳到问题的源头或者出发点进行分析的思路非常重要。一切逻辑推理都需要前提,如果大前提错了,那整个推理的大厦都将倒塌。

分析的前提——TA和TBx共享一个全局变量global,该全局变量的数值由TA进行维护,TBx原则上只对该变量进行读取。

对该前提进行怀疑的时候,就开始柳暗花明了。

我们在代码中搜索了global使用的所有位置,大海捞针般地找到了一处非常怪异也十分可疑的代码。

它长成这样:

/* 内部接口 */
static int get_slot(void)
{
    ... /*此处省略对global范围的校验*/
    return global;
}
/* 对外接口:由其他从线程调用 */
int slot_get(void)
{
    ...
    /*始作俑者就在这里;实际上是自己给自己赋值了,而且中间还进行了一系列耗时的操作*/
    global = get_slot(); 
    
    return global;
}

上述代码确实比较奇怪,可能是历史遗留问题。问题就出在 global = get_slot(); 上,包含该代码的函数会被其他从线程调用,这也是为什么从线程创建的越多,问题越容易暴露的原因。


3 问题解决

3.1 常规解决

3.1.1 加锁

在所有修改global的地方加上锁即可。但这种方式比较笨重,逻辑本身就只有一处写,所以本可以不用加锁的。

3.1.2 保证只有一个线程写

针对业务的特点——只需要主线程修改global的值,其他从线程不需要写,只需要读——可以将get_slot()函数与slot_get()函数合并,去除slot_get()中对global的写操作即可。

3.2 更优的方式

人为保证只有主线程写global还是比较难的,有没有语法技巧来保证这一点呢?

有的。可以使用const关键字配合 “特权指针”的方式达到这样的效果。

下面给出一个小例子:

#include <stdio.h>

volatile const int global = 0;

void main(void)
{
    	/* 使用指针(姑且称之为特权指针)指向const变量地址;注意需要强转,不然会报报警 */
        int *pPrivilege = (int *)&global; 

        *pPrivilege = 7;
        printf("The global value is %d .\n", global);

        //global = 3; /*如果将改行注释去掉编译会报错,提示该变量不可以修改*/
}

将global定义为const类型,只有通过指针才能修改,直接修改global编译时会报错。

# 直接修改global编译时会报错
main.c: In function ‘main’:
main.c:12:2: error: assignment of read-only variable ‘global’
  global = 3; 
  ^

4 复盘

  1. 进程内共享全局变量,如果有超过一个线程写该变量,则需要加锁(或者保证原子操作)。
  2. 如果只有一个线程写该变量,其他线程均为读,可以不加锁。为了保证只有一个线程写,可以使用const+特权指针的方式。

恭喜你又坚持看完了一篇博客,又进步了一点点!如果感觉还不错就点个赞再走吧,你的点赞和关注将是我持续输出的哒哒哒动力~~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值