嵌入式系统架构浅谈:编程设计模式 (二)---嵌入并发和资源管理的设计模式
扫描二维码
随时随地手机看文章
嵌入并发,意味着多线程或者多任务,基本上都是使用了系统,linux系统或RTOS系统之类的实现。RTOS系统里任务的调度主要有抢占式和时间片调度两种,具体的区别这里就不详细说明了。此篇章包含了并发的一些术语,如并发性,临界性,资源,死锁等的概念。最好是详细阅读RTOS系统的书籍。
声明:文章基于《C嵌入式编程设计模式》这本书,英文是Design Patterns for Embedded Systems in C。主要是做个笔记,并添加一点个人的理解,分享出来与各位探讨。
1. 嵌入并发和资源管理的设计模式
总共有8个模式,前两个循环执行模式和静态优先级模式,提供了两个不同的方法来调度任务或线程。接下来3个模式临界区模式,守卫调用模式和队列模式,为了使解决在多任务环境下串行访问资源的问题。汇合模式讲的是多任务以不同的方式进行同步。最后两个模式是关注预防死锁问题。希望下面的模式能够各位一点启发。
1.1 循环执行模式
循环模式有非常简单的方式调用多个任务的特点,允许所有的任务有同等机会运行,但是不能及时响应紧急事件。一般在资源少的系统里面使用,避免了RTOS的开销,也不需要复杂的任务调度。简单就是最大的优点。
1.1.1 模式结构
CyclicExecutive有一个controlLoop()函数,可以反复调用每个任务的run操作。也需要等待任务的结束再调用下一个任务。
1.1.2 角色
1.1.2.1 抽象任务(AbstractCEThread)
通过声明run()函数为线程提供接口。它是用来循环执行的任务函数。
1.1.2.2 循环控制(CyclicExecutive)
这个类用于循环执行每个任务。此外也有全局栈和任务本身需要的静态数据。模式的一个变体是时间触发循环执行,在这个变体中,CyclicExecutive设置使用CycleTimer来开启每个周期。也就是使用这个变体可以在周期类执行每个函数。
1.1.2.3 循环定时器(CycleTimer)
图中有表示带有“0,1”,这个定时器是可选的。当定时器时间到时,可以调用中断或者返回TRUE给hasElapsed()函数。CyclicExecutive将调用start()为下一个周期开始计时。
1.1.2.4 具体任务实现(ConcreteCEThread)
每个ConcreteCEThread都有自己的run()函数,用于具体的任务实现。
1.1.3 效果
如前所述,该模式优点在于简单,一方面很难导致调度程序错误,另一方面对紧急事件响应不足,使得仅使用在内存小的设备。还有缺点是,任务间的通讯会值得考虑,比如一个任务需要另一个任务的数据,那么数据只能保存在全局的内存或共享资源中。我们尽量不要定义太多的全局变量,否则会难以管理维护,和造成内存的浪费。
1.1.4 实现
该模式的实现非常简单。在大多数情况下,循环执行可能仅是应用的main()函数中调用。
1.2 静态优先级模式
大多数的实时操作系统都是静态优先级模式。所以想要使用这个模式直接移植RTOS系统就好了。这里的模式复杂度和完整度是无法比得上RTOS系统的,不过阅读这里也可以使你对RTOS的任务调度有所了解,因为这是基于这个框架的。静态优先级模式能够为任务划分优先级,能够更好响应高优先级时间。
1.2.1 模式结构
除了右下角AbstraceStaticThread,SharedResource,ConcreteStaticThread这三个类,其他一般是由RTOS实现。
1.2.2 角色
1.2.2.1 抽象线程(AbstraceStaticThread)
是一个抽象类,提供run()函数给调度器运行。
1.2.2.2 具体线程(ConcreteStaticThread)
ConcreteThread作为AbstraceStaticThread具体实现run()函数。
1.2.2.3 互斥锁(Mutex)
是一个互斥的信号量类,用来串行访问SharedResource。当一个任务调用了互斥量的lock()函数,其他任务尝试锁定的同一个互斥量时候,会被阻塞,直到互斥量的解锁或超时退出。
1.2.2.4 队列(PriorityQueue)
PriorityQueue是根据优先级,对指向StaticTaskControlBlock的指针进行排序,也就是说队列里存储的其实每个线程的排队。一般在RTOS系统里,存在不止一个队列,有就绪队列,阻塞队列等,调度器会从就绪队列取出第一个执行。
1.2.2.5 资源(SharedResource)
该资源可能在一个或多个线程里共享,需要保证资源的正常,在下面模式会说明资源共享的问题。
1.2.2.6 栈(Stack)
每个AbstraceStaticThread都有一个栈用于返回地址和传递参数。
1.2.2.7 调度器(StaticPriorityScheduler)
最简单的法则:总是运行最高优先级的准备线程。RTOS系统里,任务创建,任务切换等都需要经过调度器。任务创建成功后,会把任务按优先级加入到就绪列表中,任务挂起就会加入到挂起列表。系统有个滴答时钟中断或其他能够进行任务切换,查找下一个运行的任务可以有通用方法,就是从就绪列表取。另一种是硬件方法,使用处理器自带的硬件指令来实现,需要硬件本身支持。
1.2.2.8 程序控制块(StaticTaskControlBlock)
包含了它相应的AbstraceStaticThread对象的调度信息。有线程的优先级,默认开始地址,目前地址,只要线程还在没被销毁,这个块就会伴随着存在。
1.2.3 效果
静态优先级模式能够对事件提供及时响应,可以对CPU大程序优化,避免单线程因等待时占用CPU这种浪费。因RTOS系统的支持,线程间通讯也有很多保证,邮箱,信号量机制,避免了过多的全局变量。
1.1.4 实现
最好的方式是直接移植成熟的RTOS系统来实现。使用这种模式,需要对前期开发有个设计,对内存分配,优先级分配等因素,需要在程序开发前有个规划,否则可能会造成后面存在各种问题。复杂度比单线程的高,所以需要你有个深入的理解,才能对RTOS系统运用掌握,但是也不用害怕,RTOS始终还是中小的系统,有时间可以研究源码,RTOS对指针,数据结构的运用非常的成熟高效。
1.3 临界区模式
临界区模式是任务协调最简单的方式。它直接禁止了任务的切换,在临界区内安全访问之后,再退出临界区。
1.3.1 模式结构
模式结构非常简单,在进入临界区后才访问资源。调度程序不参与临界区的开启和结束过程,知识提供服务禁止和重启任务切换。如果调度系统不提供,则临界区能够在硬件级别使用C的asm直接开关中断处理。
1.3.2 角色
1.3.2.1 临界区(CRShaaredResource)
使用这个元素来禁止任务切换,以防止任务同时访问资源。这个例子里,受保护资源是Value属性,相关的服务都必须使用临界区来保护,setValue()和getValue()函数必须独立实现临界区。
1.3.2.2 任务集合(TaskWithSharedResource)
这个元素代表所有想要访问共享资源的任务集。这些任务并不知道保护资源的方法,因为它被封装在共享资源内。
1.3.3 效果
模式特点就是禁止调度任务的切换,更严格的,禁止所有的中断。注意的是,一旦元素离开了临界区,将重启任务切换,另外使用了临界区,就注定会影响到其他任务的时序,所以尽量保证临界区的时间不要长。
1.3.4 实现
绝大多数的RTOS系统直接提供函数,调用即可。
1.4 守卫调用模式
守卫调用模式提供了锁定的机制串行访问,可以阻止当锁定后来自其他线程的调用资源。在RTOS系统里,直白的说就是信号量。使用这个模式可能会导致优先级导致,或死锁的问题发生。
1.4.1 模式结构
在模式下,多个PreemptiveTasks通过他们的函数访问GuardeResource。当一个线程调用一个正在锁定的信号量时,调度服务会把该线程加入到阻塞队列中,等待当那个信号量释放或超时时,解除阻塞。调度服务必须作为临界区实现信号量的lock()功能,以防止可能的竞争条件。
1.4.2 角色
1.4.2.1 共享资源(GuardedResource)
在这个类中使用互斥信号量来互斥访问。在访问资源之前,执行与Semaphore实例关联的lock()函数。如果Semaphore是在非锁定状态,则变为锁定;如果在锁定状态,则Semaphore会调度复位发信号阻塞这个任务。
1.4.2.2 任务(PreemptiveTask)
访问共享资源的任务。
1.4.2.3 互斥信号量(Semaphore)
它串行访问GuardedResource。lock()函数是用于访问资源之前,release()函数是访问资源后,调用释放信号量。
1.4.3 效果
该模式提供及时访问资源,并同时阻止多个能够导致数据损坏和系统错误行为的同时访问。如果资源没有上锁,那么访问资源并不会遭受到延迟。
1.4.4 实现
通过使用RTOS提供的信号量函数。一般都会提供创建信号量,摧毁信号量,上锁,解锁的接口。
1.5 队列模式
队列模式是任务异步通讯常见的实现。它提供了在任务间的通讯方式。发送者将消息队列Cyrus队列中,一段时间过后,接受者从队列取出消息。它也可以实现了串行访问共享资源,把访问消息排队,并且在稍后处理,这避免了共享资源同时访问的问题。
1.5.1 模式结构
QUEUE_SIZE声明决定队列能容纳最大的元素数目。必须足够大来处理最差的情况,也不要太大以免内存的浪费。
1.5.2 角色
1.5.2.1 消息(Message)
它可以任何东西,是简单的数据值,或发送消息的详细数据报结构。
1.5.2.2 消息队列(MessageQueue)
MessageQueue是QTasks间交换的信息存储。提供了getNextIndex()函数来运行计算下一个有效的索引值。insert()函数在头部位置将Message插入到队列中并更新头索引。remove()函数可以用于删除最旧的消息。iFull(),isEmpty()两个用来检测队列是否已满,是否为空。
1.5.2.3 互斥信号量(Mutex)
是互斥信号量,如静态优先级模式中的描述类似。
1.5.2.4 任务(QTask)
QTask是MessageQueue的客户,要么调用insert()插入新消息,要么调用remove()访问最早的数据。
1.5.3 效果
当数据在任务间传递,队列模式十分好用。互斥量可以确保队列本身不会由于同时访问造成损坏。相比守卫调用模式,队列模式接收数据不是很及时。
1.5.4 实现
队列的最简单实现是消息元素数组。有简单的优点,也会有灵活性不足,占用空间固定等缺陷。更多是使用链表的方式来实现队列。MessageQueue还可以添加多个缓冲区,每个优先级一个队列,这样实现优先级策略,或者基于消息优先级,通过插入元素队列中实现。在复杂的系统中,预测最佳队列大小是不可行的,如果使用数组实现队列的方式,会存在超出容量的问题。在这种情况下,可以额外使用一个缓冲队列在作为临时存储。
1.6 汇合模式
任务必须以不同的方式同步。发生同步可能是共享单一资源,或者等待信号量等造成,这些队列模式和守卫调用模式都能够实现。但是如果同步需要的条件更加复杂呢?汇合模式就是解决这个问题。当所有的任务都满足同步条件时,才能继续运行。
1.6.1 模式结构
需要同步的线程至少2个,同时拥有唯一的Rendezvous。
1.6.2 角色
1.6.2.1 聚合(Rendezvous)
用于管理同步。它通过两个方式:reset()函数重置同步标准为初始条件。synchronize()函数,当任务想要同步时调用这个方法。如果不满足标准,则任务阻塞。这个通常可以使用观察者模式或守卫调用模式实现。
1.6.2.2 计数信号量(Semaphore)
这个通常是计数信号量,有创建,摧毁,上锁和释放标准锁的接口函数。用于存储当前所有任务满足同步条件的数量。当等于预设值时,同步条件满足。
1.6.2.3 线程(SynchronizingThread)
代表使用Rendezvous同步的每个线程。
1.6.3 效果
在这个模式中,两个或更多的任务都同时满足某个条件时,才能继续运行或调用回调函数。
1.6.4 实现
该模式可以通过前面的观察者模式,或者守卫调用模式实现。如果使用的是观察者模式,则任务必须使用函数的地址注册,当满足同步条件时调用。如果使用的是守卫调用模式,则每个Rendezvous对象拥有唯一的信号量,任务想同步时调用synchronize()函数告知给Rendezvous,当Rendezvous满足同步条件时,释放信号量,并且任务随后根据通常的调度策略全部释放运行。
1.7 同时锁定模式
首先不考虑软件自身导致的错误,发生死锁需要满足4个条件:
-
互斥锁资源。
-
当请求其他资源时,一些资源已经锁定。
-
当资源锁定是允许抢断。
-
存在循环等待条件。
死锁能够通过打破这4个条件的任意一个避免。使用临界区模式打破的是条件1和条件3。队列模式避免了条件1的发生。
同时锁定模式是通过破坏条件2达到避免死锁的目的。模式以全或无的形式工作。要么所有需要的资源一次都锁定,要么都没有锁定。简单来说在线程需要某个资源的时候,只有把所有的资源都一起上锁成功,才能成功往下执行,这样就避免了两个线程都在请求对方的资源造成的死锁。
1.7.1 模式结构
一般来说,MultimasteredResource是不同资源集合的任意数目的一部分,其中ResourceMaster的单独实例管理一个这样的集合。
1.7.2 角色
1.7.2.1 资源管理(MultimateredResource)
这个元素通过多个ResourceMasters管理,然后他有自己的互斥信号量来避免同时申请锁。使用QueryMutex必须通过tryLock()函数,以便能够通过ResourceMasters决定所有的尝试锁定将会是成功或者失败。
1.7.2.2 互斥量(QueryMutex)
这个算数是一个正常的互斥信号量,与之前的不用,它提供了tryLock()函数。这个函数和lock()函数目的是一样的,都是为了上锁,只是tryLock()函数除此之外,如果锁失败,他将会返回一个错误代码,而不是阻塞当前的线程。
1.7.2.3 客户(ResourceClient)
这个元素是一个客户,想要一次访问所有资源集合来避免死锁。它直接访问MultimateredResource,直到成功接收到ResourceMaster上的锁。在使用完资源后释放。
1.7.2.4 控制锁(ResourceMaster)
ResourceMaster控制整个资源合集的锁。
1.7.3 效果
同时锁定模式通过消除必要条件2,通过一次锁定所有需要的资源或一个都不锁防止死锁。但是这样会增加了其他任务执行的延时,而且很可能发生在甚至没有实际资源的冲突下。在资源更多,更广泛时出现这种情况更明显。此外,模式不能解决优先级倒置问题,事实上可能更严重。
1.7.4 实现
需要保证tryLock()函数错做之前确保成功锁定MultimasteredResource。
1.8 排序锁定
排序锁定是另一种确保死锁不会发生的方法,这次是用过防止条件4发生。通过对资源排序,并且需要客户总是按照那个指定的顺序锁定资源,这样就不可能形成循环等待条件。
1.8.1 模式结构
1.8.2 角色
1.8.2.1 锁(Mutex)
与上面的模式一样,提供两个基本的函数lock()和release()。
1.8.2.2 资源管理(OrderedResource)
这个是模式的核心。它有resourceID属性,是一个唯一的与每个资源关联的ID,并且与ResourceList关联。这个类执行的排序锁定规则永远是:如果资源的resourceID大于任意已锁定资源最大的resourceID,则资源仅能被锁定。ResourceClient首先需要调用lockDyadic(),然后添加到资源列表中,在对资源操作完成之后,调用releaseDyadic()函数。书上把这种访问称作为二元的,与二元不一样的一元,差异在一元是在内部完成上锁,使用资源,解锁。而二元是可以保持在锁的状态,等到资源使用完之后在释放。
1.8.2.3 客户(ResourceClient)
代表了想要调用OrderedResource服务的元素集合。对于客户,不需要知道关于resourceID本身的任何东西。
1.8.2.4 资源列表(ResourceList)
在这个元素里,如果传递的resourceID大于已锁定资源的最大一个,则addLock()返回成功。否则返回失败。
1.8.2.5 已使用资源(ResourceReference)
这仅是一个在有序列表中包含的resourceID数组。只是保存一个最大值是不够的,因为很多资源可能在任何时候锁定。
1.8.3 效果
模式通过确保所有的客户按相同的顺序锁定资源来消除死锁。这个模式需要在设计时做好分析来规划好资源的排序。例如现在有两个线程,都需要用到资源A,B,C,如果线程1按A,B,C的顺序锁定,线程2按C,B,A的顺序锁定,就有可能发生死锁。因此该模式就是为了让资源都按照规定的序列来锁定。
1.8.4 实现
模式的实现需要给每个OrderedResource增加额外的resourceID,并且在ResourceList的逻辑中确保每个OrderedResource的resourceID大于任意当前所的resourceID。还有,已经上锁的resourceID的列表必须维护,当OrderedResource释放时,可以适当地锁定其他。
ResourceList最常见的实现是一个按照锁定顺序表示的resourceID整形数组。
免责声明:本文内容由21ic获得授权后发布,版权归原作者所有,本平台仅提供信息存储服务。文章仅代表作者个人观点,不代表本平台立场,如有问题,请联系我们,谢谢!