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

Posted 穿越临界点

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了随手记——进程内共享全局变量需要加锁么?相关的知识,希望对你有一定的参考价值。

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


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

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

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+特权指针的方式。

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

以上是关于随手记——进程内共享全局变量需要加锁么?的主要内容,如果未能解决你的问题,请参考以下文章

Linux 进程与线程四(加锁--解锁)

互斥锁与多线程间共享全局变量

安装pod教程(MAC怕忘记,随手记下)

Python进程与线程

inndb 读大量数据时会加读锁么?

python多线程全局变量和锁