iOS老司機的多執行緒PThread學習分享

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第4天,點選檢視活動詳情

前言

  • iOS中關於多執行緒相關的文章不勝列舉.
  • 誠然, 掌握了iOS中的GCD相關API, 不論開發語言是Objective-C還是Swift, 都能夠處理絕大多數業務開發中的多執行緒場景.
  • 但是, 作為一個有追求的iOS開發者, 除了掌握常見API的使用, 我們還應探索多執行緒的底層, 避免只見樹木不見森林.
  • 機緣巧合, 有幸學習了Casa的PThread課程, 針對GCD的底層PThread, Casa在這兩個多小時的課程中有著深入淺出的講解. 幫助廣大開發者既見樹木又見森林. Casa部落格PThread介紹
  • 如果你對iOS中多執行緒的底層技術PThread也有所好奇, 不妨和我一起跟著Casa的思路看下去:)
  • ps: 文章純手打, 如有錯誤, 請評論區指正, 在此謝過了~

章節1 執行緒基礎概念和操作

1.1. POSIX和多執行緒

  • POSIX Thread

  • 各種各樣的作業系統, 他們介面不統一, 所以需要針對每一種作業系統寫不同的程式碼

  • 很麻煩, 怎麼辦?

  • Portable Operating System Interface 可移植作業系統介面

  • POSIX標準中的Thread章節(2.9 Threads)

  • 定義的函式都是pthread開頭的

  • 大部分主流系統都不是嚴格遵守POSIX

  • POSIX具備指導意義, 實際情況還是要看作業系統對應文件

1.2. 執行緒的建立

  • 如何建立一個執行緒?

  • 猜一下 執行緒 = 建立執行緒(要做的事情, 事情的引數, 執行緒的配置, 儲存執行緒的結構體)

  • 結果? pthread_create(執行緒記錄, 執行緒屬性, 事情, 引數) int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void*(*start_routine)(void *), void *arg)

1.3. 執行緒的屬性和Join/Detach

  • stack

  • sechedule

    • 走系統預設
    • 交給系統
  • 雜項

    • detach state 
      • joinalbe(預設)/detached 
      • 有人等/無所謂 
    • scope
      • 執行緒競爭時, 參與競爭的範圍
      • 即優先順序有範圍
      • process(預設)/system
  • Detach vc Join

    • 一個detach屬性的執行緒
      • 意味著你拿不到這個執行緒的返回值(如果有的話)
      • 而且, 在這個執行緒結束之後, 相關資源就會被立刻回收.
    • 一個join屬性的執行緒
      • 線上程結束之後資源不會被立刻釋放, 而是等待別的執行緒來join
      • 當把自己的返回值交給來join的執行緒之後, 自己就會被釋放
      • 如果一直沒有執行緒來join, 那這個執行緒就會一直存在, 直到程序結束.
  • Detach

    • -> 建立執行緒 -> 繼續執行
    • Detach屬性的子執行緒 ->
    • 任務結束系統立刻回收
  • Join(接應)

    • int pthread_join(pthread_t thread, void **retval)
    • 儲存子執行緒任務結果
    • -> 建立執行緒 -> 呼叫pthread_join並等待指定執行緒 等待中 pthread_join的等待結束 -> 繼續執行
    • join屬性的子執行緒 -> 任務結束 ^ 傳遞任務結果
  • 很多個執行緒一起Join一個子執行緒?

    • 子執行緒只能被join 1次!
    • 先到先得!
    • 創富join一個執行緒無任何作用

1.4. 執行緒的結束

  • join

    • pthread_join(pthread_t thread, void **value_ptr)
    • 父執行緒監聽子執行緒的結束, 並通過pthread_join函式獲得子執行緒的返回值
  • exit

    • pthread_exit(void **value_ptr)
    • 子執行緒主動結束, 通過pthread_exit傳遞值給父執行緒的pthread_join去接收
  • kill

    • pthread_kill(pthread_t thread, int sig)
    • 向 [子執行緒/自己] 傳送指定的訊號, 如果子執行緒沒有響應該訊號的程式碼,
    • 則交由程序響應. 例如傳送SIGQUIT訊號, 子執行緒不響應的話,
    • 程序就會響應該訊號, 結束程序
  • return

    • 本質上跟pthread_exit一樣, 但是不會呼叫cleanup函式
    • cleanup函式, 清理函式
      • pthread_cleanup_push
      • pthread_cleanup_pop
      • 要一一對應, 否則編譯不過

1.5. 取消一個執行緒

  • pthread_cancel取消一個執行緒

  • int pthread_cancel(pthread_t threas)

  • 呼叫pthread_cancel可以讓對應執行緒執行到取消點時取消執行緒

  • 執行緒取消點?

    • POSIX標準, 
    • 表裡的函式都是執行緒取消點
    • 不一定準!不是所有的標準都去遵守, 看作業系統
  • 有些C庫沒有符合POSIX標準, 所以在呼叫這些函式之前,可以自建執行緒取消點

  • void pthread_testcancel(void), 告訴CPU檢查狀態, 是否被取消

1.6. POSIX裡其他有用的函式

  • pthread_once

    • pthread_once(pthread_once_t *once_control, void (routine)(void))
    • 使用PTHREAD_ONCE_INIT去初始化once_control
  • pthread_self & phtread_equal

      • pthread_t pthread_self() 
      • 告訴你自己是誰
      • int pthread_equal(pthread_t thread1, pthread_t thread2)
      • 告訴你跟你一樣的人是誰

章節2 鎖和各種情況

2.7. Mutex鎖

  • 可以衍生出遞迴鎖共享鎖

  • 鎖是幹什麼用的?

    • 一條執行緒給count加1
    • 期望的count結果是2
      1. 從記憶體讀取變數的值
      1. 在讀到的值上+1
      1. 將結果寫回記憶體
  • 兩條執行緒給count加1, 期望的count結果是3

    • 多條執行緒共享同一塊記憶體
      1. 從記憶體讀取變數的值 count 1 count 1
      1. 在讀到的值上+1 count 2 count 2
      1. 將結果寫回記憶體 count 2 count 2
  • 怎麼辦?

    • 臨界區的概念
    • 不希望一個執行緒在執行任務的時候, 其他執行緒摻和進來
    • 這段任務叫臨界區, 一個任務只能有一個執行緒執行
  • 問題原因?

    • 臨界區被多個執行緒同時操作了
  • 通過加鎖使得臨界區只有一個執行緒在執行

  • 加鎖的情況

    • 申請鎖
    • 從記憶體讀取變數的值
    • 在讀到的值上+1
    • 將結果寫回記憶體
    • 釋放鎖
    • 此時綠色執行緒可以成功讀取記憶體中變數的值
  • Mutex Lock 最常用的鎖, 相關的5個函式

    • int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr)
    • int pthread_mutex_destroy(pthread_mutex_t *mutex)
    • int pthread_mutex_lock(pthread_mutex_t *mutex)
    • int pthread_mutex_unlock(pthread_mutex_t *mutex)
    • int pthread_mutex_trylock(pthread_mutex_t *mutex) 不必讓執行緒一直處於等待狀態

2.8. Mutex鎖的各種屬性

  • Mutex鎖的屬性

    • priority ceiling
      • 數字 防止優先順序反轉 當前執行緒優先順序
    • mutex protocol
      • INHERIT/ NONE(預設) / PROTECTED 防止優先順序反轉 
    • process shared
      • PRIVATE(預設)
      • SHARED
      • 是否能夠跟其它執行緒共享鎖, 哪怕是跨程序
    • mutex type 
      • NORMAL 不記錄持有人
      • DEFAULT(預設) 保留名
      • ERRORCHECK 不能重複加鎖解鎖
      • RECURSIVE 允許同一執行緒加N次鎖, 但也要解鎖N次才會釋放
    • robust
      • STALLED(預設) / ROBUST
      • 如果沒解鎖執行緒就跪了: 啥也不做 / 讓下一個執行緒處理

2.9. 優先順序反轉及解決方案

  • 優先順序反轉?

    • 低優先順序的執行緒反而優先於高優先順序的執行緒執行
  • 正常的情況

    • 低優先順序執行緒 R -> 釋放資源
    • 高優先順序執行緒 -> R被佔用 通知低優先順序的執行緒我要搶佔你了
  • 優先順序反轉的場景

    • R -> 釋放資源 被中優先順序的執行緒搶佔了, 系統讓中優先順序的執行緒先執行
    • -> R          中優先順序的任務結束, 低優先順序的才能繼續執行
    • 中優先順序的執行緒打斷了低優先順序執行緒的釋放流程
    • 使得中優先順序執行緒反而先於高優先順序的執行緒執行
  • 怎麼辦?

    • 不應該讓中優先順序的執行緒去打斷低優先順序執行緒的資源回收流程
    • 釋放資源的時候(關鍵臨界區)不允許被中斷
    • 無鎖同步方案(Non-blocking Synchronization / Read-Copy-Update)
    • Priority Ceiling Protocol
      • 是Mutex鎖的一個屬性
      • 配置了該屬性之後, 其實就是配置了優先順序
      • 低優先順序 1 中優先順序 5 高優先順序 8
      • 釋放資源(ceiling更高的才能搶佔)
      • 中優先順序的執行緒無法搶佔低優先順序的執行緒了
      • 因為priority沒有ceiling高
    • Priority Inheritance
      • 中優先順序的執行緒無法搶佔低優先順序執行緒了
      • 因為priority沒有紅色執行緒高
      • 低優先順序inherit(繼承)自高優先順序, 中優先順序的不會比高優先順序執行緒高

2.10. 跨程序共享鎖

  • process shared

  • 讓鎖可以被跨程序共享

  • 看看多程序, fork函式建立程序 ```

fork()

PID = 0  PID > 0 ```

  • 程序共享鎖

    • 通過程序的共享記憶體, 進行鎖的共享 ```

int shmid = shmget(1616, sizeof(TShared), IPC_CREAT|0666);

TShared shm = (Tshared )shmat(shmid, NULL,0);

pthread_mutex_init(&shm->Lock, 帶上pshared屬性); ```

  • 程序共享鎖和共享鎖是什麼區別?

    • 絕大多數場景是用在讀寫鎖的讀鎖裡面
    • 很少用
    • 排程的單位往往是執行緒
    • 涉及到程序共享, 容易出錯, 儘量少用

2.11. 遞迴鎖

  • mutex type 

    • NORMAL 不記錄持有人, 重複加鎖, 無休止等待, 死鎖
    • DEFAULT 保留名
    • ERRORCHECK 不能重複加鎖解鎖, 會報錯
    • RECURSIVE 允許同一執行緒加N次鎖, 但也要解鎖N次才會釋放, 會報成功 ```

void foo() {

... 申請鎖();

...foo();

...釋放鎖();

} ```

  • 不會被自己鎖住, 即使遞迴呼叫也能正常申請到鎖

2.12. 鎖的Robust機制

  • robust 使健壯

    • STALLED(預設) / ROBUST
    • 如果沒解鎖執行緒就跪了;
    • 啥也不做 / 讓下個執行緒處理
  • 當持有一個鎖的執行緒還沒釋放就掛了, 會發生什麼?

    • STALLED 
      • 無法申請到鎖, 未定義的行為. 
      • 後面申請這個鎖的執行緒可能會一直wait
    • ROBUST
      • 下一個申請這個鎖的執行緒會收到一個 EOWNERDEAD 錯誤
      • 第三個第四個申請鎖的執行緒會處於waiting狀態
      • 這個執行緒可以嘗試恢復上一個執行緒掛掉之後對鎖或對程式執行邏輯的影響
      • 如果恢復成功, 就可以呼叫pthread_mutex_consistent()函式來標記這個鎖已經恢復正常
      • 然後這個鎖就相當於被這個執行緒給持有了
      • 當這個執行緒釋放鎖了之後, 其他後面的執行緒就可以正常使用鎖了
      • 如果恢復失敗, 這個鎖就會永遠處於不可用的狀態, 只能通過pthread_mutex_destroy()來回收這個鎖
    • Robust工作原理
      • 執行一些恢復邏輯
      • pthread_mutex_consistent() -> 釋放鎖

2.13. 在不同場景下一個執行緒重複加解鎖

  • 重複加解鎖會怎麼樣?

    • mutex type
    • robust
    • 同一個執行緒重複加解鎖
  • type       robust   重複加鎖(自己給自己加鎖) 重複解鎖(自己給自己解鎖)

  • NORMAL     STALLED  死鎖                      未定義行為

  • NORAML     ROBUST   死鎖                      報錯

  • ERRORCHECK 任意值   返回錯誤                  報錯

  • RECURSIVE  任意值   重複加鎖                  報錯

  • DEFAULT    STALLED  未定義行為                未定義行為

  • DEFAULT    ROBUST   未定義行為                報錯

2.14. 死鎖

  • 互相鎖死就是死鎖

  • 單執行緒可能死鎖嗎? 可能, 自己重複加鎖

  • 主要是多執行緒下的死鎖

  • 怎麼辦?

    • 按照順序去加鎖
    • 第一個執行緒完成自己的任務後, 第二個執行緒才能申請到資源

2.15. 讀寫鎖

  • 解決讀多寫少的問題

  • 期望避免在臨界區執行的時候, 其他執行緒進入到臨界區產生干擾

  • 如果別的執行緒進來是為了讀取資料, 進入臨界區, 不做壞事

  • 申請寫鎖, 

  • 申請讀鎖, 

  • 根據執行緒的行為來申請讀寫鎖, 提高多執行緒的效能

  • 讀寫鎖

    • 專治讀得太多, 寫得太少
    • 建立和銷燬 pthread_rwlock_init & phtread_rwlock_destroy
    • 讀鎖 phtread_rwlock_rdlock & pthread_rwlock_tryrdlock
    • 寫鎖 pthread_rwlock_wrlock & pthread_rwlock_trywrlock
    • 解鎖 pthread_rwlock_unlock
  • 讀寫鎖的2個屬性

    • PShared 是否程序間共享
    • Kind (防止寫飢餓)
      • Prefer Reader 讀優先, 預設值
        • 寫飢餓, 當讀優先的時候, 寫執行緒必須要等待前面的讀執行緒都執行結束了, 才能得到執行
        • 讀鎖是共享出去的, 多個執行緒使用
      • Prefer Writer Non-Recursive 寫優先
        • 只要申請寫鎖時, 後面申請讀鎖就不讓進來了 
      • Prefer Writer 同上, glibc中只提供PWNR, 新增這個只是為了跟POSIX對齊

章節3 多執行緒下的各種機制

3.16. Thread Specific Data - TSD

  • TSD是幹什麼的?

    • 記憶體中的資料, 線上程間是被共享的, 
    • 如果想有一個數據, 只能被本執行緒內所有函式訪問
    • 不能被別的執行緒訪問, 應該怎麼辦?
    • 定義的資料是在棧上開闢的, 只能自己訪問.
    • 定義的資料是在堆上開闢的, 所有執行緒都能訪問, 怎麼辦? TSD!
  • Thread-specific Data API列表

    • 建立 int pthread_key_create(pthread_key_t *key, void (*destructor)(void *))
    • 刪除 int pthread_key_delete(pthread_key_t key)
    • 寫入 int pthread_setspecific(pthread_key_t key, const void *value)
    • 讀取 void * pthread_getspecific(pthread_key_t key)
  • 不同的執行緒拿到同一個key, 拿到的也是自己維護的資料, 不同執行緒可以共用key

  • TSD還是比較慢的, 儘量少用!

3.17. Condition Variables 條件變數

  • 一種執行緒同步的方式, 執行緒進入臨界區前, 等待訊號

  • 例子

      1. 你自己看一下, 只要房間沒人, 你就可以進
      • 進衛生間, 加鎖, 
      1. 不管房間是不是有人, 別人叫你進了, 你才能進.
      • 進辦公室, 也可以用加鎖的方式實現, 但有一個問題
      • 如果辦公室從一開始就沒人, 辦公室裡沒有任何工作人員
      • 加鎖的方式解決這個問題, 關鍵的點是, 之前辦公室裡必須要有人
      • 假設加鎖的執行緒, 晚於臨界區, 無法進入等待狀態
      • => Condition Variables! 條件訊號, 有可能是遲來的
      • 條件變數的定義, 收到訊號, 辦公室來人了發訊號
      • 鎖是先獲得鎖的執行緒可以讓別的執行緒等待
      • 條件變數, 先等待訊號, 再進入臨界區
      • 進入臨界區的先後順序不同.
  • Condition Variables

    • 設定條件讓別的執行緒等待, 通過條件變數讓別的執行緒繼續執行
    • int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr)
    • int pthread_cond_destroy(pthread_cond_t *cond)
    • int pthread_cond_signal(pthread_cond_t *cond) 只會有一個人進入臨界區
    • int pthread_cond_broadcast(pthread_cond_t *cond)
      • 廣播訊號, 所有等待的人都會收到訊號
      • 收到訊號都會進入臨界區
    • int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
  • Condition Variable的2個屬性

    • process shared 
      • shared / private(預設)
      • 條件變數是否可以跨程序?
      • 需要跟mutex的process share屬性相配合
    • clock
      • int值; 各種巨集, 但只能用: CLOCK_REALTIME(預設)/CLOCK_MONOTONIC(不受系統時間影響)
      • 控制超時時鐘, 用於條件變數的timewait函式
    • 一般都是走預設

3.18. 使用條件變數要注意的點

  • 一定要跟mutext一起用

  • 不跟mutext一起用的情況

    • Condition Variable的空等現象 1 ```

void thread_conciton_1() {

done = 1;

pthread_cond_signal(&condition_variable_signal)l

}

void thread_function_w() {

while (done == 0) {

// 可能會引入bug的地方, fuc1立刻跑完了, 訊號已經扔出去過了

pthread_cond_wait(&condition_variable_signal, NULL);

}

} ``` - Condition Variable的空等現象 2

    • 沒有先進行條件檢測, 再等待條件 (注意while和if的選擇)
    • fuc1的訊號可能已經發出過了
    • 一定要在臨界區扔出條件變數
      • Condition Variable的資料保護
      • 要在臨界區釋放mutex鎖
      • 流程圖
        • 其他執行緒乘虛而入的機會
        • 臨界區 -> 扔訊號 -> 喚醒其他執行緒
        • 臨界區裡面扔訊號 喚醒其他執行緒
  • 條件變數Condition Variable三大注意

      1. 一定要結合Mutex使用, 空等
      1. 一定要先進行條件檢測, 空等
      1. 一定要在臨界區扔訊號, bug
  • 在Mutex裡面去做訊號的監聽

    • 監聽訊號時, 訊號沒有到, 此時執行緒會被掛起
    • 前面申請的Mutex就會被釋放出來, 其他的條件變數執行緒就會能夠獲得這個鎖

3.19. Semaphore訊號量

  • 條件變數不能控制數量, 訊號量可以控制數量

  • 如果一個景區同時只能固定數量的人蔘觀, 應該怎麼辦?

    • 鎖也可以做, 5個人5把鎖, 但是這樣做很麻煩
    • 條件變數, count計數, 判斷, 此時count是49同時有10個人檢視, 進入59人
    • 針對count要有個鎖, 實現麻煩
    • => Semaphore! 
    • 一臺電腦只聯接了3個印表機
      • Semaphore去實現會更優雅
      • 作業系統會提供相應實現
  • Semaphore相關API

    • 不同作業系統
      • int sem_init(sem_t *sem, int pshared, unsigned int value)
      • int sem_open(const char *name, int oflag, ...)
    • int sem_post(sem_t *sem) // value 加1
    • int sem_wait(sem_t *sem) // value 減1
    • int sem_destroy(sem_t *sem) // 釋放訊號量
    • int sem_getvalue(sem_t *sem, int *sval) // 獲得訊號量當前的值
  • Semaphore使用: 值 > 0、post +1、wait -1

  • 生產者消費者程式碼

    • 生產者每生產完一個產品post +1
    • 消費者每消費一個產品wait -1
    • 用完destroy
    • 建立銷燬+1-1

3.20. Barrier多執行緒柵欄函式

  • 等別的執行緒都到某個點後, 我再繼續

    • 建一個房子, 三件事前準備, 找三個幫手同時辦三件事情
    • 三個事情都完成, 才能開始後面的事情
    • 先完成手上的事情後, 才能進行下一步
    • => Barrier!
  • Barrier相關API

    • 設立一個柵欄, 讓相關的執行緒走到預定位置就wait, 所有人都走了, 再繼續.
    • int pthread_barrier_init(pthread_barrier_t *barrier, const pthread_barrierattr_t *attr, unsigned int count)
    • int pthread_barrier_destroy(pthread_barrier_t *barrier)
    • `int pthread_barrier_wait(pthread_barrier_t *barrier) 只要湊夠一開始建立的執行緒的數量, 就能達到目的
  • Barrier只有1個屬性

    • Process-shared
    • 可選項 private(預設) / shared
    • 作用 是否可以進行跨程序共享
  • Barrier到底是個啥?

    • 解決了什麼問題?
    • 三個執行緒不知道什麼時候開始, 什麼時候結束
    • 需要在某個時刻, 三個執行緒需要同時觸發
    • 第一個執行緒完成任務 -> 柵欄點, 等待
    • 第二個執行緒完成任務 -> 柵欄點, 等待
    • 第三個執行緒完成任務 -> 柵欄點, 等待
    • 所有的執行緒都到達了柵欄點 => 所有的執行緒可以繼續了
  • 天選之子機制

    • wait函式返回的值是一個巨集定義
    • 其他的執行緒返回的是0
    • 所有wait的執行緒喚醒之後
    • 只有一個執行緒收到的返回值是PTHREAD_BARRIER_SERIAL_THREAD
    • 其它執行緒收到的返回值是0
    • int result = phterad_barrier_wait(&mybarrier);
  • 天選之子可以幹什麼用?

    • 多執行緒歸併排序, 
    • 需要一個唯一的執行緒做結果的歸併

課程學習導圖

發文不易, 喜歡點讚的人更有好運氣👍 :), 定期更新+關注不迷路~

ps:歡迎加入筆者18年建立的研究iOS稽核及前沿技術的三千人扣群:662339934,坑位有限,備註“掘金網友”可被群管通過~