The increasing scale of code and multi-threading technology based on RTOS has made embedded software development more focused on “concurrency control and thread safety.” When multiple execution threads (referring to any context running code, including threads and interrupt service routines) need to access the same shared resources (including software data and hardware resources), errors may occur due to race conditions.
These errors are easy to create but hard to find, and ensuring correctness from the design perspective is more beneficial.
Causes of Race Conditions
A race condition occurs when a shared resource is accessed by multiple execution threads in a “non-atomic” manner, leading to errors because one execution thread’s operation is interrupted by another execution thread. Shared resources include hardware devices and software entities.
The most obvious shared resource is a global variable. If we define the following array, which is read and written by multiple threads, it will become the easiest to understand race condition.
int g_aiGlobalBuf[100];
Another less obvious shared resource is caused by non-reentrant functions. If multiple threads call the following function, a race condition will also occur.
void ProcGlobalBuf(int iPos, int iVal)
{
g_aiGlobalBuf[iPos] = iVal;
}
Using hardware resources by multiple threads can also introduce race conditions. For example, if the following function (using pseudo-code) is called simultaneously by two threads, the data from the first thread may not have been fully sent before being “overwritten” by the second thread’s data.
void SendByDMA(const void *p_vBuf, int iSize)
{
DMA.StartAddr = p_vBuf;
DMA.Count = iSize;
EnableDMA();
}
In short: any resource used by multiple execution threads may produce a “race condition.”
Concurrency Control Rules
Rule One: Avoid Resource Sharing Whenever Possible.
If there is no concurrent access, race conditions cannot occur. Therefore, the design of the code should involve minimal sharing. The most obvious application of this idea is to avoid using global variables. If we place resources where multiple execution threads can find them, there must be sufficient justification.
For example, in hardware resource allocation, the serial port can only be called by Thread 1, the network port can only be called by Thread 2, and the LCD can only be called by Thread 3. This eliminates race conditions for these three hardware resources from the start, and the same principle can be applied to software resources.
Rule Two: Shared Resources That Cause Race Conditions Must Be Locked.
However, in the world of computers, sharing is a reality. Anytime hardware or software resources are shared outside a single execution thread, because another thread may produce inconsistent observations of that resource, access to that resource must be explicitly managed.
Read-only data (such as the chip’s serial number) appears consistent to any execution thread that accesses it, thus race conditions cannot occur.
In an embedded environment based on RTOS, there are generally three ways to lock shared resources: disabling interrupts, using semaphores, and preventing task switching.
Disabling interrupts is applied in two situations: if task code and interrupt routines share resources, or if the access time to shared resources is very short (such as operating a variable); if only threads are accessing some shared resources and the operation time is not too long, preventing task switching is sufficient; in all other cases, semaphores will be used.
Rule Three: Functions Called by Multiple Threads Must Be Reentrant.
To determine whether a function is reentrant, consider the following rules:
A reentrant function generally uses variables in an atomic manner unless those variables are stored in the stack of the caller or are private variables of the task.
A reentrant function generally does not call other non-reentrant functions.
A reentrant function generally does not use hardware in a non-atomic manner.
Rules for Using Locks
When we create an object that can be accessed in parallel, we should also define locks to control access. The locking pattern must be arranged from the start; otherwise, subsequent improvements will be very difficult.
If a function that acquires a lock calls other functions that also attempt to acquire that lock, our code will deadlock, meaning the lock holder is not allowed to acquire the lock a second time; if attempted, the system will hang.
Functions provided for external calls must explicitly handle locking. When writing internal functions that assume the caller has handled locking, we should clearly state this assumption; otherwise, months later when we look back at this code, it will be hard to remember whether a lock is required when calling a particular function.
Avoid situations where multiple locks are needed as much as possible. If multiple locks are indeed needed, prevent deadlocks: always acquire locks in the same order and understand how other code operates on locks; first acquire local locks and then acquire other locks; when holding locks, avoid suspending threads as much as possible, as this can lead to a decrease in real-time performance and even permanently suspend the system.
Original link: https://blog.csdn.net/jiangjunjie_2005/article/details/30713585
For learning reference and knowledge dissemination only, copyright belongs to the original author. If there is any infringement, please contact for deletion. Thank you!
Previous Recommendations
Essentials | Analysis of Program Auto-Start
What are the current mainstream small embedded GUIs?
A practical software framework for MCU without OS
Application of Table-Driven Method in Embedded Product Development
OTA Remote Upgrade of BootLoader Based on STM32
Efficient, Memory-Saving, Any Format Queue
Embedded Project Generator, Check it Out!
A Clear Idea for Writing LCD Drivers (with Code Analysis)
An Efficient Method for Merging BootLoader and APP Firmware
Practical | A Simple and Easy-to-Use Menu Framework
Data Abstraction Example Based on Simulated I2C (with Code)
How to Handle Endianness in Custom Protocol Parsing and Packing?
What are the Different Ways to Send Data via Serial Port?
Sharing a CPP Open Source Project Suitable for Embedded Systems
Not Sure Where to Start Learning Embedded Systems? Perhaps Start from These Knowledge Points.