C语言中的自增操作在多线程环境下是否安全?

16

我在FreeRTOS(FreeRTOSV7.4.0\FreeRTOS\Source\tasks.c)中找到了一些代码:

void vTaskSuspendAll( void )
{
    /* A critical section is not required as the variable is of type
    portBASE_TYPE. */
    ++uxSchedulerSuspended;
}

显式地指明无需保护是因为类型是"portBASE_TYPE",这是一个"long"类型。我的理解是它假设自增操作对于这种类型是原子的。但是,在我反汇编之后,我没有找到任何证据,只是简单的load->add->store。那么这是个问题吗?

void vTaskSuspendAll( void )
{
        /* A critical section is not required as the variable is of type
        portBASE_TYPE. */
        ++uxSchedulerSuspended;
 4dc:   4b03            ldr     r3, [pc, #12]   ; (4ec <vTaskSuspendAll+0x10>)
 4de:   f8d3 2118       ldr.w   r2, [r3, #280]  ; 0x118
 4e2:   1c50            adds    r0, r2, #1
 4e4:   f8c3 0118       str.w   r0, [r3, #280]  ; 0x118
 4e8:   4770            bx      lr
 4ea:   bf00            nop
 4ec:   00000000        .word   0x00000000

000004f0 <xTaskGetTickCount>:
        return xAlreadyYielded;
}

10
这并非由C语言定义,而是取决于编译器/硬件平台。 - Oliver Charlesworth
1
在处理线程安全时采用的一个好策略是:如果有即使是微小的竞态条件出现的可能性,那么它就一定会发生。这并没有回答你的问题,但如果你发现自己在想“这个操作几乎是原子性的,所以我不需要锁定”,那么它很有用,可以让你放心地意识到锁定的重要性。 - paddy
1
最终,我找到了这里为什么是安全的原因。谢谢大家! 链接 - user1603164
当FreeRTOS在posix模拟器下运行时,其代码是不线程安全的。我已经进行了检查,上述糟糕结果导致了valgrind报告了数百个并发错误。然而有趣的是,它仍然大部分时间能够正常工作。我猜这更多是侥幸而非设计所致。(但在ARM上应该没问题。尽管如此,我仍认为应该添加一些内容,使其在线程安全性要求显式的平台上变得线程安全) - Martin
FreeRTOS有许多端口,但这个可能是错误的。如果需要portBASE_TYPE是原子的,请尝试使用char(它只能保持true和false)。 - lkanab
3个回答

9
不,C语言中递增值并不能保证原子性。你需要提供同步机制,或使用系统特定的库来执行原子递增/递减操作。

那么你的意思是这可能是一个错误吗?即使在单核环境下? - user1603164
1
@user1603164 绝对没错:如果在加载和存储值之间,您的代码被抢占,那么另一个线程可以在您的情况下修改该值,从而导致难以发现的错误。即使在单CPU、单核心、非超线程环境中也会发生这种情况。 - Sergey Kalinichenko
1
是的,即使在单核环境下也是如此。 x ++; 没有理由生成执行内存单指令读-修改-写操作的机器代码。编译器可以自由地使用不同的载入和存储来实现 ++,而且出于更好的调度等原因可能会这样做。此外,许多计算机甚至没有读取-修改-写操作指令。在任何这样的情况下,线程都可以在读取后但在写入之前被抢占。 - R.. GitHub STOP HELPING ICE
有没有什么建议可以使用 https://launchpad.net/gcc-arm-embedded 编译器来生成针对 Cortex-M4 的 atomic++?否则我必须禁用/启用 IRQ 来保护? - user1603164
@user1603164 看起来 uxSchedulerSuspended 变量的确切值在任务挂起和重新激活之间不允许更改,因此您无需修改任何内容。 - Sergey Kalinichenko

9

正如您所记录的那样,它不是原子性的。但从不那么严格的意义上来说,它仍然可以是“线程安全”的:一个long不能处于不一致的状态。这里的危险程度在于,如果n个线程调用vTaskSuspendAll,则uxSchedulerSuspended将增加1到n之间的任何值。

但如果变量是不需要完美的东西,比如追踪用户请求挂起的次数的跟踪器,那么这可能完全没有问题。有“线程安全”的含义是“无论如何交错调用,此操作都会产生相同的结果”,还有“线程安全”的含义是“如果您从多个线程调用此操作,不会发生任何异常”。


我认为原始FreeRTOS代码中的注释很令人困惑。我敢打赌,所有FreeRTOS需要的只是uxSchedulerSuspended非零才能停止,因此它无论增加1还是n都没有关系。 - Mark Lakata
哇!太棒了的答案。 - lkanab

6
操作不是原子性的,但没有地方说它是。然而,代码是线程安全的,但你必须非常熟悉代码正在做什么,以及它如何适配调度器的设计才能知道这一点。如果其他任务在加载和存储之间修改变量也没有关系,因为当执行任务再次运行时,它会发现变量处于与执行原始加载时相同的状态(因此修改和写入部分仍然保持一致和有效)。
正如之前发布的注意事项,long类型不能处于不一致的状态,因为它是其所在体系结构的基础类型。但是,考虑一下如果代码运行在8位(或16位)机器上且变量是32位会发生什么。那么它就不是线程安全的,因为所有32位都将逐字节或逐字修改,而不是一次性全部修改。在这种情况下,一个字节可能被加载到寄存器中,进行修改,然后在上下文切换时写回到RAM中(留下另外三个字节未修改)。如果下一个执行的任务读取相同的变量,它将读取一个已经修改过的字节和三个未修改的字节,这将导致严重问题。

如果变量在下次运行任务时保证处于相同的状态,为什么对于增加多字节类型,这个假设不成立呢?+1 - Sergey Kalinichenko

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接