以下的代码段在两个任务中同时对一个全局变量++、-- 5000000 次数因为对变量的计算结果是多少呢?
volatile int32_t counter = 0;
void start_task(void *pvParameters)
{
(void)(pvParameters);
int add = 5000000;
while(1)
{
if(--add)
{
counter++;
}
else
{
break;
}
}
while(1);
}
void start_task1(void *pvParameters)
{
(void)(pvParameters);
int sub = 5000000;
while(1)
{
if(--sub)
{
counter--;
}
else
{
break;
}
}
while(1);
}在 ARM 架构下,如果++和--操作不是原子操作(这是多数情况下的实际情况),两个任务同时对全局变量counter进行 5000000 次自增和自减,最终结果不会是 0,而是一个不确定的非零值(可能为正、负或零,但大概率非零)。
关键原因分析:
counter++和counter--在 ARM 架构下通常会被编译为多步指令(非原子操作),大致流程为:
从内存读取counter的值到寄存器(load)。
寄存器中的值加 1 或减 1(modify)。
将寄存器中的结果写回内存(store)。
当两个任务并发执行时,可能出现指令交错,导致数据覆盖。例如:
任务 1 读取counter=0,准备执行+1。
任务 2 同时读取counter=0,准备执行-1。
任务 1 写入结果1,任务 2 随后写入结果-1。
最终counter=-1,但实际执行了 1 次+1和 1 次-1,理论上应为 0,却因交错导致结果错误。
随着操作次数增加(5000000 次),这种交错会累积,导致最终结果偏离 0,且每次运行结果可能不同(不确定性)。
本地代码的运行结果如下:

为了解决上述问题我们可以使用FTOS 的 互斥锁来保护上述临界资源,我么也可以使用ARM LDREX/STREX 指令来实现原子操作来避免上述问题。ARM 中 ldrex 和 strex 通过独占访问监控器实现原子操作。ldrex 读取内存时标记该位置为当前处理器独占;strex 仅在该位置仍保持独占状态时写入成功,否则失败。
LDREX 指令描述如下:

STREX 的指令描述如下

有了上述认识我们通过LDREX 和 STREX 就可以实现原子操作更新内存,添加如下代码来原子访问内存
__attribute__((always_inline)) static inline rt_atomic_t __LDREXW(volatile rt_atomic_t *addr)
{
rt_atomic_t result;
__asm volatile ("ldrex %0, %1" : "=r" (result) : "Q" (*addr) );
return result;
}
__attribute__((always_inline)) static inline rt_atomic_t __STREXW(volatile rt_atomic_t value, volatile rt_atomic_t *addr)
{
rt_atomic_t result;
__asm volatile ("strex %0, %2, %1" : "=&r" (result), "=Q" (*addr) : "r" (value) );
return result;
}
rt_atomic_t rt_hw_atomic_add(volatile rt_atomic_t *ptr, rt_atomic_t val)
{
rt_atomic_t oldval;
do
{
oldval = __LDREXW(ptr);
} while ((__STREXW(oldval + val, ptr)) != 0U);
return oldval;
}
rt_atomic_t rt_hw_atomic_sub(volatile rt_atomic_t *ptr, rt_atomic_t val)
{
rt_atomic_t oldval;
do
{
oldval = __LDREXW(ptr);
} while ((__STREXW(oldval - val, ptr)) != 0U);
return oldval;
}修改上述测试代码继续验证
void start_task(void *pvParameters)
{
(void)(pvParameters);
int add = 5000000;
while(1)
{
if(--add)
{
rt_hw_atomic_add(&counter,1);
}
else
{
break;
}
}
while(1);
}
void start_task1(void *pvParameters)
{
(void)(pvParameters);
int sub = 5000000;
while(1)
{
if(--sub)
{
rt_hw_atomic_sub(&counter,1);
}
else
{
break;
}
}
while(1);
}通过LDREX 和 STREX 保护变量的访问后上述代码的执行结果为0,和预期的保持一致。

我要赚赏金
