多线程之间同步是继多线程学习之后,需要重点掌握的又一个重要内容。一个实时操作系统里面,如果只有多线程而没有线程间同步,各个线程都无序运行,那么必然会导致整个系统的运行出现各种问题。
正是由于一个较大的任务拆分为多个小任务,这些小任务是由多个线程去执行的,那么,这些小任务之间必然会存在着千丝万缕的关系,小任务的运行更不能只管自扫门前雪,不管他人瓦上霜,因此,线程间同步是必须掌握的内容。
关于多线程之间的同步,RT-Thread官方提供了比较丰富的文档作为参考,具体可以查看以下链接:
https://www.rt-thread.org/document/site/programming-manual/ipc1/ipc1/
本文尝试从以下几个方面总结一下RT-Thread线程间同步的学习过程
什么是线程间同步,为什么需要线程同步?
上一篇文章里面提及到,音乐播放器通过多线程工作的时候,需要通过合理的调度方式,才能让各个线程协同工作。而这里所说的“合理的调度方式”,其中一种方式就是指多线程同步。
什么是线程间同步?通俗一点来说,线程间同步是指多个线程之间进行协商工作的方式。前面已经说过,线程在工作的时候,虽然只专心在做一件事情,但线程在工作的时候,并不能只埋头苦干,而不顾其他线程的状态,因此必须要有一种方式,来告知其他线程关于自身的工作状态。
为什么需要线程同步?继续用音乐播放器来举例说明,按照前文的图例,假如不使用多线程播放音乐,会有什么后果呢?后果有可能是,音乐文件读取线程比歌词文件读取线程跑得慢,导致歌曲还没播放到那一步,而歌词反而先显示出来了,还有其他可能性,导致歌曲,歌词,MV三者播放的顺序乱套了,不能同步显示。
如果要让音乐播放能正常工作,就需要在4个工作线程之间加入线程同步机制。比如歌词文件读取线程可能运行得比较快,而音乐文件读取线程读取音乐比较慢,那么,这两个线程之间就需要进行同步,快的线程要稍微等一下,等慢的线程发送一个同步消息,这样两者才能一起愉快地继续运行。
线程间同步的方式
针对RT-Thread实时操作系统,线程间同步主要有三种方式:信号量,互斥量,事件集。这三种线程同步机制各有优缺点,在实际开发工作里面,需要根据不同的应用场景进行区分使用。
信号量是一种非常灵活的线程同步方式,通过信号量可以衍生出多种功能,比如,锁、线程同步、资源计数,后面讲述的互斥量也可以通过二值型信号量来实现。生活中的停车场应用场景,就是信号量的一种具体体现。
1、当停车场空的时候,停车场的管理员发现有很多空车位,此时会让外面的车陆续进入停车场获得停车位。
2、当停车场的车位满的时候,管理员发现已经没有空车位,将禁止外面的车进入停车场,车辆在外排队等候。
3、当停车场内有车离开时,管理员发现有空的车位让出,允许外面的车进入停车场;待空车位填满后,又禁止外部车辆进入。
在这个场景里面,管理员就相当于信号量,管理员手中空车位的个数就是信号量的值(非负数,动态变化),停车位相当于公共资源(临界区),车辆相当于线程,车辆通过获得管理员的允许取得停车位,就类似于线程通过获得信号量访问公共资源。
信号量是没有“所有权”这种概念的,也就是说,对于一个非二值型的信号量,多个线程可以对其进行获取/释放操作,也可以递归获取信号量,因此,对于非二值型信号量,在使用过程中可能会出现线程优先级翻转和线程死锁的问题。
系统内核提供以下信号量的API函数接口,如下图所示。
互斥量,顾名思义,就是一种相互排斥的信号量,是一种特殊形式的二值型信号量。这种情况就类似于一个停车位,当A汽车占据了停车位(获取到互斥量)的时候,其他汽车就不能获取该车位了,必须等A汽车离开该车位的时候,才能有机会争夺该车位。
互斥量跟信号量不同的是,互斥量只有两种状态值,对于拥有互斥量的线程,表示该线程已经拥有了该互斥量的所有权和控制权,该互斥量只能由该线程来进行释放,这样就可以解决线程递归获取互斥量出现的死锁问题。
对于信号量中存在的优先级翻转问题,在互斥量里面不会出现,这是因为互斥量里面实现了高优先级继承算法。优先级继承是通过在高优先级线程尝试获取共享资源而被挂起的期间内,将低优先级的线程的优先级提升到高优先级线程的优先级别,从而解决优先级翻转引起的问题。
系统内核提供以下互斥量的API函数接口,如下图所示。
事件集,也是线程同步的一种机制,但与信号量或互斥锁不同,事件集是可以实现一对多或多对多同步的。也就是说,一个线程发出一个(或多个)事件,一个(或多个)等待该事件的线程在获取到该事件后,就可以获得运行的权限。
事件集是使用一个32位无符号整型的变量来表示的,每一个位表示一个事件,这些事件可以是“逻辑与”或“逻辑或”的关系。换句话说,就是可以让一个线程等待一个事件(逻辑或)到达就执行,或者让一个线程等待所有事件(逻辑与)到达才执行。
事件集在某些场合里面是可以替代信号量的。但事件集与信号量不同,事件集的事件在清除之前,是不能累计的,也就是说,一个线程发送了多次同一事件,由于不能累计,也就相当于只发了一次该事件,直到该事件被清除。
系统内核提供以下事件集的API函数接口,如下图所示。
多线程同步的应用示例
多线程同步的应用示例,主要是为了验证信号量,互斥量,事件集的API接口函数,并且通过实验现象观察这三种线程同步方式的运行情况。
示例源码下载链接:
https://github.com/embediot/rtthread_study_notes
信号量示例主要演示了一个“生产者-消费者”的设计模式,生产者线程不断生产产品(数值加1)放入仓库(循环数组),消费者线程不断从仓库里面取出产品,仓库的读写操作都需要使用信号量的锁机制进行同步。
互斥量示例主要创建了三个动态线程,这三个动态线程不断争夺这个互斥量的使用权,通过实验现象可以观察到,持有互斥锁的线程的优先级,会通过优先级继承算法,调整到等待线程优先级中的最高优先级。
事件集示例主要初始化了一个事件集和两个线程,一个线程发送事件,另一个线程等待事件。等待事件的线程,分别使用了“逻辑与”和“逻辑或”这两种事件接收方式。
具体示例的实现可以查看工程源码,在synchronize.h头文件中,打开相应的宏定义开关,重新编译工程并下载到开发板即可。
线程间同步的注意事项
在进行多线程间同步的时候,关于信号量,互斥量,事件集这三种线程同步方式,有以下一些注意事项:
1、中断与线程间的互斥,不能采用信号量(锁)的方式,应该采用开关中断的方式。
2、资源计数类型的应用场景,多数都是混合方式的线程间同步,由于单个的资源处理存在线程的多重访问,因此需要对资源进行锁方式的互斥操作。
3、在使用信号量的时候,应该要注意优先级翻转的问题,合理安排任务的优先级。
4、不能递归获取信号量,否则有可能会造成“死锁”的情况。
5、线程不能长时间占用互斥量,在获得互斥量之后,不能再更改持有互斥量线程的优先级。
6、不能在中断服务程序里面使用互斥量。
7、事件集仅能用于线程同步,不能用于线程间传输数据。
8、事件集不会形成队列,在清除事件之前,发送多次跟发送一次都是同样的效果。