struct _a_struct{
int x;
int y
volatile bool alive=false;
} ASTRUCT;
ASTRUCT a_struct;
//Thread 1
a_struct.x = x;
a_struct.y = y;
a_struct.alive =true;
//thread 2
if (a_struct.alive==true)
{
draw_struct(a_struct.x, a_struct.y);
}
这是一个在两个线程之间实现共享对象的正常情况,一个线程正在更新对象数据,另一个线程则等待同一个对象数据。上面的代码似乎没啥问题,但问题还是隐藏在里面。它不会产生预期的结果。我们可以在结构体中都用volatile修饰,但这将产生低效的代码。既然不想失去效率,又想在两个线程之间共享数据,这篇文章将会解释上面的代码有啥问题,为什么应该避免对volatile的乱用。
int* pi; // is a pointer to an int.
int volatile* pvi; // is a pointer to a volatile int; meaning the object it is pointing to, is volatile
int* volatile vpi; // is a volatile pointer to an int; pointer is volatile, the object it is pointing to is an int. This is of fairly less use.
int volatile* volatile vpvi; // volatile pointer to a volatile int; both the pointer and the memory location it is pointing to, are volatile int.
这种定义性还是看英文的比较好。
Complex Objects
You can declare a volatile struct as
struct volatile _a
{
int mem_i;
int* pmem_i;
} A;
A中的成员也会被被volatile修饰。考虑如下代码
A a_struct; //a_struct is volatile
a_struct.mem_i; // the type is volatile int
a_struct.pmem_i; // the type is, wait for it, int* volatile (the pointer is volatile not the object it is pointing to) 即 指针变量被修饰,而非对象
When using volatile on a collective such as structs, volatile is prepended to left of the identifier and to the right of the type. 结构体类型中volatile的位置:类型的右边,标识符的左边
int volatile *pvi; //pointer to volatile int.
int* pi; //pointer to an int.
pvi = pi;
When modifying a volatile type with non-volatile type, the compiler will implicitly cast the non-volatile type to a volatile type.
pi=pvi;
这时编译器会提示警告类似于搞什么我不懂,我得先把volatile关键字干掉。在一些老编译器压根儿没提示,到时候你也不知道会出什么幺蛾子。
int i;
int* pi;
int volatile* pvi;
int func(int i, int* pi);
func(i,pvi);
上面代码一样不能通过编译,定义形参 int* pi,但传递的是volatile修饰的实参pvi。根据C标准,没有这样使用情况,意味着啥都可能发生。
When modifying a non-volatile type with volatile type, the compiler should throw a warning, if it doesn’t then raise a ticket to your compiler team. 不要尝试用volatile修饰的类型去修改未经volatile修饰的类型,一般编译器会报错,要是没报错就给编译小组张红牌吧。
volatile uint16_t my_delay;
void wait(uint16_t time) {
my_delay = 0;
while (my_delay
volatile uint16_t my_delay;
void wait(uint16_t time) {
uint16_t tmp;
EnterCritical();
my_delay = 0;
ExitCritical();
do {
EnterCritical();
tmp = my_delay;
ExitCritical();
} while(tmp
volatile int ready;
int Message[100];
void foo( int i )
{
Message[i/10] = 42;
Ready = 1;
}
void Thread2(i)
{
while(ready != 1);
//read message
}
在上面的代码中,foo()函数更新消息队列并更新消息标识(告知线程2数据已准备好),线程2等待消息标识被置为1,然后读取消息队列。注意ready被volatile修饰,如果没有修饰,那么编译器可能会将while循环语句优化掉,这还没完。如果使用GCC编译器-O2优化级别对上面代码进行编译,就会发现消息标识位在消息队列更新前已经更新了,线程2会一直读取历史数据队列。作为开发人员的默认解决方案是把消息也用volatile修饰起来,这会造成代码执行的更低效。更糟糕的是问题可能还没解决,因为机器会自行重排序,是的,机器会对代码进行重排序。为啥呢?
asm volatile ("" : : : "memory");
解决问题的办法就是使用编译语言扩展
volatile int ready;
int message[100];
void foo (int i) {
message[i/10] = 42;
asm volatile ("" : : : "memory");
ready = 1;
}
这条语句之前限制了所有RAM的访问,语句之后再重新载入访问,在编译屏障前后也不允许编译器排序代码。虽然主要的编译器gcc,英特尔CC,和LLVM提供这个选项,但编译屏障本身不是一个很好的解决方案,一些编译器也不提供这个选项。最好的解决办法是使用内存屏障。
void vPortYieldFromISR(void) {
/* Set a PendSV to request a context switch. */
*(portNVIC_INT_CTRL) = portNVIC_PENDSVSET_BIT;
/* Barriers are normally not required but do ensure the code is completely within the specified behavior for the architecture. */
__asm volatile("dsb");
__asm volatile("isb");
}
上面两条汇编指令 dsb isb 分别实现数据同步屏障,指令同步屏障。从而保证数据和指令程序段执行的序列化,有关内存屏障可阅读: http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dai0321a/index.html
In a kernel setting as in multithreaded environment you can be sure to have kernel locking primitives which make the data safe i.e. mutexes, spinlocks and barriers. These locks are designed to also prevent unwanted optimizations and make sure the operations are atomic on both the cpu and compiler level. Thus if they are being used properly then you don’t need volatile.
Thus the only significant use of volatile (that one can think of) in a kernel setting can be for accessing a memory mapped IO. Since you don’t want the compiler to optimize out the register accesses within the critical region, you still have to use volatile inside the critical region even if you use locks around those accesses. But in most kernel settings you have special accessor functions for accessing IO memory regions, because accessing this memory region directly is frowned upon in a kernel setting. These accessor functions must make sure to prevent any unwanted optimizations and if they do it properly then volatile is not needed.