查看原文
其他

【必看】嵌入式Engineer必经之路 -- "同步问题"

bug菌 最后一个bug 2021-01-31

1、聊一聊

    今天为大家分享一首五月天的《干杯》,"毕业季"、"那群人"、"那些事"都终将变成了回忆,唯一不变的是曾经我们的相遇,~干杯~

    好了,今天为大家带来个人觉得在进行RTOS编程中或者是OS编程中,非常要注意的,也是非常容易忽略的,甚至是有时候难以排查的一类问题 -- "同步问题",今天作者可能把该部分知识讲明白。

2、何为同步问题

    同步问题其实主要是由于多线程并发对共享资源访问导致的问题,我们都知道并发能够提高CPU对指令执行的利用率,但是凡事有利有弊,对于开发者在程序设计过程中需要解决的并发访问共享资源的问题也就变得棘手了。

 一个并发问题实例


分析一下:
  • 上图所示的例子,Cnt作为一个共享单元,当thread_B在满足访问条件以后进行了任务调度,切换到Thread_A对Cnt进行自加处理,此时再次切换到Thread_B使用Cnt对ADC数组进行访问,那么此时Thread_B中并不是访问的预先设计的ADC[5]了,这样便与我们所想要设计Thread_B运行状态不一致了。

  •  对于上面的问题从小的方面讲仅仅只是会影响到一次数据读错,大的方面可能导致数据越界,甚至程序奔溃,所以这也是很多嵌入式小伙伴在前期玩OS非常头疼的一个问题,而且经常听到的一句话:"怎么这个变量被异常修改了?",那么我们有什么办法能够避免该问题的出现呢?


3、原子操作(Atomic Operation)

    第一次接触这个名词不知道大家有什么想问的 ? 其实原子是化学反应中不可分割的最小粒子。

    那么也就说明原子操作是不会被打断的,他可以由多条指令构成,不过其执行顺序不能够被改变,一旦执行便会一直执行到结束。



    其实作者觉得吧,原子操作就是并发运行中的局部顺序执行,并发提高了CPU的利用率,但是使得逻辑更加复杂;而单任务顺序执行有着更好的可定位性可观测性,所有了兼容两者优势就形成了原子操作。


4、硬件同步方案

    大伙在学习RTOS的时候都会接触到一个必学的概念叫"临界区",不过现在大部分小伙伴都会把临界区狭义的认为就是关闭总中断为进入临界区,重新开启总中断为退出临界区,可以说禁用硬件中断是实现临界区的一种硬件手段,不过广义上的临界区的定义为线程执行过程中需要形成互斥的连续指令流。


分析一下:
  • 硬件上通过关闭中断的方法便能够关闭系统的任务调度,从而保证临界区代码执行的原子性,说白了就是按照正常顺序执行代码。

  • 如果临界区代码过多这样会导致系统的实时性大大降低,因为直接禁用了总中断,而中断个人认为就是为了处理异常事件、解放CPU的查询开销而生的。

  • 所以大部分的书籍上都会提到该临界区内的代码尽量简短,同时还要注意的是如果一旦忘记使能中断,估计程序就凉了大半截了。

  • 既然是为了处理好同步问题,其根源还是在软件指令流的执行,那么是否有软件上的解决方案呢?


5、软件同步方案

    上一小节我们说了硬件关中断方案,那么这里我们研究一下通过代码上的处理能够达到同步和互斥的效果呢?

1)错误案例  

    作者经常看到一些初学的小伙伴解决同步问题,直接用一个共享的变量加个if的形式就认为搞定了,比如下面的例子。


分析一下:
  • 可能这样的代码对于OS编程老手来说一眼就看出问题了,不过作者在很久很久之前也这样认为OK的,再说谁不是这样过来的呢?其实稍微仔细想想如图所示线程A的位置,发生任务切换到线程B,此时线程AB都满足进入临界区条件,于是便违背了临界区每次只能有一个任务访问的限制。

  • 其实上面的问题引起的主要原因就是我们C语言大部分语句都不是原子操作型语句,大部分的C语句都是由多条汇编指令构成的,这样执行的顺序等等都会影响到程序的最后结果。

2)软件实现方案 

    既然那种方式行不通,那么通过软件序列有没有办法实现临界区呢?我们来看看下面的做法:

分析一下:
  • 首先我们需要有一个前提条件,因为我们的这些标志其实本质上也是属于共享变量,暂定其读写均为原子操作,否则会存在异常问题。

  • 上面的代码基本上就解决了前面误区实例所导致的同时进入临界区问题,大家可以模拟几个过程是不会有同时堵塞和同时进入的问题了。

  • 那么上面是两个线程之间的同步互斥问题如果是N个线程形成同步又该如何编写 ? 可以大体想象一下会存在N个条件的判断,这样代码会变得非常难看,而且容易出错,所以为了简化存软件设计,这里就采用了软硬结合的办法进行处理。


6、软硬结合高效版本

    前面我们说过禁用中断之间的内容可以认为是原子操作,同时我们一般支持OS的芯片指令集中都会提供对应的原子操作指令,这样便简化了多线程的同步处理。

参考伪程序:
1#include <stdio.h>
2#include <stdlib.h>
3/*************************************************
4 * Fuction: 锁的伪代码 
5 * Author : (公众号:最后一个bug)
6 ************************************************/
 
7typedef struct{
8 int lock; 
9}sSpinLock;
10
11#define INI_LOCK(spinLock)  Clear(spinLock)
12
13#define LOCK_RESOURCE(spinLock) do\
14                      {\
15                        while(Identfy_Set(spinLock))\ 
16                        {\
17                        //加入继续队列 \
18                        //执行任务调度\
19                        }\
20                      }while(0)
21
22#define ULOCK_RESOURCE(spinLock) do\
23                      {\
24                         Clear(spinLock);\
25                      }while(0)
26
27/*************************************************
28 * Fuction:  main-参考伪代码 
29 * Author : (公众号:最后一个bug)
30 ************************************************/
           
31int main(int argc, char *argv[]) {
32    //定义锁 
33    sSpinLock stSpinLock;
34    //初始化锁 
35    INI_LOCK(stSpinLock);
36    //进入临界区 
37    LOCK_RESOURCE(spinLock);
38
39    //临界区执行 
40
41    //退出临界区 
42    ULOCK_RESOURCE(spinLock);
43    return 0;
44}
分析一下:
  • 我们可以观察前面几个小节的同步问题大部分都是因为置位和判断不是原子操作而导致了各种与设计不符合的问题;

  • 那么我们可以通过硬件提供的原子操作指令进行设计,一般也就是一条汇编指令来取代之前的多条指令操作,形成原子操作;

  • 应该有很多小伙伴想到了其实对于单核CPU,其中的禁用中断形成的临界区也就形成了类原子操作的形式,然后结合软件互斥处理也形成了一种非常好的同步互斥接口。

  • 不过多核CPU对于仅仅关闭当前CPU中断是不太奏效的,因为其他核的CPU会同样响应中断可能修改共享资源,不过对于原子操作指令却能够适应多核CPU。


7、最后小结

    今天的分享个人觉得对于一些小伙伴进阶应该是非常有帮助的,特别是大家在刚开始进行多线程编程的学习过程中,这些也是经常让大家摸不着头脑的,因为这些问题不是必然会出现,但是对于程序的稳定性是灾难性的。

    好了,这里是公众号:“最后一个bug”,一个为大家打造的技术知识提升基地。同时非常感谢各位小伙伴的支持,我们下期精彩见!

推荐好文  点击蓝色字体即可跳转

【√】以后复位芯片,数据再也不会丢了(理论篇)

【√】以后复位芯片,数据再也不会丢了(实战篇)

【进阶】除数为0,程序会奔溃吗?

【重磅】剖析MCU的IAP升级软件设计(设计思路篇)

STM32F103 IAP在线升级通过串口升级

【典藏】自制小型GUI界面框架(设计思想篇)

GUI必备知识之“告别”乱码(浅显易懂)

【经典】把脉printf中的C进阶技巧

【连载】通过"库文件"学单片机驱动编程(5)-完结篇

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存