技術乾貨:追根原子操作、鎖、同步實現原理
在多線程環境下,i++並不安全。使用兩個線程同時操作i++ 1w萬次,可能得出i的值並不是2w。想要安全的使用i++, 在java上可以使用synchronized關鍵字, c/c++語言中可以使用pthread_mutex_lock。c的sleep函數也同樣讓我著迷,它可以讓線程「暫停」,想不通它是怎麼做到的。但感覺它們應該有共性的,搶佔一個被佔用的鎖時是要出現「等待」的。
1. 原子操作i++以前維護過一個c/c++項目,它實現了一套的引用計數來管理對象。跟蹤它在增加引用計數時調用了android_atomic_inc,android_atomic_inc又調用了android_atomic_add,android_atomic_add函數是內嵌彙編實現的:
(註:文中代碼均可左右滑動)xtern 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]
"
"add %1, %0, %5
"
"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;
}
第一眼看上去,肯定會懷疑代碼是不是找錯了,怎麼會有一個循環呢?我們慢慢分析一下,依據c/c++調用彙編參數傳遞格式,可以列出下表:
do while (status != 0);
這很好理解,關鍵是怎樣達到原子操作(線程安全)呢,就是ldrex和strex指令的功能了,兩條指令的官方文檔:
主要看Operation欄。指令ldrex在取值時會在對應內存上做一個exclusive access標記;指令strex存值時有這個標記就會存值成功,返回0,沒有這個標記就會存值失敗,返回1。這個標記就是獨佔式訪問標記,是針對cpu的,多核環境下也沒有問題。
2. sleep函數的暫停sleep函數的實現會涉及到系統調用,這裡我們只需要關注sleep用戶態的代碼和內核的代碼即可,至於它們是怎樣關聯的,請參考arm平台linux系統調用這篇文章。
sleep用戶態代碼相對簡單,從sleep函數調到nanosleep標籤:
unsigned int sleep(unsigned int seconds)
{
struct timespec t;
/* seconds is unsigned, while t.tv_sec is signed
* some people want to do sleep(UINT_MAX), so fake
* support for it by only sleeping 2 billion seconds
*/
if ((int)seconds
seconds = 0x7fffffff;
t.tv_sec = seconds;
t.tv_nsec = 0;
if ( !nanosleep( &t, &t ) )
return 0;
if ( errno == EINTR )
return t.tv_sec;
return -1;
}
ENTRY(nanosleep)
mov ip, r7
ldr r7, =__NR_nanosleep
swi #0
mov r7, ip
cmn r0, #(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno
END(nanosleep)
sleep內核態代碼:
SYSCALL_DEFINE2(nanosleep, struct timespec __user *, rqtp,
struct timespec __user *, rmtp)
{
struct timespec tu;
if (copy_from_user(&tu, rqtp, sizeof(tu)))
return -EFAULT;
if (!timespec_valid(&tu))
return -EINVAL;
return hrtimer_nanosleep(&tu, rmtp, HRTIMER_MODE_REL, CLOCK_MONOTONIC);
}
完整的調用棧如下:
最終調到context_switch, context_switch函數的調用會引起進程調度,就是cpu切換到其它進程去執行了,當前進程就「失去」cpu了,這裡會涉及時間片的概念。所以sleep函數是不佔用cpu資源的,對調用者來說它是「耗時」的。我們知道sleep函數在「暫停」指定時間後會繼續執行後面的代碼,現在cpu切換到其它進程去執行了,那cpu什麼時候再切換回來呢!
3. 線程鎖c線程鎖pthread_mutex_lock,也可以達到同步的效果。而鎖是軟體層抽象出來的概念,它是如何實現的。在使用者的角度可以分為兩段,第一段是鎖狀態的更改;第二段是等待鎖的釋放。
先說第一段,鎖狀態的更改。下面是調用棧:
pthread_mutex_lock
pthread_mutex_lock_impl
__bionic_cmpxchg
__bionic_cmpxchg函數在bionic_atomic_arm.h文件中,實現代碼:
/* Compare-and-swap, without any explicit barriers. Note that this functions
* returns 0 on success, and 1 on failure. The opposite convention is typically
* used on other platforms.
*/
__ATOMIC_INLINE__ int
__bionic_cmpxchg(int32_t old_value, int32_t new_value, volatile int32_t* ptr)
{
int32_t prev, status;
do {
__asm__ __volatile__ (
__ATOMIC_SWITCH_TO_ARM
"ldrex %0, [%3]
"
"mov %1, #0
"
"teq %0, %4
"
#ifdef __thumb2__
"it eq
"
#endif
"strexeq %1, %5, [%3]"
__ATOMIC_SWITCH_TO_THUMB
: "=&r" (prev), "=&r" (status), "+m"(*ptr)
: "r" (ptr), "Ir" (old_value), "r" (new_value)
: __ATOMIC_CLOBBERS "cc");
} while (__builtin_expect(status != 0, 0));
return prev != old_value;
}
這段代碼與上面原子操作i++部分相似,這裡不多解釋了,核心還是ldrex和strex, 獨佔式讀寫操作。
第二段,等待鎖的釋放。這部分是使用系統調用實現的,代碼有用戶態部分和內核態部分。用戶態部分的調用棧:
pthread_mutex_lock
pthread_mutex_lock_impl
__futex_wait_ex
__futex_syscall4
__futex_syscall3
摘出主要代碼:
// __futex_syscall3(*ftx, op, val)
ENTRY(__futex_syscall3)
mov ip, r7
ldr r7, =__NR_futex
swi #0
mov r7, ip
bx lr
END(__futex_syscall3)
// __futex_syscall4(*ftx, op, val, *timespec)
ENTRY(__futex_syscall4)
b __futex_syscall3
END(__futex_syscall4)
可以知道最終調用的系統調用是futex, 但使用arm平台linux系統調用提供的方法找內核代碼,這裡的參數個數不太明確,但模糊查找可以確定是SYSCALL_DEFINE6(futex。內核態棧調用圖:
是不是從消息5開始就很熟悉啦,和上面sleep內核態代碼相同,最終都調到了context_switch, 引起進程調度。
synchronized 關鍵字既然我們了解了c的鎖是如何實現的,那麼java的鎖呢?這裡主要探討java關鍵字synchronized。
先看一段java代碼(摘自MobLink):
void updateIntent(Activity activity, Intent intent) {
/*
* 1. 檢查intent是否存在場景/是否需要從伺服器恢復場景
* 2. 檢查緩存intent是否相同
* 3. 檢查之前的msg_id是否存在
*/
if ((!isNeedToUpdateIntent(intent)) && !isNeedRestoreSceneFromServer) {
return;
}
IntentRecorder ir;
synchronized (cacheIntent) {
if (cacheIntent.isSame(activity, intent)) {
return;
}
// 先緩存一下,防止多次調用,導致多個場景
cacheIntent.activity = activity;
if (null == intent) {
cacheIntent.intent = new Intent();
} else {
cacheIntent.intent = intent;
}
ir = new IntentRecorder(cacheIntent);
}
// 如果存在id, 需要修改參數
if (thisHander.hasMessages(MSG_PRE_RESTORE_SCENE)) {
thisHander.removeMessages(MSG_PRE_RESTORE_SCENE);
}
Message msg = thisHander.obtainMessage(MSG_PRE_RESTORE_SCENE, ir);
thisHander.sendMessage(msg);
}
這個函數可以被多個線程調用,調用之後把傳過來的activity、intent對象緩存在cacheIntent對象里,然後在另外一個地方使用緩存在cacheIntent對象里的activity和intent,activity和intent是「成對」的,為了防止讀取時又有寫入導致的activity和intent不成對,所以加了一個synchronized關鍵字。
編譯成class後,使用d2j-jar2dex.bat和d2j-dex2smali.bat反編譯生成smali代碼,如下:
.method updateIntent(Landroid/app/Activity;Landroid/content/Intent;)V
.catchall { :L2 .. :L4 } :L3
.catchall { :L5 .. :L7 } :L3
.catchall { :L9 .. :L10 } :L3
.registers 9
.prologue
const/16 v5, 1001
.line 209
invoke-direct { p0, p2 }, Lcom/mob/moblink/utils/MobLinkImpl;->isNeedToUpdateIntent(Landroid/content/Intent;)Z
move-result v2
if-nez v2, :L1
iget-boolean v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->isNeedRestoreSceneFromServer:Z
if-nez v2, :L1
:L0
.line 236
return-void
:L1
.line 214
iget-object v3, p0, Lcom/mob/moblink/utils/MobLinkImpl;->cacheIntent:Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
monitor-enter v3
:L2
.line 215
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->cacheIntent:Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
invoke-virtual { v2, p1, p2 }, Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;->isSame(Landroid/app/Activity;Landroid/content/Intent;)Z
move-result v2
if-eqz v2, :L5
.line 216
monitor-exit v3
goto :L0
:L3
.line 227
move-exception v2
monitor-exit v3
:L4
throw v2
:L5
.line 220
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->cacheIntent:Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
invoke-static { v2, p1 }, Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;->access$202(Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;Landroid/app/Activity;)Landroid/app/Activity;
.line 221
if-nez p2, :L9
.line 222
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->cacheIntent:Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
new-instance v4, Landroid/content/Intent;
invoke-direct { v4 }, Landroid/content/Intent;->()V
invoke-static { v2, v4 }, Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;->access$302(Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;Landroid/content/Intent;)Landroid/content/Intent;
:L6
.line 226
new-instance v0, Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->cacheIntent:Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
invoke-direct { v0, p0, v2 }, Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;->(Lcom/mob/moblink/utils/MobLinkImpl;Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;)V
.line 227
.local v0, ir:Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
monitor-exit v3
:L7
.line 230
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->thisHander:Landroid/os/Handler;
invoke-virtual { v2, v5 }, Landroid/os/Handler;->hasMessages(I)Z
move-result v2
if-eqz v2, :L8
.line 231
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->thisHander:Landroid/os/Handler;
invoke-virtual { v2, v5 }, Landroid/os/Handler;->removeMessages(I)V
:L8
.line 234
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->thisHander:Landroid/os/Handler;
invoke-virtual { v2, v5, v0 }, Landroid/os/Handler;->obtainMessage(ILjava/lang/Object;)Landroid/os/Message;
move-result-object v1
.line 235
.local v1, msg:Landroid/os/Message;
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->thisHander:Landroid/os/Handler;
invoke-virtual { v2, v1 }, Landroid/os/Handler;->sendMessage(Landroid/os/Message;)Z
goto :L0
:L9
.line 224
.end local v0
.end local v1
iget-object v2, p0, Lcom/mob/moblink/utils/MobLinkImpl;->cacheIntent:Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;
invoke-static { v2, p2 }, Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;->access$302(Lcom/mob/moblink/utils/MobLinkImpl$IntentRecorder;Landroid/content/Intent;)Landroid/content/Intent;
:L10
goto :L6
.end method
查看smali代碼發現synchronized代碼段前後插入monitor-enter, monitor-exit指令,在.line 214、.line 216、.line 227處。
之後嚴謹的思路是去了解dalvik怎樣解釋monitor-enter和monitor-exit指令,但這會有很多東西要說,且不是本文描述的重點。我們直接從delvik解釋位元組碼時,遇到monitor-enter和monitor-exit指令做了什麼事情開始說起,其實每條位元組碼指令都會對應一個c/cpp函數,delvik遇到這個位元組碼時就會執行這個函數。這些指令每條分別對應一個cpp文件, 這些文件存放在「/dalvik/vm/mterp/c/」目錄下。查看一下monitor-enter指令對應的實現函數:
HANDLE_OPCODE(OP_MONITOR_ENTER /*vAA*/)
{
Object* obj;
vsrc1 = INST_AA(inst);
ILOGV("|monitor-enter v%d %s(0x%08x)",
vsrc1, kSpacing+6, GET_REGISTER(vsrc1));
obj = (Object*)GET_REGISTER(vsrc1);
if (!checkForNullExportPC(obj, fp, pc))
GOTO_exceptionThrown();
ILOGV("+ locking %p %s", obj, obj->clazz->deor);
EXPORT_PC(); /* need for precise GC */
dvmLockObject(self, obj);
}
FINISH(1);
OP_END
追蹤一下dvmLockObject函數。這裡同樣像c線程鎖一樣分為兩段,第一段是鎖狀態的更改,調用棧:
dvmLockObject
android_atomic_acquire_cas
android_atomic_cas
android_atomic_cas函數的實現代碼:
extern ANDROID_ATOMIC_INLINE
int android_atomic_cas(int32_t old_value, int32_t new_value,
volatile int32_t *ptr)
{
int32_t prev, status;
do {
__asm__ __volatile__ ("ldrex %0, [%3]
"
"mov %1, #0
"
"teq %0, %4
"
#ifdef __thumb2__
"it eq
"
#endif
"strexeq %1, %5, [%3]"
: "=&r" (prev), "=&r" (status), "+m"(*ptr)
: "r" (ptr), "Ir" (old_value), "r" (new_value)
: "cc");
} while (__builtin_expect(status != 0, 0));
return prev != old_value;
}
看到這裡,是不是很熟悉。和上面原子操作及c線程鎖一樣,他們都是使用ldrex和strex來完成的,獨佔式讀寫arm指令。
第二段是等待鎖的釋放,追蹤代碼調用棧:
dvmLockObject
nanosleep
dvmLockObject函數代碼太長,分析起來也很費勁,這裡摘出主要實現代碼,其實也包括第一段的代碼:
這裡是不是也很熟悉,因為它調用了nanosleep系統調用,和sleep代碼的實現是一樣的。
以前很不理解原了操作、線程鎖、等待,不同的語言里更是千差萬別,但追根究底之後發現原來他們是「一樣的」,萬變不離其宗吧。
[ShareSDK] 輕鬆實現社會化功能 強大的社交分享
[SMSSDK] 快速集成簡訊驗證 聯結通訊錄社交圈
[MobLink] 打破App孤島 實現Web與App無縫鏈接
[MobPush] 快速集成推送服務 應對多樣化推送場景
[AnalySDK] 精準化行為分析 + 多維數據模型 + 匹配全網標籤 + 垂直行業分析顧問
BBSSDK | ShareREC | MobAPI | MobPay | ShopSDK | MobIM | App工廠
截止2018 年4 月,Mob 開發者服務平台全球設備覆蓋超過84 億,SDK下載量超過3,300,000+次,服務超過380,000+款移動應用


TAG:Mob開發者服務平台 |