# 嵌入式 架构中多线程的同步与互斥设计 **Repository Path**: allankun/semaphore ## Basic Information - **Project Name**: 嵌入式 架构中多线程的同步与互斥设计 - **Description**: 给大家分享一个自己对同步与互斥的总结,该总结是我在工作中实际应用过的场景介绍。希望对大家有帮助。 - **Primary Language**: C++ - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2022-03-26 - **Last Updated**: 2022-03-26 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 嵌入式架构中多线程的同步与互斥设计 ## 前言 为了提高处理效率和速度,一般的程序设计中采用了多线程技术,但是由于有的时候存在多个线程同时访问或者处理一个共同区域的情况,如图1所示,线程A和线程B都可以对共同区域进行设定操作,而线程C则从共同区域读取设定的内容,然后根据设定的内容来执行对应的处理,当共同区域只能存储一个设定内容时,如果不使用互斥控制,那么,线程A设定的内容还没有等线程C来读取的时候,就被线程B设定的内容给覆盖掉了。为了防止互相干扰,需要对线程进行互斥控制或者说是排他控制。嵌入式架构如何实现互斥控制,都有哪些类,使用什么方法,本文将进行详细介绍。 ![输入图片说明](%E5%89%8D%E8%A8%80.png) ## 信号量 如果嵌入式设备使用的时VxWorks操作系统,因此有必要对VxWorks的信号量进行讨论。VxWorks有三种信号量。 1. 二进制信号量 2. 互斥信号量 3. 计数信号量 ### 二进制信号量 可以完成同步和互斥的操作。VxWorks提供了如下接口可以操作互斥信号量。 semBCreate() :生成二进制信号量 semDelete() :删除信号量 semGive() :释放信号量 semTake () :取得信号量,取不到时,阻塞线程继续执行。 在架构中,把以上接口进行了封装成了Semaphore类。如果使用Semaphore类的话,其实就是在使用VxWorks的二进制信号量。 ### 同步使用方法: 举例说明,如图2,对两个线程,进行同步操作,线程A和线程B分别开始执行,两个线程都操作同一个二进制信号量。线程B执行过程中调用了semTake()来取得二进制信号量,但是由于信号量没有被释放过,取得失败,线程B进入了阻塞。由于线程A执行过程中调用了semGive()释放了二进制信号量,阻塞中的线程B取得了信号量,阻塞解除, 得以继续执行。完成了两个县城间的同步操作。 **技巧:需要同步的两个线程,同时处理同一个二进制信号量,一个负责释放,一个负责取得。** ![输入图片说明](%E7%BA%BF%E7%A8%8B%E9%98%BB%E5%A1%9E.png) 示例代码: ```C #include #include #include #include sem_t sem; //全局二进制信号量变量 // 打印机 void printer(char *thread_name, char *str) { printf("%s : ",thread_name); while(*str!='\0') { putchar(*str); fflush(stdout); str++; sleep(1); } printf("\n"); } void *thread_fun_1(void *arg) { semTake(&sem); // 信号量取得 char *str = "hello"; printer("thread_1",str); printf("thread_fun_1 end\n"); } void *thread_fun_2(void *arg) { char *str = "world"; printer("thread_2",str); printf("thread_fun_2 end\n"); semGive(&sem); // 信号量開放 } // tid2线程使用完打印机后,tid1线程才可以使用打印机 int main(void) { pthread_t tid1, tid2; //二进制信号量创建,初始默认值是0 semBCreate (&sem,0,0); pthread_create(&tid1, NULL, thread_fun_1, NULL); pthread_create(&tid2, NULL, thread_fun_2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); //二进制信号量删除 semDelete (&sem); return 0; } ``` ### 互斥使用方法: 举例说明,如图3,对两个线程,进行互斥操作,线程A和线程B分别开始执行,由于两个线程都操作同一个共同区域,使用二进制信号量的来互斥操作。线程B执行过程中调用了semTake()来取得二进制信号量,开始操作共同区域,操作完后semGive()释放了二进制信号量,在线程B操作共同区域期间,线程A调用了semTake()来取得二进制信号量, 但是由于信号量没有被释放过,取得失败,线程A进入了阻塞,等线程B释放信号量后,线程A取得了信号量,开始操作共同区域,操作完后semGive()释放了二进制信号量,完成了分别操作共同区域。 **技巧:使用取得信号量和释放信号量把需要互斥访问的区域包围起来。成对出现。** ................ semTake(&sem); // 互斥访问区域 semGive(&sem); ............... ![输入图片说明](%E7%BA%BF%E7%A8%8B%E4%BA%92%E6%96%A5.png) 示例代码: ```C #include #include #include #include sem_t sem; void printer(char *thread_name, char *str) { semTake(&sem); // 信号量取得 printf("%s : ",thread_name); while(*str!='\0') { putchar(*str); fflush(stdout); str++; sleep(1); } printf("\n"); semGive(&sem); // 信号量開放 } void *thread_fun_1(void *arg) { char *str = "hello"; printer("thread_1",str); printf("thread_fun_1 end\n"); } void *thread_fun_2(void *arg) { char *str = "world"; printer("thread_2",str); printf("thread_fun_2 end\n"); } // tid2和 tid1线程分别通过信号量来互斥访问printer int main(void) { pthread_t tid1, tid2; //二进制信号量创建,初始默认值是1 sem_init(&sem,0,1); pthread_create(&tid1, NULL, thread_fun_1, NULL); pthread_create(&tid2, NULL, thread_fun_2, NULL); pthread_join(tid1, NULL); pthread_join(tid2, NULL); sem_destroy(&sem); return 0; } ``` ### 互斥信号量 特殊的二进制信号量,是对二进制信号量的优化,只能完成互斥的操作。 VxWorks提供了如下接口可以操作互斥信号量。 semMCreate() :生成互斥信号量 semDelete() :删除信号量 semGive() :释放信号量 semTake () :取得信号量,取不到时,阻塞线程继续执行。 在架构中,把以上接口进行了封装成了MutexLock类。如果使用MutexLock类的话,其实就是在使用VxWorks的互斥信号量。 ### 互斥使用方法: 请参照二进制信号量的互斥使用方法。 ### 计数信号量 就是在二进制信号量基础上,能够记录释放的次数. VxWorks提供了如下接口可以操作互斥信号量。 semCCreate() :生成互斥信号量 semDelete() :删除信号量 semGive() :释放信号量 semTake () :取得信号量,取不到时,阻塞线程继续执行。 在架构中,把以上接口进行了封装成了Semaphore类。如果使用Semaphore类的话,其实就是在使用VxWorks的二进制信号量。 可以使用计数信号量来完成同步和互斥,具体可以参照二进制信号量的说明. ## Monitor 架构中,设计的Monitor这个类,它和Semaphore类以及MutexLock类有一个共同的功能,就是都具备多线程的互斥功能。但是Monitor这个类还有它自己的特点。 它有如下功能: 1. 让线程进入Monitor 2. 让线程退出Monitor 3. 让线程在Monitor中等待 4. 通知Monitor中等待状态的线程,解除等待,开始取得信号量。 架构中的Monitor类,是对MutexLock类和计数信号量合并使用的。 让线程进入Monitor和让线程退出Monitor,是通过MutexLock来进行互斥。 让线程在Monitor中等待和通知Monitor中等待状态的线程,解除等待,是通过计数信号量来进行控制。 ### MessageQueue 架构中,设计的MessageQueue这个类,这个类维护着一个固定长度的字符数组。设计这个类的目的就是在多线程通信时使用这个类的对象就可以完成了多线程之间的通知操作。 MessageQueue类有两个重要的接口,一个是send接口,一个是receive接口。 send接口是用来把消息写入固定长度的字符数组中。 receive接口是用来读取固定长度的字符数组中的消息。 因为是多线程,所以需要在send接口和receive接口中进行互斥处理,MessageQueue类使用了Monitor进行互斥。 一般的使用场景如下所示。 线程1和线程2和线程3负责向数组发送消息,线程4负责从数组接收消息,根据接收到的消息,线程4去实行对应的操作。 ![输入图片说明](MessageQueue.png) MessageQueue类的对象最好由创建线程4的类来生成,并且根据消息种类来创建各个的发送消息的接口,供线程1和线程2和线程3来同步调用。在线程4的run函数中来执行receive接口,负责接收消息。由于使用了Monitor来互斥访问,这样做到了只要有消息,线程4就一直接收消息并及时处理,没有消息时,线程4立即停止接收,等待消息进入。而只要固定长的数组还能接收消息的话,线程1和线程2和线程3就一直向数组里发消息,直到数组无法接收为止,这时线程1和线程2和线程3会等待数组出现空余空间。 Monitor在MessageQueue中是如何发挥作用的? 最开始,固定长数组为空,线程4进入Monitor后,由于没有消息,在Monitor中进入等待状态,并且交出MutexLock, 这时的等待是由计数信号量引起的。 然后,线程1或线程2或线程3之一的一个线程抢到MutexLock,没抢到的继续等待,抢到的线程则判断数组是否可以写入,如果可以写入,则把消息写入,通知在Monitor中可能存在的等待状态的读取线程,让它解除等待,开始读取。 如果不可以写入,则说明数组已经满了,当前这个写入的线程在Monitor中主动进入等待状态,等待读取线程把数组全部读取到空时,来通知它解除等待,开始写入。 这就是Monitor的特别之处,它本身由两个信号量来维护的。但是对于数组的本质操作仍然是互斥的。 ## 总结 下图是架构的设计类与VxWorks的信号量的关系图。 互斥(排他):Semaphore MutexLock Monitor 同步:Semaphore ![输入图片说明](%E6%80%BB%E7%BB%93.png) 如果只是需要互斥时首选MutexLock类 ## 嵌入式系统的互斥说明 嵌入式系统中MutexLock类来负责互斥操作。 XXXXXX/include/foundation_class/MutexLock.h XXXXXX/foundation_class/MutexLock.cpp MutexLock.cpp类中主要实现了以下操作。 1. 创建一个MutexID MutexLock_CREATE(ret, mutexLockId, (SEM_Q_PRIORITY | SEM_INVERSION_SAFE)) 2. 释放一个MutexID MutexLock_DELETE(ret, mutexLockId); 3. 对一个MutexID上锁, 函数名mutexLock() MutexLock_TAKE(ret, mutexLockId, WAIT_FOREVER); WAIT_FOREVER表示没有TimeOut时间,一直等待。 4. 对一个MutexID解锁,函数名mutexUnLock() MutexLock_GIVE(ret, mutexLockId); ### 外部模块调用的方式 使用前生成一个MutexLock的对象MutexObject。 在共享代码区域如下调用 MutexObject-> mutexLock () XXXXXXXXXX XXXXXXXXXX MutexObject-> mutexUnLock() 使用完之后,释放这个对象。 ### 利用场景 当有多个线程同时访问同一个代码区域的话,就需要在被访问的代码区域的前后,需要使用Mutex来加以控制。以防止由于多个线程的访问,导致代码区域的内容或动作出现混乱。 例如:线程A和线程B都需要访问线程C的一段代码(共同区域),如果线程C的共同区域没有用Mutux来进行访问控制的话,就像下面这样,线程A设值完了之后,当再次执行取值操作之后,取出的值已经被线程B给重写了。如果线程A只想利用线程C来临时保存自己的值得话,那么下面的处理肯定就出错了。 ![输入图片说明](mutex.png)