Android原子操作的實現方式和CPU的架構有密切關係,現在的原子操作一般都是在CPU指令級別實現的,這樣不但簡單,而且效率非常高。下面看看arm平臺下Android是如何實現原子操作的。
雖然原子操作的接口函數有十來個,但是實際上只有兩個函數中通過彙編代碼實現了原子操作:函數android_atomic_add和android_atomic_cas,其他的函數都是在內部調用它們而已。這兩個函數的原理差不多,下面我們以加法爲例來了解一下原子變量的實現原理。函數android_atomic_add的代碼如下:
extern ANDROID_ATOMIC_INLINE
int32_t android_atomic_add(int32_t increment, volatile int32_t *ptr)
{
int32_t prev, tmp, status;
android_memory_barrier();
do {
__asm__ __volatile__ ("ldrex %0, [%4]\n"
"add %1, %0, %5\n"
"strex %2, %1, [%4]"
: "=&r" (prev), "=&r" (tmp),
"=&r" (status), "+m" (*ptr)
: "r" (ptr), "Ir" (increment)
: "cc");
} while (__builtin_expect(status != 0, 0));
return prev;
}
上面代碼中的宏ANDROID_ATOMIC_INLINE的定義是:
#define ANDROID_ATOMIC_INLINE inline __attribute__((always_inline))
實際上就是把函數規定爲inline函數。
android_memory_barrier是告訴CPU這裏需要內存屏障。下節會介紹內存屏障。
接下來是一段內嵌彙編,這段彙編可以用僞代碼來表示:
do {
ldrex prev,[ptr]
add tmp, prev, increment
strex status, tmp, [ptr]
} whiile(status != 0)
在add指令的前後有兩條看上去比較陌生的指令:ldrex和strex。這兩條是AMRV6新引入的同步指令。ldrex指令的作用是把指針ptr指向的內容放到prev變量中,同時給執行處理器做一個標記(tag),標記上指針ptr的地址,表示這個內存地址已經有一個CPU正在訪問。當執行到strex指令時,它會檢查是否存在ptr的地址標記,如果標記存在,strex指令會把add指令執行的的結果寫入指針ptr指向的地址,並且返回0,然後清除該標記。返回的結果0會放在status變量中,這樣循環將結束。
如果在strex指令執行前發生了線程的上下文切換,在切換回來後,ldrx指令設置的標誌將會被清除。這時再執行strex指令時,由於沒有了這個標誌,strex指令將不會完成對ptr指針的存儲操作,而且status變量中的返回結果是1。這樣循環不能結束,重新開始執行,直到成功爲止。
__builtin_expect是gcc的內建函數,有兩個參數,第一個參數是一個表達式,第二個參數是一個值。表達式的計算結果也是函數的結果。__builtin_expect是用來告訴gcc預測表達式更可能的值是什麼,這樣gcc會根據預測值來優化代碼。代碼中表達的含義是預測“status!=0”這個表達式的值爲“0”,也就是預測while循環將結束。
內嵌彙編可以參考本人的博文:gcc內嵌彙編介紹