ios 多線程開發(一)簡介


簡介

線程是在一個程序中並發的執行代碼的方法之一。雖然有一些新的技術(operations, GCD)提供了更先進高效的並發實現,OS X和iOS同時也提供了創建和維護線程的接口。

這里將要介紹線程相關的包以及如何使用他們。同時也會介紹程序中多線程代碼的同步。

 

關於線程開發

多年以來,電腦的最大處理速度受制於單個處理器的處理速度。當單核處理器開始達到他們的上線時,芯片市場轉向了多核的設計,這樣電腦就可以同時處理多個任務了。OS X在處理系統任務時使用了這些優勢,你自己的程序也可以使用這些優勢。

 

什么是線程

線程是在程序中實現並發的相對輕量級的方式。在系統級別,程序都在運行,系統根據每個程序的需要來分配執行時間。在每個程序中,可以有一個或多個線程來執行,他們可以同時處理不同的任務。實際上是由系統來管理這些線程的計划,執行,中斷等。

在技術的角度上來說,線程是執行代碼所需的內核級別和程序級別的數據結構的組合。內核級的數據結構用來給線程分發事件以及把線程放到可用的cpu上。程序級的數據機構用來處理調用隊列以及線程要用到的屬性和狀態。

在非並發的程序中,只有一個線程在執行。這個線程的開始和結束伴隨着程序的main方法,在這期間一個接一個的執行實現的方法。相比之下,支持並發的程序由一個線程開始,然后在需要的時候創建新的線程。每個新線程有他們自己的開始並且獨立於主線程之外運行。程序中的多線程有兩個明顯的優勢:

  • 多線程可以優化程序的相應時間
  • 多線程可以優化程序在多核處理器上的性能。

如果你的程序只有一個線程,那么這個線程需要做所有的事。它要相應事件,更新窗口以及程序中實現的所有行為。單線程的問題是一次只能做一件事。所以如果有一件事要很長時間才能處理完呢?當代碼在忙着計算需要的結果時,程序就停止了相應用戶事件以及更新窗口。如果這個行為持續太長時間,用戶可能會強制退出程序。如果把自定義的計算移到其他線程,這樣程序的主線程就能自由的相應用戶事件了。

隨着多核處理器的流行,線程提供了一種優化性能的方式。線程可以同時在不同的核上處理不同任務,這樣可以在給定的時間里做更多的事情。

不過,線程也不是優化性能的靈丹妙葯。隨着線程的好處帶來的也有一些潛在問題。多線程會讓你的代碼變的復雜。每個線程都需要核其他線程合作來避免發生沖突。因為同一個程序中的線程分享同一片內存,他們可以訪問相同的數據結構。如果兩個線程同時在維護一個數據,一個線程可能會覆蓋了另一個的數據這樣可能會破壞數據結構。雖然有合適的保護機制,你也需要注意編譯選項可能會給你帶來微小(有可能不是那么微小)的bug

 

多線程的相似方案

自己創建線程的一個問題是增加了代碼的不確定性。線程在程序中是一種相對底層核復雜的支持並發的方式。如果不完全理解,可能會碰到同步和時間的問題,可能會導致程序行為和預期的不一致,或者crash,或者破壞了用戶的數據。

另外一個要考慮的問題是是否真的需要多線程和並發。有時候可能有很多事情要做但是也不一定需要多線程。多線程會加大進程的內存和CPU開銷。可能會發現計划的任務開銷太大或者有更好的其他選擇來實現。

下面列出了一些多線程的相似方案。不僅有多線程多替換方案(比如operation和GCD)也有單個線程一些很有效的相似方案。

技術 描述

Operation

 Operation對象會封裝一個在另一個線程上執行的任務。這個封裝影藏了執行任務時的線程管理,讓你可以專注於處理自己的事情。通常會和operation queue一起使用,它實際上會管理operation在一個或多個線程上執行。

Grand Central Dispatch(GCD)

GCD也是讓你專注於自己事情的另一個替代方案。使用GCD的話,你只需要定義你需要執行的任務然后加到工作隊列中,它就會自動在合適的線程上計划你的任務了。相對於自己使用多線程,工作隊列會查看CPU的內核數以及負載來更效率的執行任務。

閑時消息

對於一些優先級相對較低的任務,閑時消息可以讓程序在不忙的時候來執行。Cocoa使用NSNotificationQueue來支持閑時消息。如果需要閑時消息,向NSNotificationQueue對象發送一個帶NSPostWhenIdle選項的消息。隊列在空閑的時候會分發消息。

異步方法

系統提供了很多異步的方法來自動實現並發。這些API使用系統進程或者自定義線程來執行任務(實際的執行是由系統控制的)。當設計程序的時候,優先查看是否提供了這些異步方法。

定時器

可以使用定時器來處理一些瑣碎的小事情,但是需要定期檢查。

 

支持線程

OS X和iOS都有幾種技術來支持多線程。另外他們都提供管理和同步線程的方法。下面會介紹一些使用多線程必須要知道的一些東西。

多線程技術

技術 描述

Cocoa threads

 Cocoa使用NSThread實現多線程。Cocoa在NSObject中也提供了產生新線程和在已有線程上執行代碼的方法。

POSIX threads

POSIX threads提供了一個基於C的多線程接口。如果你不是在寫Cocoa程序,這個是實現多線程的最好選擇。POSIX接口使用起來相對容易和靈活。

在應用級別看, 所有的線程在本質上是一樣的。在線程開始之后,線程主要在三種狀態:運行,准備,阻塞。如果線程沒有運行,要么是阻塞了等待輸入,或者是已經准備運行但是還沒被調度。線程不斷在這幾種狀態間切換知道他們運行完。

創建一個新線程時,必須要制定入口方法。入口方法種包含你要在線程種運行的程序。當方法返回,或者你明確的終止了,這個線程就永久的停止了並且系統會回收它。因為多線程相對是很消耗內存和CPU的,所以盡量讓入口方法處理的事情很明確或者設置一個run loop來處理周期性的任務。

 

Run Loops

run loop是線程異步接受消息的基礎設施。run loop會檢測線程的一個或多個事件。當事件觸發后,系統會喚醒線程然后把事件分發給run loop,然后它會把事件分發給你指定的處理程序。如果沒有事件要處理,run loop會讓線程掛起。

並不需要為每個線程創建一個run loop,但是創建一個可以有更好的用戶體驗。Run loop可以讓線程使用最少的資源長期存活着。因為沒事做的時候run loop讓線程掛起,這樣省去了輪詢的過程。輪詢很浪費CPU並且會阻止CPU省電和休眠。

要配置run loop只需要在線程開始時指定一個run loop對象,設置事件處理程序,然后讓run loop運行就可以了。系統會自動為主線程創建一個run loop。如果你想創建另外一個長期存在的線程,可以自己為他們配置一個run loop。

 

同步工具

多線程最大的壞處之一就是爭奪資源。如果多個線程同時使用或修改一個資源,問題就出現了。緩解這個問題的一種方式是避免使用共同的資源,讓每個線程都有他們獨立的資源。完全保持獨立並不是很好的選擇,你也可以使用鎖,conditions,原子操作等技術。

鎖提供了一種強有力的方法來保證一個資源只被一個線程訪問。最經常用到的鎖叫做互斥鎖。當一個線程想要訪問被另一個線程鎖住的資源時,它要一直等到另一個線程釋放鎖。很多系統框架提供了互斥鎖,雖然他們底層的技術都是一樣的。另外Cocoa提供了一些互斥鎖的擴展來支持不同類型的行為,比如遞歸。

除了鎖以外,系統還提供conditions,它來保證程序種的任務安正確的順序執行。conditions就像是看門人,它會阻塞線程直到它達到運行的條件。當達到條件后,condition會讓線程繼續運行。POSIX和Foundation框架都提供了conditions。(如果是使用operation,你可以配置operation對象執行任務的依賴關系,和conditions的行為很相似)

雖然鎖和conditions是並發中很常見的設計,原子操作是保護和同步訪問資源的另一種方式。當進行數學運算或表量數據的邏輯運算時,原子操作是替代鎖的一個輕量級的方法。原子操作使用特殊的硬件指令來保證修改變量完成后其他線程才能訪問。

 

線程間通訊

好的設計會盡量減少通訊,但是,線程間通訊也是必要的。線程可能需要發起新任務請求或者把計算的結果報告給主線程。這些情況下,就需要有方法來支持線程間通訊了。很幸運,同一個進程下的線程有很多方式可以通訊。

線程通訊有很多方式,每個都有好處和不足。下面列出了OS X上的大部分的通訊機制。(除了message queues和Cocoa distributed object,其他的在iOS上都可以用)。下面的技術是按照復雜度遞增排序的。

通訊機制

機制 描述

直接發消息

Cocoa程序支持直接在另一個線程上執行selector。這就意味着一個線程可以直接在另一個線程上執行方法。由於是在另一個線程上執行,這種方式發送的消息會直接在目標線程上序列化

全局變量,共享內存或對象

另一個線程間通訊的方式是使用全局變量,共享對象或內存。雖然共享變量很快也很簡單,但是它比直接發消息更脆弱。在並發程序中全局變量必須要用鎖或者其他同步機制小心的保護。沒保護好可能會導致爭奪資源,數據損壞甚至crash。

Conditions

Conditions是一個同步工具,它可以控制一個線程什么時候執行一段代碼。可以把Conditions看作是一個看門人,只有當狀態滿足時才會讓線程運行。

Run loop資源

自定義的run loop是你在線程上設置來接收指定的消息的。由於它是事件驅動的,run loop會在沒事情做時自動把線程掛起,這樣可以提高線程的效率

Ports 和 sockets

線程間基於port的通訊是一個更復雜的方式,同時也是一個很可靠的技術。更重要的是,ports和sockets可以和外部實體通訊,比如其他進程和服務。對於效率方面,port的實現是基於run loop資源,所以當沒有數據的時候線程會掛起。

消息隊列

基於legacy的多線程服務在管理數據方面定義了一個先進先出的隊列。雖然消息隊列和簡單方便,但是相對於其他技術它並不是那么高效。

Cocoa distributed object

Dirtributed objects是基於port通訊的上層實現。雖然它可以用來做線程間通訊,但是會有很大的開銷。它更適用於進程間通訊,因為進程間通訊的開銷本來就很大。

 

設計的小提示

為了確保並發代碼的正確性下面說一些提示,有一些對於提升性能也會有幫助。對於任何更新,最好寫代碼的前,中,后都要對比一下代碼的性能。

 

避免手動創建線程

手動創建線程比較繁瑣也容易出錯,應該盡量避免。OS X和iOS提供了隱式支持並發的API。相對於自己創建線程,更推薦異步API,GCD以及operation 對象。這些技術在幕后做線程相關的工作並且可以保證他們正確的工作。另外,相對於自己創建線程,GCD和operation對象可以根據系統負載更高效的管理線程。

 

讓線程合理的繁忙

如果需要自己創建管理線程,要記住線程會消耗寶貴的系統資源。要確保線程分配任務所消耗的時間和產出是合理的。同時,應該終止大部分時間不做事的線程。線程會占用內存,釋放它不僅可以增加當前程序的可使用內存,也能讓其他程序有更過的內存可以使用

注意:在釋放空閑線程之前,最好記錄一下程序的當前性能。在更改之后,在記錄一些性能看看是否有優化。

 

避免共享數據

最簡單的避免線程資源沖突的方法是給每個線程一份他們需要的資源。最小化線程間通訊和爭奪資源能讓他們更好的工作。

就算很注意共享資源的加鎖,可能還是會有其他問題。比如一些數據修改的時候需要按照特殊的順序,這時候可以用基於transaction的代碼來解決這個問題。在設計多線程代碼時,避免競爭資源是首先要考慮的事情。

 

線程和用戶界面

如果程序有圖形用戶界面,推薦在主線程中接收用戶事件和更新界面。這樣可以避免處理事件和繪制窗口的線程同步問題。有一些框架,比如Cocoa必須是這樣,對於一些不強制這樣的框架,建議這樣做和讓邏輯變簡單。

也有一些例外可以使用另一個線程來處理圖形操作。比如,使用另一個線程來處理圖片計算。這樣可以優化性能。

 

清楚線程退出的行為

進程在所有非獨立線程退出后退出。默認情況下只有程序的主線程是非獨立的,但是也可以自己創建非獨立線程。當用戶退出程序時,系統會立即結束其他獨立線程,因為獨立線程做的事情被認為是可有可無的。如果在用后台線程存儲數據到硬盤或其他重要工作,盡量使用非獨立線程來防止程序退出時數據丟失。

創建非獨立線程需要做一些額外的工作。因為上層線程技術默認不創建非獨立線程,所以你需要使用POSIX API來創建。另外,需要在主線程中添加非獨立線程結束的代碼。

如果是Cocoa程序,可以使用applicationShouldTerminate:回調方法來讓程序延時退出。使用延時結束時,重要的線程完成他們的任務后應該調用replyToApplicationShouldTerminate:方法。

 

處理異常

異常處理機制會在異常拋出時根據當前調用堆棧來做必要的清理。因為每個線程有它自己的調用堆棧,所以每個線程負責捕捉它自己的異常。其他線程的異常捕捉失敗和主線程捕捉失敗是一樣的,會導致它所屬的進程被終止。不能把異常拋給另一個線程。

如果需要提示其他線程(比如主線程)當前線程的異常狀態,應該捕捉這個異常然后只是把消息發給另一個線程來告訴它發生了什么。取決於你的設計和你具體想做什么,出現異常的線程可以繼續執行(如果還能執行的話),等待指示,或直接退出。

注意:在Cocoa中,NSException對象會自己維護自己,所以在捕捉到后可以在線程間傳遞。

有些情況下,會自動捕捉異常,例如@synchronized在objective-c中隱式的異常處理。

 

線程結束后的清理

最好的線程退出方式是自然退出,也就是運行到它主入口的結束。雖然有方法讓線程立即終止,不過這些方法是最后的辦法。在線程自然結束前終止它會讓它不能清理自己的現場。如果它有申請內存,打開文件或者占用其他資源,其他代碼可能就不能在訪問這些資源,或者內存泄漏,或者導致其他潛在的問題。

 

庫中的線程安全

雖然程序開發這自己會控制程序是否用多線程執行,庫的開發者不能控制。開發庫的時候,你應該假設使用者會用多線程或者隨時會切換到多線程。因此,關鍵代碼部分一定要用鎖。

對於庫開發者,使用多線程時才用鎖是不明智的。只要有可能會用鎖,在庫中要盡早的使用鎖,最好在初始化庫的時候顯式的調用。雖然也可以用庫的靜態初始化方法創建鎖,盡量在沒有其他方式的時候才用。初始化也會花事件的,而且會影響性能。

注意:庫中互斥鎖的鎖和解鎖一定要配對。一定要記得加鎖,而不是期望使用者在線程安全環境中使用。

如果是開發Cocoa庫,你可以注冊NSWillBecomeMultiThreadedNotification來知道程序變為多線程的了。但是不能依賴於這個消息,有可能在庫代碼被調用之前就已經分發了。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com