https://blog.csdn.net/jiangjunjie_2005/article/details/30713585
嵌入式环境下并发控制与线程安全
代码规模日益增大和基于RTOS的多线程技术使嵌入式软件开发越来越关注“并发控制和线程安全”,当多个运行线程(指正在运行代码的随意上下文,包含线程和中断服务程序)须要訪问相同的共享资源时(包含软件数据和硬件资源)。就可能由于竞态而导致错误。
这样的错误easy制造,但非常难找到,从设计上保证正确性收益更大。
竞态的产生
当一个共享资源被多个运行线程“非原子性”訪问时,一个运行线程的操作被还有一个运行线程打断而带来错误就叫“竞态”,共享资源包含硬件设备和软件实体。
最明显的共享是全局变量,假设定义例如以下数组,它被多个线程读和写,这将成为最易理解的竞态。
int g_aiGlobalBuf[100];
还有一种不太明显的共享是由不可重入函数造成的,假设多个线程调用了以下的函数,那么相同产生竞态。
void ProcGlobalBuf(int iPos, int iVal)
{
g_aiGlobalBuf[iPos] = iVal;
}
硬件资源被多个线程使用也会引入竞态,如以下的函数(使用伪代码)被2个线程同一时候调用,那么第一个线程的数据可能还没有发送完成就被第二个线程的数据“覆盖”。
void SendByDMA(const void *p_vBuf, int iSize)
{
DMA.StartAddr = p_vBuf;
DMA.Count = iSize;
EnableDMA();
}
一句话:不论什么被多个运行线程使用的资源都可能产生“竞态”。
并发控制规则
规则一仅仅要可能。就应该避免资源的共享。
假设没有并发訪问,就不会有竞态的产生。因此,设计代码应该具有最少的共享。
这样的思想的最明显应用是避免使用全局变量。假设我们将资源放在多个运行线程都会找到的地方,则必须有足够的理由。
比如硬件资源的分配中,串口仅仅能由线程1调用。网口仅仅能由线程2调用。LCD仅仅能由线程3调用。那么这3种硬件资源的竞态一開始就杜绝了。同理能够应用到软件资源上。
规则二引发竞态的共享资源必须加锁
然而。在计算机的世界里共享就是现实的生活,在单个运行线程之外共享硬件或软件资源的不论什么时候,由于另外一个线程可能产生对该资源的不一致观察,就必须显示地管理对该资源的訪问。
像仅仅读数据(如芯片的序列号)对于不论什么訪问它的运行线程看来都是一致的,那么竞态是不可能发生的。
基于RTOS的嵌入式环境下对共享资源的加锁一般有3种途径:关中断、使用信号量、禁止任务切换。
关中断应用于2种情况:假设任务代码和中断程序共享资源,或者共享资源訪问时间非常短(如操作一个变量);假设仅线程之间訪问一些共享资源且操作时间不太长,禁止任务切换能够胜任;其它情况下都将使用信号量。
规则三被多线程调用的函数必须是可重入
一个函数是否是可重入的推断规则:
一个可重入函数一般用原子的方法使用变量,除非这些变量存储在调用这个函数的堆栈中或这些变量是任务的私有变量。
一个可重入函数一般不调用其它的不可重入的函数。
一个可重入函数一般不用非原子的方法使用硬件。
锁的使用规则
当我们创建一个可被并行訪问的对象时,应该同一时候定义用来控制訪问的锁。
锁定模式必须在一開始就安排好。否则其后的改进将会非常困难。
假设某个获得锁的函数要调用其它相同试图获取这个锁的函数,我们的代码就会死锁,即不同意锁拥有者第二次获得这个锁。假设试图这么做,系统将持起。
提供给外部调用的函数必须显示地处理锁定,在编写那些假定调用者已处理了锁定的内部函数时。我们自己应该显示地说明这样的假定,否则几个月后再回头来看这些代码时,非常难记清在调用某个特定函数时是否须要拥有锁。
尽可能避免出现须要多个锁的情况,实在须要多个锁就要防止死锁的发生:始终以相同的顺序获得锁,而且了解其它代码操作锁的做法;先获取自己局部锁。再获取其它锁。在拥有锁时尽量避免线程被挂起,那样会导致实时性下降,甚至永久性挂起系统。