iOS多執行緒調研

Perfect_Dream發表於2017-12-19

關於多執行緒的基礎釋義就不多做解釋了,下面引用一句百度百科上的話作為開頭:

多執行緒是為了同步完成多項任務,不是為了提高執行效率,而是為了提高資源使用效率來提高系統的效率。執行緒是在同一時間需要完成多項任務的時候實現的。

本文涉及內容

  • 多執行緒的目的
  • 多執行緒的優缺點
  • 為什麼要用多執行緒
  • 什麼時候使用多執行緒
  • iOS執行緒狀態
  • iOS執行緒安全
  • iOS常見多執行緒
    • Pthread
    • NSThread
    • NSOperation
    • Grand Central Dispatch(GCD)

一. 目的

隨著計算機技術的發展,程式設計模型也越來越複雜多樣化。但多執行緒程式設計模型是目前計算機系統架構的最終模型。隨著CPU主頻的不斷攀升,X86架構的硬體已經成為瓶,在這種架構的CPU主頻最高為4G。事實上目前3.6G主頻的CPU已經接近了頂峰。   如果不能從根本上更新當前CPU的架構(在很長一段時間內還不太可能),那麼繼續提高CPU效能的方法就是超執行緒CPU模式。那麼,作業系統、應用程式要發揮CPU的最大效能,就是要改變到以多執行緒程式設計模型為主的並行處理系統和併發式應用程式。   所以,掌握多執行緒程式設計模型,不僅是目前提高應用效能的手段,更是下一代程式設計模型的核心思想。多執行緒程式設計的目的,就是"最大限度地利用CPU資源",當某一執行緒的處理不需要佔用CPU而只資源打交道時,讓需要佔用CPU資源的其它執行緒有機會獲得CPU資源。從根本上說,這就是多執行緒程式設計的最終目的。   也就是說多執行緒的目的不是為了提高執行效率,而是為了提高資源使用效率,為了最大限度地利用CPU資源。   其實仔細想想,如果這就是正確答案的話就不可避免的產生一個矛盾。假設一下,有一個任務需要分成不等的小任務執行,如果按照上述結果看,最好是分成儘可能多的執行緒併發處理,這樣可以最大限度的利用CPU的資源,並且可以最短時間內完成任務。但是某些情況會出現不一樣的情況,比如說這個任務是資料庫的增刪改查,為了保證資料的正確性我們需要進行加鎖同步等處理。任務執行時CPU還需要不斷的切換子執行緒進行任務處理,這樣看不光沒有提高執行效率,反而拉低了執行效率,而且大量佔用了CPU資源,浪費很多的記憶體空間(預設情況下,主執行緒佔用1M,子執行緒佔用512KB),那麼到底該不該使用多執行緒?

二. 優缺點

從上述問題來看,到底該不該使用多執行緒,為什麼要用多執行緒就是個問題了。為了想清楚這個問題,接下來先了解一下多執行緒的優缺點:

1. 多執行緒的優點

  • 使用執行緒可以把佔據時間長的程式中的任務放到後臺去處理
  • 使用者介面可以更加吸引人,這樣比如使用者點選了一個按鈕去觸發某些事件的處理,可以彈出一個進度條來顯示處理的進度
  • 程式的執行速度可能加快
  • 在一些等待的任務實現上如使用者輸入、檔案讀寫和網路收發資料等,執行緒就比較有用了。在這種情況下可以釋放一些珍貴的資源如記憶體佔用等等。
  • 多執行緒技術在IOS軟體開發中也有舉足輕重的位置。
  • 執行緒應用的好處還有很多,就不一一說明了   -- 引自百度百科

2. 多執行緒的缺點

  • 如果有大量的執行緒,會影響效能,因為作業系統需要在它們之間切換。
  • 更多的執行緒需要更多的記憶體空間。
  • 執行緒可能會給程式帶來更多“bug”,因此要小心使用。
  • 執行緒的中止需要考慮其對程式執行的影響。
  • 通常塊模型資料是在多個執行緒間共享的,需要防止執行緒死鎖情況的發生。   -- 引自百度百科

三. 為什麼要用多執行緒

為什麼要使用多執行緒,維基百科上有這樣一段話:

一個執行緒持續執行,直到該執行緒被一個事件擋住而製造出長時間的延遲(可能是記憶體load/store操作,或者程式分支操作)。該延遲通常是因快取失敗而從核心外的記憶體讀寫,而這動作會使用到幾百個CPU週期才能將資料回傳。與其要等待延遲的時間,執行緒化處理器會切換執行到另一個已就緒的執行緒。只要當之前執行緒中的資料送達後,上一個執行緒就會變成已就緒的執行緒。這種方法來自各個執行緒的指令交替執行,可以有效的掩蓋記憶體訪問時延,填補流水線空洞。 舉例來說: 週期 i :接收執行緒 A 的指令 j 週期 i+1:接收執行緒 A 的指令 j+1 週期 i+2:接收執行緒 A 的指令 j+2,而這指令快取失敗 週期 i+3:執行緒排程器介入,切換到執行緒 B 週期 i+4:接收執行緒 B 的指令 k 週期 i+5:接收執行緒 B 的指令 k+1 在概念上,它與即時作業系統中使用的合作式多工類似,在該任務需要為一個事件等待一段時間的時候會主動放棄執行時段。 -- 維基百科

假設一下,如果你的程式是單執行緒,然後網路比較慢的情況下下載了一張圖片,我的天,使用者可以洗洗睡了。 再假設一下,現在有三個任務需要處理。單獨一條執行緒處理它們分別需要5、3、3秒。如果三個CPU並行處理,那麼一共只需要5秒。相比於序列處理,節約了6秒。而同步/非同步,描述的是任務之間先後順序問題。假設需要5秒的那個是儲存資料的任務,而另外兩個是UI相關的任務。那麼通過非同步執行第一個任務,我們省去了5秒鐘的卡頓時間。 對於同步執行的三個任務來說,系統傾向於在同一個執行緒裡執行它們。因為即使開了三個執行緒,也得等他們分別在各自的執行緒中完成。並不能減少總的處理時間,反而徒增了執行緒切換(這就是文章開頭舉的例子) 對於非同步執行的三個任務來說,系統傾向於在三個新的執行緒裡執行他們。因為這樣可以最大程度的利用CPU效能,提升程式執行效率。 綜合上述而言我們可以得出結論,在需要同時處理IO和UI的情況下,真正起作用的是非同步,而不是多執行緒。可以不用多執行緒(因為處理UI非常快),但不能不用非同步(否則的話至少要等IO結束)。也就是說非同步方法並不一定永遠在新執行緒裡面執行,反之亦然。 如果這樣說,那什麼時候多執行緒才會起到真正意義上的效率?什麼時候該使用多執行緒進行程式開發?

四. 什麼時候使用多執行緒

首先來了解一下這個概念:“多執行緒的使用主要是用來處理程式‘在一部分上會阻塞’,‘在另一部分上需要持續執行’的場合”。一般是根據需求,可以用多執行緒,事件觸發,callback等方法達到。但是有一些方法是隻有多執行緒能辦到的就只有用多執行緒或者多程式來完成。 舉個簡單的例子,能理解就行。假設有這樣一個程式, 1會不停的處理收到的所有TCP請求。對於每個TCP請求做不同的操作。不能有遺漏 2有很多特定的請求會向一個伺服器傳送儲存的資料,或者是等待使用者輸入。 來看看。第1個要求很簡單。用個while迴圈就搞定了。但第2個特性呢。一旦在等待使用者輸入或者是連線伺服器時,程式會“阻塞”一段時間,這一段時間內就無法處理其他的TCP請求了。 所以可以利用多執行緒,每個執行緒處理不同的TCP請求。這樣程式就不會“阻塞”掉了。 總之,凡事自然有利有弊,多執行緒帶來了高效率的同時也帶來了一定程度我不穩定性,上述內容只是在多執行緒本身的基礎上得出的結論,如果綜合執行緒安全,同步,通訊等情況去看就會變得更加負責。 總結一下就是,當應用在前臺操作的同時還需要進行後臺的計算或邏輯判斷的情況可以使用多執行緒,但是需要綜合考慮使用多執行緒的不穩定性,儘量避免由於使用多執行緒而產生的新問題。

五. iOS執行緒狀態

一般來說,執行緒有五個狀態

  • 新建狀態:執行緒通過各種方式在被建立之初,還沒有呼叫開始執行方法,這個時候的執行緒就是新建狀態;
  • 就緒狀態:在新建執行緒被建立之後呼叫了開始執行方法,但是CPU並不是真正的同時執行多個任務,所以要等待CPU呼叫,這個時候執行緒處於就緒狀態。處於就緒狀態的執行緒並不一定立即執行執行緒裡的程式碼,執行緒還必須同其他執行緒競爭CPU時間,只有獲得CPU時間才可以執行執行緒。
  • 執行狀態:執行緒獲得CPU時間,被CPU呼叫之後就進入執行狀態
  • CPU 負責排程可排程執行緒池中執行緒的執行
  • 執行緒執行完成之前(死亡之前),狀態可能會在就緒和執行之間來回切換
  • 就緒和執行之間的狀態變化由 CPU 負責,程式設計師不能干預
  • 阻塞狀態:所謂阻塞狀態是正在執行的執行緒沒有執行結束,暫時讓出CPU,這時其他處於就緒狀態的執行緒就可以獲得CPU時間,進入執行狀態。
  • 執行緒通過呼叫sleep方法進入睡眠狀態
  • 執行緒呼叫一個在I/O上被阻塞的操作,即該操作在輸入輸出操作完成之前不會返回到它的呼叫者
  • 執行緒試圖得到一個鎖,而該鎖正被其他執行緒持有;
  • 執行緒在等待某個觸發條件
  • 死亡狀態:當執行緒的任務結束,發生異常,或者是強制退出這三種情況會導致執行緒的死亡。執行緒死亡後,執行緒物件從記憶體中移除。一旦執行緒進入死亡狀態,再次嘗試重新開啟執行緒,則程式會掛。

七. iOS執行緒安全

一塊資源可能會被多個執行緒共享,也就是多個執行緒可能會訪問同一塊資源,比如多個執行緒訪問同一個物件、同一個變數、同一個檔案和同一個方法等。因此當多個執行緒訪問同一塊資源時,很容易會發生資料錯誤及資料不安全等問題。因此要避免這些問題,我們需要使用某些方式保證執行緒的安全,比如“執行緒鎖”。

這邊有一段程式碼,可以先用來測試一下:

@property (atomic, assign) int intA;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //thread A
        for (int i = 0; i < 10000; i ++) {
            self.intA = self.intA + 1;
            NSLog(@"Thread A: %d\n", self.intA);
        }
    });

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //thread B
        for (int i = 0; i < 10000; i ++) {
            self.intA = self.intA + 1;
            NSLog(@"Thread B: %d\n", self.intA);
        }
    });
複製程式碼

即使將intA宣告為atomic,最後的結果也不一定會是20000。原因就是因為self.intA = self.intA + 1;不是原子操作,雖然intA的getter和setter是原子操作,但當我們使用intA的時候,整個語句並不是原子的,這行賦值的程式碼至少包含讀取(load),+1(add),賦值(store)三步操作,當前執行緒store的時候可能其他執行緒已經執行了若干次store了,導致最後的值小於預期值。這種場景就是多執行緒不安全的表現之一。

為了更加安全的使用多執行緒,也為了程式碼可以正確的執行,我們需要一種保證執行緒安全的機制,“執行緒鎖”就誕生了,接下來將簡單的瞭解一下iOS中的常用鎖。 在網上查詢了有些資料,發現大多數資料只介紹了以下幾種鎖:

  1. OSSpinLock (暫不建議使用,原因參見這裡
  2. dispatch_semaphore
  3. pthread_mutex
  4. NSLock
  5. NSCondition
  6. pthread_mutex(recursive)
  7. NSRecursiveLock
  8. NSCondition
  9. @synchronized

這張圖片是在網上找到的關於這七種鎖的效能測試:

iOS多執行緒調研
當然除了這七種鎖之外iOS還提供了很多其他的鎖,比如C語言實現的讀寫鎖(Read-write Lock),自旋鎖(Spin Lock)等 這裡就不做對鎖的解釋了,文章下邊會有連結對各種鎖在iOS中的應用做詳細解釋,還有配合iOS常用多執行緒方法寫的一些小demo,對這方面有興趣的可以去看一下。

六. iOS常見多執行緒使用方法

接下來就介紹一下iOS常見的幾種多執行緒實現方式,因為篇幅比較長,所以寫到另外幾篇文章裡邊:

1. Pthreads

這篇文章主要是介紹Pthreads的,裡邊會有Pthreads的基礎釋義,也有常用API與屬性的介紹,當然也會介紹一些常用鎖和Pthreads的配合使用,連結在這Pthread。然而裡邊並不會針對全部的鎖做解析,只是針對某幾種鎖做釋義解析以及與Pthreads的配合使用。

2. NSThread

然後是NSThread這個,在網上找了很多資料,看了很多文章,但是總是不太符合自己的心意。剛好公司有機會讓我參與多執行緒的調研,就試著總結了一下它的常用屬性與API,也有一些加鎖程式碼。

3. GCD

GCD的內容太多了,到發出此連結的時候已經修改過三次了。還是有很多知識沒有涉及到,希望以後有時間補上吧。Grand Central Dispatch,這是GCD調研結果的連結。 ####4. NSOperation 全程看著官方文件寫的,附帶了一些應用例項,函式解析等NSOperation

以上,就是本人對多執行緒的調研結果。調研期間我看到這樣一句話“我們的目的不是研究出多麼牛逼的鎖,而是研究安全更高效的多執行緒方式”,當然,在沒有沒這樣牛逼的多執行緒實現方式的時候,還是有必要了解一下各種鎖的優缺點的。

有志者、事竟成,破釜沉舟,百二秦關終屬楚; 苦心人、天不負,臥薪嚐膽,三千越甲可吞吳.

相關文章