ISR和多线程程序中的C'Volatile'关键字?

时间:2012-10-05 02:24:57

标签: c multithreading embedded

我读到了内存映射硬件寄存器,ISR和多线程程序中C volatile关键字的用法。

1)注册

uint8_t volatile * pReg;
while (*pReg == 0) { // do sth } // pReg point to status register 

2)ISR

int volatile flag = 0;
int main() 
{
    while(!flag) { // do sth }
}
interrupt void rx_isr(void)
{
    //change flag
}

3)多线程

int volatile var = 0;
int task1()
{
    while (var == 0) { // do sth }
}
int task2()
{
    var++;
}

我可以看到为什么编译器可能会错误地优化案例1中的while,如果volatile不存在,'因为变量是从硬件进行的,编译器可能看不到由代码创建的变量的任何更改。

但对于案例2)和3),为什么需要使用volatile?在这两种情况下,变量都声明为 global ,编译器可以看到它被使用了在不止一个地方。那么,如果变量不是while,为什么编译器会优化volatile循环呢?

是因为编译器副设计不知道"异步调用" (在ISR情况下)或多线程?但这不可能,对吧?

此外,案例3)看起来像是没有volatile关键字的多线程中的常见程序。我们假设我在全局变量中添加了一些锁定(没有volatile关键字):

int var = 0;
int task1()
{
    lock();   // some mutex
    while (var == 0) { do sth }
    release()
}
int task2()
{
    lock();
    var++;
    release();
}

对我而言看起来很正常。那么在多线程中我真的需要volatile吗? 为什么我从未见过volatile限定符添加到变量中以避免在之前优化多线程程序?

5 个答案:

答案 0 :(得分:9)

使用volatile关键字的要点是防止编译器生成使用CPU寄存器的代码作为表示变量的更快方法。这会强制编译代码在每次访问时访问RAM中的确切内存位置,以获取可能已由另一个实体更改的最新值。通过添加volatile,我们确保我们的代码知道任何其他人(如硬件或ISR)对变量所做的任何更改,并且不会发生一致性问题。

如果没有volatile关键字,编译器会尝试通过将RAM中的变量内容读入CPU寄存器一次来生成更快的代码,并在循环或函数中使用该缓存值。访问RAM可能比访问CPU寄存器慢几十倍。

我已经有过第1项和第2项的经验,但我认为您不需要在多线程环境中将变量定义为volatile。添加锁定/解锁机制对于解决同步问题是必要的,与volatile的内容无关。

答案 1 :(得分:2)

编译器确实允许 else 更改变量,除非满足某些特定条件。其中一个是易失性访问;其他是一定的编译障碍。

您可能想到的编写多线程代码的天真方式确实容易出错,并且会被视为未定义的行为。如果你有正确的多线程代码,那么优化仍然是合法的(比如你的最终task1,其中循环仍然是UB并且可能被抛出),或者同步原语包含必要的障碍(通常在一些原子变量的内部)。

为了解决这个问题,这里是多线程示例的更正版本:

 for (;;)
 {
     lock();
     if (var != 0) { unlock(); break; }
     unlock();
 }

unlock()函数的实现引入了一个编译器屏障,可确保无法优化循环。

答案 2 :(得分:2)

是因为编译器副设计不知道“异步调用”(在ISR情况下)或多线程?但这不可能,对吧?

是的,就是这样。

在C语言中,编译器没有并发概念,因此只要单个线程的视图无法注意到差异,就可以重新排序和缓存内存访问。

这就是为什么你需要volatile(阻止对变量进行这种优化),内存障碍(在所有变量的程序的单个点上阻止它)或其他形式的同步,如锁定(通常作为内存障碍) )。

答案 3 :(得分:0)

您可以使用障碍自由地避免多线程软件中的volatile变量。你可以在linux内核源代码中找到很多例子。使用障碍而不是volatile允许编译器生成更有效的代码。

答案 4 :(得分:0)

关于案例2),

我在你的问题中多次编写了与案例2相同的代码,并没有遇到任何问题。我认为这是因为现代编译器可以处理这种情况。比如,编译器可以“看到”我在“rx_isr”中更改“flag”,并且不添加任何优化。但是,由于以下原因,这是不安全的:

1)编译器的优化级别,这可能会影响以下原因3)

2)调用isr的方法,可能是函数指针超出了编译器的视图

3)编译器实现,不同的编译器可能有不同的定义“看到标志在isr中改变了”

...

因此,为了最大限度地保证安全性和便携性,只需添加“volatile”。