嵌入式环境下并发控制与线程安全

💡 原文中文,约1800字,阅读约需5分钟。
📝

内容提要

嵌入式软件开发越来越注重并发控制和线程安全。竞态条件的错误往往难以察觉,因此在设计阶段保证正确性的收益更为显著。竞态条件的产生包括共享资源被多个线程访问以非原子操作的方式,以及不可重入函数的使用。锁的使用规则包括定义用来控制访问的锁,并避免死锁的发生。

🔎

延伸解读

设计阶段预防竞态更划算

文章指出竞态条件错误往往难以察觉,因此设计阶段保证正确性收益更显著。这意味着开发者应在架构和编码初期就识别共享资源,并规划并发控制策略,而不是等到测试阶段才排查难以复现的并发缺陷。提前预防能降低调试成本,并避免后期修改锁定模式带来的困难。

共享资源不限于全局变量

除了全局变量,不可重入函数和硬件资源(如DMA)也是常见的竞态来源。例如多个线程调用同一函数操作全局缓冲区,或同时使用DMA导致数据覆盖。这提醒开发者,任何被多个执行线程使用的资源都可能引发竞态,需全面审视函数和硬件访问路径,而非仅关注显式共享的数据。

加锁方式需按场景选择

文章给出三种加锁途径:关中断、禁止任务切换和使用信号量。关中断适用于任务与中断共享资源或访问时间极短的情况;禁止任务切换适用于仅线程间访问且操作时间不长的场景;其他情况则使用信号量。开发者应根据共享资源的类型、访问时长和参与线程来选择合适的机制,以平衡实时性与安全性。

锁的设计要提前并防死锁

文章强调创建可并行访问的对象时应同时定义锁,且锁定模式需一开始安排好。为避免死锁,应尽可能避免需要多个锁;若必须使用,则始终以相同顺序获取锁,先获取局部锁再获取其他锁,并在拥有锁时避免线程被挂起。这些规则有助于保持系统实时性,防止永久性挂起。

❓

Q&A

嵌入式开发中为什么要在设计阶段就重视并发控制和线程安全?

因为竞争条件的错误往往难以察觉,在设计阶段保证正确性的收益更为显著。随着代码规模增大和基于RTOS的多线程技术普及,多个执行线程(包括线程和中断服务程序)访问共享资源时容易引发竞态条件,导致错误。

竞态条件是怎么产生的?能举几个嵌入式中的常见例子吗?

竞态条件产生于多个执行线程以非原子操作的方式访问共享资源,一个线程的操作可能被另一个线程打断。常见例子包括:多个线程读写全局变量(如全局数组)、调用不可重入函数(如直接操作全局缓冲的函数)、以及多个线程同时使用硬件资源(如DMA发送时数据被覆盖)。

在嵌入式RTOS环境下,给共享资源加锁有哪几种方式?分别适用于什么情况?

一般有三种途径:关中断、使用信号量、禁止任务切换。关中断适用于任务代码和中断程序共享资源,或共享资源访问时间很短(如操作一个变量)的情况;禁止任务切换适用于仅线程之间访问共享资源且操作时间不太长的情况;其他情况下使用信号量。

如何判断一个函数是否可重入?编写可重入函数要注意什么?

可重入函数一般用原子的方法使用变量(除非变量存储在调用函数的堆栈中或为任务私有变量),一般不调用其他不可重入的函数,一般不用非原子的方法使用硬件。编写时应遵循这些规则,确保多线程调用时不会产生竞态。

使用锁时如何避免死锁?有哪些最佳实践?

避免死锁的实践包括:不允许锁拥有者第二次获得同一个锁;提供给外部调用的函数必须显式处理锁定,内部函数应说明调用者是否需持有锁;尽可能避免需要多个锁,若需要则始终以相同顺序获得锁,先获取局部锁再获取其他锁,并在拥有锁时尽量避免线程被挂起。

为什么说避免资源共享是并发控制的首要规则?具体怎么做?

如果没有并发访问,就不会有竞态产生,因此设计代码应具有最少的共享。最明显的应用是避免使用全局变量;对于硬件资源,可以分配为仅由特定线程调用(如串口只由线程1调用,网口只由线程2调用,LCD只由线程3调用),从而从一开始杜绝竞态。

🏷️

标签

➡️

继续阅读