iOS之深入探究多线程实现、线程安全和线程死锁
生活随笔
收集整理的這篇文章主要介紹了
iOS之深入探究多线程实现、线程安全和线程死锁
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、線程與進程
① 線程與進程的定義
- 線程
- 線程是進程的基本執行單元,一個進程的所有任務都在線程中執行;
- 進程要想執行任務,必須得有線程,進程至少要有一條線程;
- 程序啟動會默認開啟一條線程,這條線程被稱為主線程或者 UI 線程。
- 進程
- 進程是指在系統中正在運行的一個應用程序;
- 每個進程之間是獨立的,每個進程均運行在其專用的且受保護的內存空間內;
- 通過“活動監視器”可以查看 mac 系統中所開啟的線程。
② 線程與進程的關系
- 地址空間:同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間;
- 資源擁有:同一進程內的線程共享本進程的資源如內存、I/O、 cpu等,但是進程之間的資源是獨立的;
- 一個進程崩潰后,在保護模式下不會對其他進程產生影響,但是一個線程崩潰整個進程都死掉,所以多進程要比多線程健壯;
- 進程切換時,消耗的資源大,效率高。所以涉及到頻繁的切換時,使用線程要好于進程。同樣如果要求同時進行并且又要共享某些變量的并發操作,只能用線程不能用進程;
- 執行過程:每個獨立的進程有一個程序運行的入口、順序執行序列和程序入口。但是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制;
- 線程是處理器調度的基本單位,但是進程不是;
- 線程沒有地址空間,線程包含在進程地址空間中。
③ 線程與Runloop的關系
- runloop 與線程是一一對應的,一個 runloop 對應一個核心的線程,為什么說是核心的,是因為 runloop 是可以嵌套的,但是核心的只能有一個,它們的關系保存在一個全局的字典里;
- runloop 是來管理線程的,當線程的 runloop 被開啟后,線程會在執行完任務后進入休眠狀態,有了任務就會被喚醒去執行任務;
- runloop 在第一次獲取時被創建,在線程結束時被銷毀;
- 對于主線程來說,runloop 在程序一啟動就默認創建好了;
- 對于子線程來說,runloop 是懶加載的,只有當我們使用的時候才會創建,所以在子線程用定時器要注意:確保子線程的 runloop 被創建,不然定時器不會回調。
二、多線程概念
① 多線程的原理
- 對于單核 CPU,同一時間,CPU 只能處理一條線程,換言之,同一時間段內只有一條線程在執行;
- iOS 中的多線程同時執行的本質是 CPU 在多個任務直接進行快速的切換,由于 CPU 調度線程的時間足夠快,就造成了多線程的“同時”執行的效果;
- 如需線程數非常多,CPU 會在 N 個線程之間切換,消耗大量的 CPU 資源;每個線程被調度的次數會降低,線程的執行效率會降低;
- 多線程是一個比較輕量級的方法,來實現單個應用程序內多個代碼執行路徑;
- 在系統級別內,程序并排執行,程序分配到每個程序的執行時間是基于該程序的所需時間和其他程序的所需時間來決定的;
- 然而,在每個程序內部,存在一個或者多個執行線程,它同時或在一個幾乎同時發生的方式里執行不同的任務。
② 多線程的意義
- 多線程的優勢:
- 能適當提高程序的執行效率;
- 能適當提高資源的利用率,如CPU、內存;
- 線程上的任務執行完成后,線程會自動銷毀;
- 多線程的劣勢:
- 開啟線程需要占用一定的內存空間,默認情況下,每一個線程占用512KB;
- 如果開啟大量線程,會占用大量的內存空間,降低程序的性能;
- 線程越多,CPU 在調用線程上的開銷就越大;
- 程序設計更加復雜,比如線程間的通信,多線程的數據共享。
③ 多線程的生命周期
- 就緒:線程對象調用 start 方法,將線程對象加入可調度線程池,等待 CPU 的調用,即調用 start 方法,并不會立即執行,而是進入就緒狀態,需要等待一段時間,經 CPU 調度后才執行,也就是從就緒狀態進入運行狀態;
- 運行:CPU 負責調度可調度線城市中線程的執行,在線程執行完成之前,其狀態可能會在就緒和運行之間來回切換,這個變化是由 CPU 負責,開發人員無法干預;
- 阻塞:當滿足某個預定條件時,可以使用休眠,即 sleep,或者同步鎖,阻塞線程執行。當進入sleep時,會重新將線程加入就緒中;以 NSThread 為例,進行休眠時間設置:
- sleepUntilDate: 阻塞當前線程,直到指定的時間為止,即休眠到指定時間;
- sleepForTimeInterval: 在給定的時間間隔內休眠線程,即指定休眠時長;
- 同步鎖:@synchronized(self);
- 死亡:分為兩種情況:
- 正常死亡,即線程執行完畢;
- 非正常死亡,即當滿足某個條件后,在線程內部(或者主線程中)終止執行(調用exit方法等退出);
- 處于運行中的線程擁有一段可以執行的時間,即時間片(CPU在多個任務直接進行快速切換的時間間隔稱為時間片):
- 如果時間片用盡,線程就會進入就緒狀態隊列;
- 如果時間片沒有用盡,且需要開始等待某事件,就會進入阻塞狀態隊列;
- 等待事件發生后,線程又會重新進入就緒狀態隊列;
- 每當一個線程離開運行,即執行完畢或者強制退出后,會重新從就緒狀態隊列中選擇一個線程繼續執行;
④ 線程池
- 飽和策略
- AbortPolicy:直接拋出RejectedExecutionExeception異常來阻止系統正常運行;
- CallerRunsPolicy:將任務回退到調用者;
- DisOldestPolicy:丟掉等待最久的任務;
- DisCardPolicy:直接丟棄任務。
三、多線程實現
| pthread | 一套通用的線程API,適用于Unix/Linux/Window等系,跨平臺,可移植,使用難度大 | C | 程序員管理 | 幾乎不用 |
| NSThread | 使用更加面向對象,可直接操作線程對象 | OC | 程序員管理 | 偶爾使用 |
| GCD | 旨在代替NSThread等線程技術,充分利用設備的多核 | C | 自動管理 | 經常使用 |
| NSOperation | 基于GCD,比GCD多了部分更加簡單實用的工能,使用更加面向對象 | OC | 自動管理 | 經常使用 |
① pthread
- POSIX線程(POSIX threads),簡稱Pthreads,是線程的POSIX標準。該標準定義了創建和操縱線程的一整套API。
- 在類Unix操作系統(Unix、Linux、Mac OS X等)中,都使用Pthreads作為操作系統的線程。
- 簡單地說,這是一套在很多操作系統上都通用的多線程API,所以移植性很強。
- 需要導入#import <pthread.h>,使用如下:
- 看代碼就會發現它需要 c 語言函數,這是比較難收的,更難受的是還需要手動處理線程的各個狀態的轉換即管理生命周期,比如,這段代碼雖然創建了一個線程,但并沒有銷毀。
② NSThread
- NSThread 是經過蘋果封裝后的,并且完全面向對象的。所以可以直接操控線程對象,非常直觀和方便。
- 但是,它的生命周期還是需要手動管理,所以這套方案也是偶爾用用,比如 [NSThread currentThread],它可以獲取當前線程類,你就可以知道當前線程的各種屬性,用于調試十分方便。
- 先創建線程類,再啟動:
- 創建并自動啟動:
- 使用 NSObject 的方法創建并自動啟動:
- NSThread 的其他方法:
③ GCD
- Grand Central Dispatch,它是蘋果為多核的并行運算提出的解決方案,所以會自動合理地利用更多的CPU內核(比如雙核、四核),最重要的是它會自動管理線程的生命周期(創建線程、調度任務、銷毀線程),完全不需要手動管理,我們只需要告訴它執行什么就行。
- 同時它使用的也是 c 語言,不過由于使用了 Block(Swift里叫做閉包),使得使用起來更加方便,而且靈活。
- GCD 的使用,請參考我的博客:iOS之深入分析GCD的函數與隊列以及多種組合使用。
④ NSOperation
- 雖然 GCD 的功能已經很強大,但是它使用的 API 依然是 C 語言的。在某些時候,在面向對象的objective-c中使用起來非常的不方便和不安全。
- 所以蘋果把 GCD 中的操作抽象成 NSOperation 對象,把隊列抽象成 NSOperationQueue 對象。
- NSOperation 特點:
- 可以控制暫停、恢復、停止:suspended、cancel、cancelAllOperations;
- 可以控制任務的優先級:threadPriority 和 queuePriority;
- 可以設置依賴關系:addDependency 和 removeDependency;
- 可以控制并發個數:maxConcurrentOperationCount;
- NSOperation 有兩個封裝的便利子類 NSBlockOperation、NSInvocationOperation, 它們都使用了并發隊列。
- NSOperation 只是一個抽象類,所以不能封裝任務。但它有 2 個子類用于封裝任務,分別是:NSInvocationOperation 和 NSBlockOperation 。創建一個 Operation 后,需要調用 start 方法來啟動任務,它會默認在當前隊列同步執行。當然也可以在中途取消一個任務,只需要調用其 cancel 方法即可。
- NSInvocationOperation 使用如下,需要傳入一個方法名:
- NSBlockOperation 使用如下:
- 這樣的任務,默認會在當前線程執行。但是 NSBlockOperation 還有一個方法:addExecutionBlock: ,通過這個方法可以給 Operation 添加多個執行 Block。這樣 Operation 中的任務會并發執行,它會在主線程和其它的多個線程執行這些任務,如下:
- 打印輸出如下:
- addExecutionBlock 方法必須在 start() 方法之前執行,否則就會報錯:
- 除了上面的兩種 Operation 以外,還可以自定義 Operation。自定義 Operation 需要繼承 NSOperation 類,并實現其 main() 方法,因為在調用 start() 方法的時候,內部會調用 main() 方法完成相關邏輯。
- 到此為止,我們可以調用一個 NSOperation 對象的 start() 方法來啟動這個任務,但是這樣它會默認是同步執行的,就算是 addExecutionBlock 方法,也會在當前線程和其他線程中執行,也就是說還是會占用當前線程,這是就要用到隊列 NSOperationQueue 了。并且,按類型來說的話一共有兩種類型:主隊列、其他隊列,只要添加到隊列,會自動調用任務的 start() 方法。
- 主隊列:
- 其他隊列的任務會在其他線程并行執行:
⑤ C和OC的橋接
- __bridge只做類型轉換,但是不修改對象(內存)管理權;
- __bridge_retained(也可以使用CFBridgingRetain)將Objective-C的對象轉換為 Core Foundation的對象,同時將對象(內存)的管理權交給我們,后續需要使用 CFRelease或者相關方法來釋放對象;
- __bridge_transfer(也可以使用CFBridgingRelease)將Core Foundation的對象轉換為Objective-C的對象,同時將對象(內存)的管理權交給ARC。
四、線程安全問題
當多個線程同時訪問一塊資源時,容易引發數據錯亂和數據安全問題,這個時候就需要互斥鎖(即同步鎖)和自旋鎖來解決了。
① 互斥鎖
- 作用與意義:
- 用于保護臨界區,確保同一時間,只有一條線程能夠執行;
- 如果代碼中只有一個地方需要加鎖,大多都使用 self,這樣可以避免單獨再創建一個鎖對象;
- 加了互斥鎖的代碼,當新線程訪問時,如果發現其他線程正在執行鎖定的代碼,新線程就會進入休眠。
- 注意:
- 互斥鎖的鎖定范圍,應該盡量小,鎖定范圍越大,效率越差;
- 能夠加鎖的任意 NSObject 對象;
- 鎖對象一定要保證所有的線程都能夠訪問。
② 自旋鎖
- 自旋鎖與互斥鎖類似,但它不是通過休眠使線程阻塞,而是在獲取鎖之前一直處于忙等(即原地打轉,稱為自旋)阻塞狀態;
- 使用場景:鎖持有的時間短,且線程不希望在重新調度上花太多成本時,就需要使用自旋鎖,屬性修飾符 atomic,本身就有一把自旋鎖;
- 加入了自旋鎖,當新線程訪問代碼時,如果發現有其他線程正在鎖定代碼,新線程會用死循環的方法,一直等待鎖定的代碼執行完成,即不停的嘗試執行代碼,比較消耗性能。
③ 互斥鎖和自旋鎖對比
- 相同點:在同一時間,保證了只有一條線程執行任務,即保證了相應同步的功能;
- 不同點:
- 互斥鎖:發現其他線程執行,當前線程 休眠(即就緒狀態),進入等待執行,即掛起。一直等其他線程打開之后,然后喚醒執行;
- 自旋鎖:發現其他線程執行,當前線程 一直詢問(即一直訪問),處于忙等狀態,耗費的性能比較高;
- 場景:根據任務復雜度區分,使用不同的鎖,但判斷不全時,更多是使用互斥鎖去處理:
- 當前的任務狀態比較短小精悍時,用自旋鎖;
- 反之則用互斥鎖。
④ atomic 原子鎖 & nonatomic 非原子鎖
- atomic是原子屬性,是為多線程開發準備的,默認屬性。
- 僅僅在屬性的 setter 方法中,增加了鎖(自旋鎖),能夠保證同一時間,只有一條線程對屬性進行寫操作;
- 同一時間 單(線程)寫多(線程)讀的線程處理技術;
- Mac開發中常用;
- nonatomic 是非原子屬性:
- 沒有鎖,性能高;
- 移動端開發常用;
五、線程與隊列
- 隊列是保存以及管理任務的,將任務加到隊列中,任務會按照加入到隊列中先后順序依次執行。
- 如果是全局隊列和并行隊列,則系統會根據系統資源去創建新的線程去處理隊列中的任務,線程的創建、維護和銷毀由操作系統管理,還有隊列本身是線程安全的。
- 使用 NSOperationQueue 實現多線程的時候是可以控制線程總數及線程依賴關系的,而 GCD 只能選擇并行或者串行隊列。
① 資源競爭
- 多線程同時執行任務能提高程序的執行效率和響應時間,但是多線程不可避免地遇到同時操作同一資源的情況。例如,如下一個資源競爭的問題,該怎么解決呢?
- 解決辦法:
- @property (nonatomic, strong) NSString *target;將nonatomic改成atomic;
- 將并行隊列 DISPATCH_QUEUE_CONCURRENT 改成串行隊;
- DISPATCH_QUEUE_SERIAL;
- 異步執行 dispatch_async 改成同步執行 dispatch_sync;
- 賦值使用 @synchronized 或者上鎖。
② 死鎖
任何事情都有兩面性,就像多線程能提升效率的同時,也會造成資源競爭的問題。而鎖在保證多線程的數據安全的同時,粗心大意之下也容易發生問題,那就是死鎖。
NSOperationQueue
- 鑒于 NSOperationQueue 高度封裝,使用起來非常簡單,一般不會出現什么問題。如下,案例展示了一個不好示范,通常我們通過控制 NSOperation 之間的從屬關系,來達到有序執行任務的效果,但是如果互相從屬或者循環從屬都會造成所有任務無法開始。
- 解決辦法:
GCD
- 在主線程同步執行造成 EXC_BAD_INSTRUCEION 錯誤:
- 和主線程同步執行類似,在串行隊列中嵌套使用同步執行任務,同步隊列 task1 執行完成后才能執行 task2 ,而 task1 中嵌套了task2 導致 task1 注定無法完成。
- 嵌套同步執行任務確實很容易出 bug ,但不是絕對,將同步隊列DISPATCH_QUEUE_SERIAL 換成并行隊列 DISPATCH_QUEUE_CONCURRENT 這個問題就迎刃而解。修改成并行隊列后案例中 task1 仍然要先執行完嵌套在其中的 task2 ,而 task2 開始執行時,隊列會另起一個線程執行 task2 , task2 執行完成后 task1 繼續執行。
- 在很多人印象中,異步執行不容易發生互相等待的情況,確實,即使是串行隊列,異步任務會等待當前任務執行后再開始:
- 常規死鎖,在已經上鎖的情況下再次上鎖,形成彼此等待的局面:
- 要解決也比較簡單,將 NSLock 換成遞歸鎖 NSRecursiveLock,遞歸鎖就像普通的門鎖,順時針轉一圈加鎖后,逆時針一圈即解鎖;而如果順時針兩圈,同樣逆時針兩圈即可解鎖。
③ 問題理解
主線程環境中,在主隊列上執行同步任務,為什么會死鎖?
- 假設當前執行的代碼是包含在任務1中,在主隊列上執行的同步任務為任務2,如下所示:
- 同步角度思考:由于是是同步任務,所以任務1此時需要等待任務2執行,任務2執行完畢后任務1才能繼續執行下去;
- 隊列角度思考:任務2會被加到主隊列的隊尾,由于串行隊列的特性,任務必須一個一個執行。因此任務2需要等待隊列中其他任務(包括任務1)都執行完之后才會輪到它去執行;
- 結果:所以出現了任務2等待任務1,任務1等待任務2的情況,導致死鎖。
此外如果串行隊列綁定線程a,那么在線程a環境中,在該串行隊列上執行同步任務,也會導致死鎖,原因同上。
主線程環境中,為什么在新創建的串行隊列中執行同步任務就不會死鎖?
- 假設當前執行的代碼是包含在任務1中,在串行隊列上執行的同步任務為任務2,如下所示:
- 同步角度思考:由于是是同步任務,所以任務1此時需要等待任務2執行,任務2執行完畢后任務1才能繼續執行下去;
- 隊列角度思考:任務2會被加到串行隊列zcp的隊尾,任務2只跟隊列YDW中的其他任務有先后順序關系,跟其他隊列上的任務無關,也就是說任務2跟主隊列中的其他任務無關,所以任務2不會等待任務1;
- 結果:任務1等待任務2,任務2不用等待任務1,任務2執行完畢后,然后繼續執行任務1。
總結
以上是生活随笔為你收集整理的iOS之深入探究多线程实现、线程安全和线程死锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS经典面试题之“runtime是如何
- 下一篇: iOS之深入分析GCD的函数与队列以及多