當前位置:
首頁 > 最新 > 技術乾貨:追根原子操作、鎖、同步實現原理

技術乾貨:追根原子操作、鎖、同步實現原理

在多線程環境下,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+款移動應用


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 Mob開發者服務平台 的精彩文章:

TAG:Mob開發者服務平台 |