iOS 查漏補缺 - 線程

在這裏插入圖片描述

多線程是我們開發和麪試中都會遇到的一個重要概念,相比於其他編程語言和平臺,iOS 的多線程使用起來要比較友好和易用一些。但是對於多線程的基本概念,我們還是需要重視起來,這對於我們探索 pthreadNSThreadGCD 以及 RunLoop 都大有裨益。

本節的大部分內容基於蘋果官方文檔。文檔地址: About Threaded Programming

前導知識

  • POSIX

POSIX Threads, usually referred to as pthreads, is an execution model that exists independently from a language, as well as a parallel execution model. It allows a program to control multiple different flows of work that overlap in time. Each flow of work is referred to as a thread, and creation and control over these flows is achieved by making calls to the POSIX Threads API.

– POSIX Threads, Wikipedia

譯:

POSIX(Portable Operating System Interface of UNIX,可移植操作系統接口)線程,即 pthreads,是一種不依賴於語言的執行模型,也稱作並行(Parallel)執行模型。其允許一個程序控制多個時間重疊的不同工作流。每個工作流即爲一個線程,通過調用 POSIX 線程 API 創建並控制這些流。

– POSIX 線程,維基百科

  • detached 和 joinable

無論在windows中還是Posix中,主線程和子線程的默認關係是:無論子線程執行完畢與否,一旦主線程執行完畢退出,所有子線程執行都會終止。這時整個進程結束或僵死,部分線程保持一種終止執行但還未銷燬的狀態,而進程必須在其所有線程銷燬後銷燬,這時進程處於僵死狀態。線程函數執行完畢退出,或以其他非常方式終止,線程進入終止態,但是爲線程分配的系統資源不一定釋放,可能在系統重啓之前,一直都不能釋放,終止態的線程,仍舊作爲一個線程實體存在於操作系統中,什麼時候銷燬,取決於線程屬性。在這種情況下,主線程和子線程通常定義以下兩種關係:

1、可會合(joinable):這種關係下,主線程需要明確執行等待操作,在子線程結束後,主線程的等待操作執行完畢,子線程和主線程會合,這時主線程繼續執行等待操作之後的下一步操作。主線程必須會合可會合的子線程。在主線程的線程函數內部調用子線程對象的wait函數實現,即使子線程能夠在主線程之前執行完畢,進入終止態,也必須執行會合操作,否則,系統永遠不會主動銷燬線程,分配給該線程的系統資源也永遠不會釋放。

2、相分離(detached):表示子線程無需和主線程會合,也就是相分離的,這種情況下,子線程一旦進入終止狀態,這種方式常用在線程數較多的情況下,有時讓主線程逐個等待子線程結束,或者讓主線程安排每個子線程結束的等待順序,是很困難或不可能的,所以在併發子線程較多的情況下,這種方式也會經常使用。

在任何一個時間點上,線程是可結合(joinable)或者是可分離的(detached),一個可結合的線程能夠被其他線程回收資源和殺死,在被其他線程回收之前,它的存儲器資源如棧,是不釋放的,相反,一個分離的線程是不能被其他線程回收或殺死的,它的存儲器資源在它終止時由系統自動釋放。

線程的分離狀態決定一個線程以什麼樣的方式來終止自己,在默認的情況下,線程是非分離狀態的,這種情況下,原有的線程等待創建的線程結束,只有當pthread_join函數返回時,創建的線程纔算終止,釋放自己佔用的系統資源,而分離線程沒有被其他的線程所等待,自己運行結束了,線程也就終止了,馬上釋放系統資源。

joinable 和 detaced 其實是主線程與子線程之間的一種關係。app 退出後,detached 線程直接進入終止態,其棧空間資源被系統回收,但是 joinable 線程資源不會被回收,所以可以在 app 退出的時候使用 joinable 線程也就是 pthread 來做一些保存資源到磁盤的操作。也就是說在主線程執行完畢要退出之前,會去處理joinable的線程。

一、線程初探

1.1 線程定義

要先了解什麼是線程,我們需要先了解什麼是進程。對於 iOS 來說,每一個 app 其實就是一個進程,這一點和 Android 有很大的區別。除了單進程之外,在未越獄之前,每個 app 只能訪問每個 app 自身的沙盒環境,不能訪問之外的內容。得益於這樣的設計,iOS 成爲了世界上最安全的操作系統(蘋果是這麼的說的🐶)。

下面給出蘋果官方對於線程的定義

Threads are a relatively lightweight way to implement multiple paths of execution inside of an application.

【譯】線程是在應用程序內部實現多個執行路徑的相對輕量的方法。

From a technical standpoint, a thread is a combination of the kernel-level and application-level data structures needed to manage the execution of code. The kernel-level structures coordinate the dispatching of events to the thread and the preemptive scheduling of the thread on one of the available cores. The application-level structures include the call stack for storing function calls and the structures the application needs to manage and manipulate the thread’s attributes and state.

【譯】從技術角度來看,線程是管理代碼執行所需的內核級和應用程序級數據結構的組合。內核負責線程事件的分發和線程優先級的調度,應用層則負責存儲線程函數中斷時的狀態和屬性的存儲方便下次內核切換時再從存儲的地方運行。

線程的定義可以總結爲以下三點:

  • 線程是進程的基本執行單元,一個進程的所有任務都在線程中執行
  • 進程想要執行任務,必須得有線程,進程至少要有一條線程
  • 程序啓動會默認開啓一條線程,這條線程被稱爲主線程或 UI 線程

下面我們看一下進程的定義:

  • 進程是指在系統中正在運行的一個應用程序
  • 每個進程之間是獨立的,每個進程均運行在其專用的且受保護的內存空間內

這兩句話不難理解,只針對於 iOS 來說,一個 app 就是一個進程,而由於有沙盒機制,每個 app 所對應的進程只能訪問當前 app 沙盒所對應的內存空間,是相互獨立的。

我們的 app 只有一條進程,而線程的話,默認只有一條主線程。我們可以通過多線程技術來開線程然後啓動線程來執行任務。而進程與線程之間的關係可以總結爲下列幾點:

  • 地址空間:同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間。
  • 資源擁有:同一進程內的線程共享本進程的資源如內存、I/O、CPU 等,但是進程之間資源是獨立的。
  • 一個線程崩潰後,在保護模式下不會對其它進程產生影響,但是一個線程崩潰後會導致整個進程的崩潰,所以多進程相比多線程更加健壯。
  • 進程切換時,消耗的資源大,效率低。所以涉及到頻繁的上下文切換的時候,使用線程要好於進程。同樣的,如果要求同時進行且要共享某些變量的併發操作,只能用線程,不能用進程。
  • 執行過程:每個獨立的進程有一個程序運行的入口、順序執行序列和程序入口。但是線程不能獨立執行,必須依存於應用程序中,由應用程序提供多個線程執行控制。
  • 線程是 CPU 調度的基本單位,進程不是。

1.2 線程相關的術語

在深入討論線程及其支持技術之前,有必要弄清楚一些基本術語。

如果你熟悉 UNIX 系統,術語 任務 於表示正在運行的進程,但在本文中並不是這樣定義的。

本文采用的術語如下:

  • 術語 線程 用於指代代碼的獨立執行路徑。
  • 術語 進程 用於指代一個正在運行的可執行文件,它可以包含多個線程。
  • 術語 任務 用於指代需要執行的工作的抽象概念。

1.3 線程的替代方案

手動創建線程會給我們的代碼帶來一定的不確定性。相對來說線程屬於抽象層次比較低且使用起來比較麻煩的一種讓應用程序支持併發的方案。如果你對於直接使用線程不熟悉的話,那麼很容易遇到線程同步和時序問題,其嚴重性可能從細微的問題到應用程序崩潰和用戶數據損壞。

所以如上圖所示,蘋果官方給出了以下幾種線程的替代方案:

  • Operation Objects: 任務對象(NSOpeartion)是 OS X 10.5 推出的一個特性。通過封裝在子線程執行的任務,隱藏底層的線程管理的具體細節,讓開發者聚焦於任務執行的本身。通常來說任務對象會與任務對象隊列(NSOperationQueue)結合使用,來實現在一個或多個線程上任務的執行。
  • GCD: OS 10.6 正式推出,GCD 是另外一種可以讓開發者無需知道線程細節而專注於任務本身的一項技術。通過使用 GCD,你可以定義你需要執行的任務,然後把這個任務加入到一個隊列中來讓 GCD 選擇在一個合適的線程上執行這個任務。隊列考慮了可用核心的數量和當前負載,這樣與直接使用線程相比可以更有效地執行任務。
  • Idle-time notifications: 空閒時通知,對於優先級和複雜度相對來說不高的任務,空閒時通知技術可以在應用程序空閒時執行任務。Cocoa 使用 NSNotificationQueue 來實現這一技術。爲了獲得空閒時通知,你需要使用 NSPostWhenIdle 選項來向 NSNotificationQueue 隊列發送通知,然後直到 Runloop 空閒時,隊列纔會執行的通知裏面的具體任務。

NSNotificationQueue 官方定義

Whereas a notification center distributes notifications when posted, notifications placed into the queue can be delayed until the end of the current pass through the run loop or until the run loop is idle. Duplicate notifications can be coalesced so that only one notification is sent although multiple notifications are posted.

【譯】通知中心收到發出的通知後會向在通知中心註冊的觀察者分發這些通知,而添加到 NSNotificationQueue 隊列中的通知只會在兩種情況下分發出去,分別是當前 Runloop 即將退出或者 Runloop 處於空閒狀態時。重複的向 NSNotificationQueue 隊列中加入通知會導致相同的通知被合併,這樣到了發送時機,對於重複的通知只會發送一條出去。

A notification queue maintains notifications in first in, first out (FIFO) order. When a notification moves to the front of the queue, the queue posts it to the notification center, which in turn dispatches the notification to all objects registered as observers.

【譯】一個通知隊列以先進先出的方式維護着通知。當一個通知移動到了隊列的頭部,隊列會將這個通知發往通知中心,通知中心將通知分派給所有註冊爲觀察者的對象。

Every thread has a default notification queue, which is associated with the default notification center for the process. You can create your own notification queues and have multiple queues per center and thread.

【譯】每一個線程都有一個默認的通知隊列,並且這個通知隊列會與當前進程的默認的通知中心相關聯。但是你可以創建自己的通知隊列,使得每個通知中心和線程有多條通知隊列

關於 NSNotificationQueue 的更多底層細節,可以參考這篇文章 一文全解iOS通知機制

  • Asynchronous functions: 異步的函數。系統自帶的 api 包含許多可以爲你提供自動併發功能的函數。這些函數的實現你無需關心,它們依託於系統的守護進程和進程或者會創建子線程來執行你所需要的任務。
  • Timers: 定時器。你可以在應用程序的主線程上使用定時器來執行週期性的任務,這些任務雖然瑣碎而無法使用線程,但仍需要定期進行維護。
  • Separate processes: 儘管比線程更重,但是在任務僅與應用程序有切線關係的情況下,創建單獨的進程可能會很有用。 如果任務需要大量內存或必須使用 root 特權執行,則可以使用進程。 例如,當 32 位應用程序將結果顯示給用戶時,你可以使用 64 位服務器進程來計算大型數據集。

1.4 線程支持

1.4.1 Cocoa 中線程相關的技術

如上圖所示,這是蘋果官網對於在 Cocoa 框架下能使用的線程技術。

簡單翻譯過來就是:

  • Cocoa Threads: Cocoa 使用 NSThread 來實現線程。除此之外,NSObject 還提供了一攬子方法來派生線程和在已存在線程上執行任務。
  • POSIX threads: POSIX 提供了基於 C 語言的一套創建線程的 API。如果不是創建 Cocoa 程序,那麼這將是最好的創建線程的方案。POSIX 接口使用起來非常簡單,並且爲配置線程提供了足夠的靈活性
  • Multiprocessing Services: 多處理服務是從舊版 Mac OS 過渡到的應用程序使用的基於 C 的舊接口。此技術僅在 OS X 中可用,任何新開發都應避免使用。

線程啓動之後,主要以三種狀態運行,分別是:

  • 運行態
  • 就緒態
  • 阻塞態

如果線程當前未在運行態,則它要麼被阻塞並等待輸入,要麼準備運行,但尚未計劃這樣做。線程繼續在這些狀態之間來回移動,直到最終退出並進入終止狀態。

1.4.2 Runloop

A run loop is a piece of infrastructure used to manage events arriving asynchronously on a thread. A run loop works by monitoring one or more event sources for the thread. As events arrive, the system wakes up the thread and dispatches the events to the run loop, which then dispatches them to the handlers you specify. If no events are present and ready to be handled, the run loop puts the thread to sleep.

【譯】一個運行循環是一個處理線程上所接收到的異步的事件的結構。運行循環管理線程上的一個或多個事件源。當事件到達時,系統將喚醒線程並將事件分配給運行循環,然後運行循環將其分配給你指定的處理程序。如果不存在任何事件或有待處理的事件,則運行循環會將線程置於睡眠狀態。

You are not required to use a run loop with any threads you create but doing so can provide a better experience for the user. Run loops make it possible to create long-lived threads that use a minimal amount of resources. Because a run loop puts its thread to sleep when there is nothing to do, it eliminates the need for polling, which wastes CPU cycles and prevents the processor itself from sleeping and saving power.

【譯】你不需要對你所創建的線程使用運行循環,但是使用運行循環可以提高用戶體驗。運行循環可以創建使用最少資源的常駐線程。因爲當沒事做的時候,運行循環會讓線程休眠,這樣就不許需要通過輪詢這種需要消耗 CPU 的低效操作從而節能。

To configure a run loop, all you have to do is launch your thread, get a reference to the run loop object, install your event handlers, and tell the run loop to run. The infrastructure provided by OS X handles the configuration of the main thread’s run loop for you automatically. If you plan to create long-lived secondary threads, however, you must configure the run loop for those threads yourself.

【譯】要配置運行循環,你要做的就是啓動線程,獲取運行循環對象的引用,安裝事件處理程序,並告訴運行循環運行。 OS X 提供的基礎結構會自動爲你處理主線程運行循環的。但是,如果計劃創建壽命長的輔助線程,則必須自己爲這些線程配置運行循環。

通過上面官方文檔的描述,runloop 其實和線程是緊密關聯的,通過 runloop 可以讓子線程一直存活而不被系統回收。同時,runloop 還能提升用戶體驗,可以重複的在子線程工作而無需爲了執行任務多次開同樣工作內容的線程。

1.4.3 線程同步

使用多線程技術會遇到多個線程同時訪問同一份資源的情況,而如果這些線程同時嘗試使用或修改資源,則會出現嚴重的問題。解決此問題的辦法通常來說有兩種,一種是消除共享資源,讓每個線程獨享其特有的資源進行操作。第二種就是通過 locks(鎖), conditions(條件), atomic operations(原子操作)等其它技術。顯然第二種方案使用頻率更高。

1.4.4 線程間通信

如上圖所示,蘋果官方給出了幾種線程間通信的方式,簡單總結一下如下:

  • 直接消息傳遞: 通過 performSelector 的一系列方法,可以實現由某一線程指定在另外的線程上執行任務。因爲任務的執行上下文是目標線程,這種方式發送的消息將會自動的被序列化。
  • 全局變量、共享內存塊和對象: 在兩個線程之間傳遞信息的另一種簡單方法是使用全局變量,共享對象或共享內存塊。儘管共享變量既快速又簡單,但是它們比直接消息傳遞更脆弱。必須使用鎖或其他同步機制仔細保護共享變量,以確保代碼的正確性。 否則可能會導致競爭狀況,數據損壞或崩潰。
  • 條件執行: 條件是一種同步工具,可用於控制線程何時執行代碼的特定部分。您可以將條件視爲關守,讓線程僅在滿足指定條件時運行。
  • Runloop sources: 一個自定義的 Runloop source 配置可以讓一個線程上收到特定的應用程序消息。由於 Runloop source 是事件驅動的,因此在無事可做時,線程會自動進入睡眠狀態,從而提高了線程的效率。
  • Ports and sockets: 基於端口的通信是在兩個線程之間進行通信的一種更爲複雜的方法,但它也是一種非常可靠的技術。更重要的是,端口和套接字可用於與外部實體(例如其他進程和服務)進行通信。爲了提高效率,使用 Runloop source 來實現端口,因此當端口上沒有數據等待時,線程將進入睡眠狀態。
  • 消息隊列: 傳統的多處理服務定義了先進先出(FIFO)隊列抽象,用於管理傳入和傳出數據。儘管消息隊列既簡單又方便,但是它們不如其他一些通信技術高效。
  • Cocoa 分佈式對象: 分佈式對象是一種 Cocoa 技術,可提供基於端口的通信的高級實現。儘管可以將這種技術用於線程間通信,但是強烈建議不要這樣做,因爲它會產生大量開銷。分佈式對象更適合與其他進程進行通信,儘管在這些進程之間進行事務的開銷也很高

1.5 使用線程的注意點

  • 避免顯式的創建線程

手動編寫線程創建代碼很繁瑣,並且可能容易出錯,因此應儘可能避免這樣做。

OS XiOS 通過其他 API 爲併發提供隱式支持。與其自己創建一個線程,不如考慮使用異步 APIGCD 或操作對象來完成工作。這些技術可以在底層爲您完成與線程相關的工作,並且可以保證正確進行。此外,GCD 和操作對象等技術旨在根據當前系統負載調整活動線程的數量,從而比您自己的代碼更有效地管理線程。

  • 讓線程合理的執行任務

如果決定手動創建和管理線程,請記住線程會消耗寶貴的系統資源。您應該盡力確保分配給線程的所有任務都可以長期有效地工作。同時,您不必擔心終止花費大部分空閒時間的線程。線程佔用的內存非常少,其中一些是 wired memory,因此釋放空閒線程不僅有助於減少應用程序的內存佔用,還可以釋放更多的物理內存供其他系統進程使用。

PS: Mac 中的內存使用可以分爲四大類

  • Wired(聯動): 系統核心佔用的,永遠不會從系統物理內存中驅除。
  • Active(活躍): 表示這些內存數據正在使用中,或者剛被使用過。
  • Inactive(非活躍): 表示這些內存中的數據是有效的,但是最近沒有被使用。
  • Free(可用空間): 表示這些內存中的數據是無效的,即內存剩餘量。

開始終止空閒線程之前,應始終記錄一組應用程序當前性能的基準測量值。 嘗試更改後,請進行其他測量以確認更改實際上在提高性能,而不是損害性能。

  • 避免共享數據結構

避免與線程相關的資源衝突的最簡單和容易的方法是爲程序中的每個線程提供所需數據的自己的副本。當您最小化線程之間的通信和資源爭用時,並行代碼最有效。

  • 線程和用戶界面

如果你的應用有圖形化的用戶界面,強烈建議在應用程序的主線程上接收用戶相關的事件和啓動界面更新的操作。這種方法有助於避免與處理用戶事件和繪製窗口內容相關的同步問題。某些框架(例如 Cocoa)通常需要此行爲,但是即使對於那些不需要的框架,在主線程上保留此行爲也具有簡化管理用戶界面的邏輯的優勢。

當然,在某些情況下,在非主線程上執行圖形操作是有助於提高性能的。例如,您可以使用子線程來創建和處理圖像以及執行其他與圖像有關的計算操作。但是遇到不確定的圖形操作的時候,最好在主線程上執行。

  • 注意線程退出時的問題

進程一直運行到所有非分離線程都退出爲止。默認情況下,僅將應用程序的主線程創建爲非分離式。當然,你也可以創建非分離的子線程。 當用戶退出應用程序時,通常認爲立即終止所有分離的線程是適當的行爲,因爲分離的線程完成的工作被認爲是可選的。但是,如果你的應用程序正在使用後臺線程將數據保存到磁盤或執行其他關鍵工作,則可能需要將這些線程創建爲非分離線程,以防止在應用程序退出時丟失數據。

將線程創建爲非分離線程(也稱爲可連接線程)需要你進行額外的工作。因爲大多數高級線程技術默認情況下都不創建可連接線程,所以你可能必須使用 POSIX API 創建線程。此外,你必須在應用程序的主線程中添加代碼,以便在非分離線程最終退出時加入它們。

  • 處理異常

拋出異常時,異常處理機制依賴於當前的調用堆棧來執行任何必要的清除工作。因爲每個線程都有自己的調用堆棧,所以每個線程負責捕獲自己的異常。在輔助線程中未能捕獲異常與在主線程中未能捕獲異常會有相同的後果:進程終止。你不能將未捕獲的異常拋出到另一個線程進行處理。

如果你需要在當前線程中將異常情況通知另一個線程(例如主線程),則應捕獲該異常,並簡單地向該另一個線程發送一條消息,指出發生了什麼。 根據您的模型和您要執行的操作,捕獲到異常的線程可以繼續處理(如果可能的話),等待指令或直接退出。

在某些情況下,可能會自動爲你創建一個異常處理程序。 例如,Objective-C 中的@synchronized 指令包含一個隱式異常處理程序。

  • 徹底的退出線程

退出線程的最佳方法自然是讓線程到達其主入口點例程的末尾。儘管有立即終止線程的功能,但這些功能僅應作爲最後的手段使用。在線程到達其自然終點之前終止該線程會阻止該線程自身的清理。如果線程已分配內存,打開文件或獲取其他類型的資源,則您的代碼可能無法回收這些資源,從而導致內存泄漏或其他潛在問題。

  • 庫中的線程安全

儘管應用程序開發人員可以控制應用程序是否使用多線程執行,但庫開發人員不能。在開發庫時,必須假設調用應用程序是多線程的,或者可以隨時切換到多線程的。因此,你應該始終對代碼的關鍵部分使用鎖。

對於庫開發人員來說,僅當應用程序變成多線程時才創建鎖是不明智的。如果你需要在某個時候鎖定代碼,請在使用庫的早期創建 lock 對象,最好是通過某種顯式調用來初始化庫。儘管你也可以使用靜態庫初始化函數來創建此類鎖,但只有在沒有其他方法時才嘗試這樣做。執行初始化函數會增加加載庫所需的時間,並可能對性能產生不利影響。

始終記住在庫中平衡互斥鎖的加鎖和解鎖。你還應該記住對庫中的數據結構加鎖,而不是依賴調用代碼來提供線程安全的環境。

如果你正在開發 Cocoa 庫,那麼如果你希望在應用程序變爲多線程時得到通知,則可以註冊爲 NSWillBecomeMultiThreadedNotification 的觀察者。但是,你不應該依賴於接收此通知,因爲它可能在調用庫代碼之前被髮送。

二、線程管理

2.1 線程開銷

如上圖所示:

  • 內核層的數據結構:開銷爲 1KB。這部分用於存儲線程數據結構和屬性,其中大部分是作爲有線內存分配的,因此無法分頁到磁盤。
  • 棧空間:iOS 程序主線程開銷爲 1MB,macOS 程序主線程開銷爲 8MB,子線程開銷爲 512KB。
  • 創建時間:大約 90ms,此值反映創建線程的初始調用與線程的入口點例程開始執行之間的時間。這些數字是通過分析在基於英特爾的 iMac 上創建線程時生成的平均值和中值來確定的,iMac 具有一個 2GHz 的雙核處理器和一個運行 OSX V10.5 的 1GB RAM。

由於其底層內核支持,Operation 對象通常可以更快地創建線程。它們不是每次都從頭開始創建線程,而是使用已經駐留在內核中的線程池來節省分配時間。

2.2 創建線程

線程創建出來後必須要執行任務纔有意義,下面介紹幾種創建線程的方式。

2.2.1 使用 NSThread 創建線程

通過 NSThread 創建線程有兩種方式:

  • 使用 detachNewThreadSelector:toTarget:withObject: 類方法來派生新的線程。
  • 創建一個 NSThread 實例對象,然後調用 start 方法。

這兩種技術都會在應用程序中創建分離式線程。分離的線程意味着當線程退出時,系統會自動回收該線程的資源。這也意味着您的代碼以後不必顯式地與線程聯接。

下面給出兩種方式的實際用法:

[NSThread detachNewThreadSelector:@selector(myThreadMainMethod:) toTarget:self withObject:nil];

NSThread* myThread = [[NSThread alloc] initWithTarget:self
                                        selector:@selector(myThreadMainMethod:)
                                        object:nil];
[myThread start];  // Actually create the thread

這裏有一個注意點,採用實例化 NSThread 對象的方式,只有調用 start 方法後,在底層線程纔會被創建出來。

使用 initWithTarget:selector:object:method 的另一種方法是將 NSThread 子類化並重寫其 main 方法。你可以使用此方法的重寫版本來作爲線程的主入口點。

如果你有一個已經創建好並且在運行中的 NSThread 線程對象,你可以通過 performSelector:onThread:withObject:waitUntilDone: 方法來向這個線程發送消息。這個方法是 NSObject 的分類中的方法,也就意味着幾乎任何對象都能適用。使用這個方法發送的消息將由另一個線程直接執行,作爲其正常運行循環處理的一部分。當然,這意味着目標線程必須在其運行循環中運行)。在這過程中你可能還需要適用鎖來進行線程同步,當然,這比適用基於 port 的線程間同信要簡單一些。

雖然 performSelector:onThread:withObject:waitUntilDone: 方法用起來很簡單,但是對於頻繁間的線程通信或時間敏感類的任務執行,請不要使用這個方法。

2.2.2 使用 POSIX 創建線程

OS X 和 iOS 爲使用 POSIX 線程 API 創建線程提供了基於 C 的支持。這種技術實際上可以用於任何類型的應用程序(包括 Cocoa 和 Cocoa Touch 應用程序),如果您爲多個平臺編寫軟件,則可能更方便。用於創建線程的 POSIX 例程被適當地調用爲 pthread_create

下面是對於 pthread_create 的簡單使用示例:

#include <assert.h>
#include <pthread.h>
 
// 線程要執行的任務 
void* PosixThreadMainRoutine(void* data)
{
    // Do some work here.
 
    return NULL;
}
 
// 啓動線程 
void LaunchThread()
{
    // 線程屬性
    pthread_attr_t  attr;
    // 線程對象
    pthread_t       posixThreadID;
    // 返回值
    int             returnVal;
 
    // 初始化線程屬性
    returnVal = pthread_attr_init(&attr);
    assert(!returnVal);
    // 設置線程爲 detach 狀態
    returnVal = pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
    assert(!returnVal);
 
    // 創建線程,傳入屬性和要執行的任務
    int     threadError = pthread_create(&posixThreadID, &attr, &PosixThreadMainRoutine, NULL);
    
    // 銷燬屬性
    returnVal = pthread_attr_destroy(&attr);
    assert(!returnVal);
    if (threadError != 0)
    {
         // Report an error.
    }
}

關於 pthread 詳細的使用請參考這篇文章 iOS 多線程技術實踐之 pthreads(一)

2.2.3 使用 NSObject 派生線程

所有繼承於 NSObject 的對象都可以通過 performSelectorInBackground:withObject: 來在子線程上執行任務。

這個方法底層實際上是調用的 NSThread 類方法 detachNewThreadSelector:toTarget:withObject: 來創建線程並執行任務。

2.2.4 在 Cocoa 應用程序中使用 POSIX 線程

雖然在 Cocoa 中使用 NSThread 是創建線程的主要方式,但是當需要的時候,還是可以使用 POSIX 線程的。但是需要遵守以下幾點:

  • Protecting the Cocoa Frameworks: 保護 Cocoa 中的框架

    • 對於多線程應用程序,Cocoa 框架使用鎖和其他形式的內部同步來確保它們的行爲正確。但是,爲了防止這些鎖在單線程情況下降低性能,在應用程序使用 NSThread 類生成其第一個新線程之前,Cocoa 不會創建這些同步的元素。如果只使用 POSIX 線程例程生成線程,Cocoa 將不會收到需要知道應用程序現在是多線程的通知。當這種情況發生時,涉及 Cocoa 框架的操作可能會破壞應用程序的穩定性或崩潰。
    • 要讓 Cocoa 知道你打算使用個線程,你只需使用 NSThread 類生成一個線程,並讓該線程立即退出。你的線程入口點不需要做任何事情。使用 NSThread 生成一個線程的行爲就足以確保 Cocoa 框架所需的鎖被放置到位。
    • 如果不確定 Cocoa 是否認爲你的應用程序是多線程的,則可以使用 NSThreadisMultiThreaded 方法進行檢查。
  • 混合使用 Cocoa 中的鎖與 POSIX

    • 在同一個應用程序中混合使用 POSIXCocoa 鎖是安全的。Cocoa 鎖和條件對象本質上只是 POSIX 互斥鎖和條件對象的包裝。但是,對於給定的鎖,必須始終使用相同的接口來創建和操作該鎖。換句話說,不能使用 CocoaNSLock 對象來操作使用 pthread_mutex_init 函數創建的互斥鎖對象,反之亦然。

2.3 配置線程屬性

在線程創建之前或者之後,你可能想要配置一些線程相關的屬性,具體內容如下:

2.3.1 配置線程所佔棧空間大小

對於你創建的每個新線程,系統都會在進程空間中分配特定數量的內存,以充當該線程的堆棧。

堆棧管理堆棧幀,線程內部聲明的局部變量就會存於此處。

要設置線程所佔用的棧空間大小,只能在線程創建之前指定。

  • Cocoa

通過實例化 NSThread 對象,然後在調用 start 方法之前調用 setStackSize: 來設置棧空間大小。

  • POSIX

創建 pthread_attr_t 結構體對象,然後將其傳入 pthread_attr_setstacksize 方法來設置棧大小,然後將 pthread_attr_t 傳入 pthread_create 函數來創建 POSIX 線程。

2.3.2 配置 TLS

每個線程會維護一個鍵值對的字典,用來在線程執行過程中存儲一些內容,這個字典

CocoaPOSIX 以不同的方式存儲線程字典,因此你不能混合和匹配對這兩種技術的調用。 但是,只要您在線程代碼中堅持使用一種技術,最終結果應該是相似的。在 Cocoa 中,你可以使用 NSThread 對象的 threadDictionary 方法來獲取 TLS,你可以在該對象中添加線程所需的任何鍵。在 POSIX 中,使用 pthread_setspecificpthread_getspecific 函數來設置和獲取 TLS

2.3.3 設置線程的分離狀態

通過 NSThread 創建的線程默認是分離式的,當應用程序退出時,這種類型的線程也將退出,所以爲了執行諸如在應用程序退出時保存數據的任務,需要創建 joinable 類型的線程。而目前的方案只有通過 POSIX 來實現,通過 pthread_attr_setdetachstate 方法來設置 pthread_t 的屬性來達到非分離式的效果。當然,如果想改變線程的狀態,可以通過 pthread_attr_setdetachstate 來將線程設置爲分離式的。

2.3.4 設置線程優先級

你所創建的線程都有與之關聯默認的優先級,內核在調度線程的時候,優先級高的線程相比於優先級低的線程更有可能性執行。但是優先級高的線程並不能保證有固定的運行時間,只是更可能被調度而已。

It is generally a good idea to leave the priorities of your threads at their default values. Increasing the priorities of some threads also increases the likelihood of starvation among lower-priority threads. If your application contains high-priority and low-priority threads that must interact with each other, the starvation of lower-priority threads may block other threads and create performance bottlenecks

【譯】 通常最好將線程的優先級保留爲默認值。增加某些線程的優先級也增加了低優先級線程之間出現飢餓的可能性。如果你的應用程序包含必須相互交互的高優先級和低優先級線程,則低優先級線程的飢餓可能會阻塞其他線程並造成性能瓶頸。

這裏可以聯想到已經不再安全的自旋鎖 OSSpinLock,具體內容參見 YYKit 作者的博文 不再安全的 OSSpinLock

如果你確實想修改線程優先級,對於 Cocoa 線程,可以使用 NSThreadsetThreadPriority 類方法設置當前正在運行的線程的優先級。對於 POSIX 線程,使用 pthread_setschedparam 函數。

2.4 編寫線程入口方法

在大多數情況下,OS X 中線程的入口點例程的結構與其他平臺上的相同。你可以初始化數據結構,進行一些工作或有選擇地設置運行循環,並在線程代碼完成後進行清理。根據你的設計,編寫輸入例程時可能需要採取一些其他步驟。

  • 創建一個自動釋放池

Objective-C 框架中鏈接的應用程序通常必須在其每個線程中至少創建一個自動釋放池。如果應用程序使用 ARC,則自動釋放池將捕獲從該線程自動釋放的所有對象。

下面的代碼演示了在 MRC 下需要在線程入口方法裏面創建一個自動釋放池

- (void)myThreadMainRoutine
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Top-level pool
 
    // Do thread work here.
 
    [pool release];  // Release the objects in the pool.
}

因爲頂層的自動釋放池直到線程退出之後纔會釋放對象,所以常駐線程需要創建一些額外的自動釋放池來達到經常性的清理效果。例如,使用運行循環的線程可能每次通過該運行循環都會創建並釋放一個自動釋放池。頻繁釋放對象可以防止應用程序的內存佔用過大,從而導致性能問題。 但是,與任何與性能相關的行爲一樣,你應該衡量代碼的實際性能,並適當調整自動釋放池的使用。

  • 設置異常處理

如果在你的應用程序裏面捕獲並拋出了異常,那麼在線程的入口方法中也需要做相應的處理,如果有拋出的異常在線程內部沒有被捕獲到將會導致程序的退出。你可以使用 C++OC 風格的 final-try&catch 代碼塊來處理。

  • 設置 RunLoop

在子線程上執行任務的時候,有兩種選擇,一種是執行完之後線程會自動退出,一種是希望線程可以一直存活來處理任務。第一種方式不需要額外的操作,而第二種方式則需要 runloop 的配合。而 iOSmacOS 中的每個線程都有對應的 runloop 對象,app 的主線程啓動之後,其對應的主運行循環也會自動開啓,但是對於子線程來說,則需要手動開啓。

2.5 終止線程

退出線程的建議方法是讓其正常退出其入口方法。儘管 CocoaPOSIXMultiprocessing Services 提供了直接殺死線程的例程,但是強烈建議不要使用此類例程。直接殺死線程會防止它自己清理掉從而導致線程分配的內存可能會泄漏,線程當前使用的任何其他資源可能無法正確清理,從而在以後產生潛在問題。

如果你預計需要在操作過程中終止線程,則應從一開始就設計線程以響應取消或退出消息。對於長時間運行的操作,這可能意味着要定期停止工作並檢查是否收到此消息。如果確實有消息要求線程退出,則該線程將有機會執行所需的清理並正常退出;否則,它可以簡單地返回工作並處理下一個數據塊。

下面是通過 runloop 以及 threadDictionary 來實現定時檢查是否要退出線程的代碼:

- (void)threadMainRoutine
{
    // 是否還有更多工作要做
    BOOL moreWorkToDo = YES;
    // 是否要退出線程
    BOOL exitNow = NO;
    // 獲取當前線程對應的 runloop 對象
    NSRunLoop* runLoop = [NSRunLoop currentRunLoop];
 
    // 將 exitNow 存入 threadDictionary 中
    NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary];
    [threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"];
 
    // 設置 runloop 的輸入源
    [self myInstallCustomInputSource];
 
    while (moreWorkToDo && !exitNow)
    {
        // 具體要做的工作
        // 完成時改變 moreWorkToDo 的值
 
        // 讓 runloop 跑起來,但如果沒有要觸發的輸入源,則立即超時。
        [runLoop runUntilDate:[NSDate date]];
 
        // 檢查輸入源處理程序是否更改了 exitNow 值
        exitNow = [[threadDict valueForKey:@"ThreadShouldExitNow"] boolValue];
    }
}

三、總結

參考資料

Threading Programming Guide - Apple

Detached vs. Joinable POSIX threads - StackOverflow

c++11中thread join和detach的區別

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章