1. 程式人生 > >iOS 多執行緒使用總結(很實用)

iOS 多執行緒使用總結(很實用)

每次準備開始新的航行,總是要複習一遍演算法啊,多執行緒啊,記憶體管理啊之類的理論和應用知識,這次把他們整理成文件,方便以後的學習和不斷的積累進步。
        多執行緒給我留下的是痛苦的記憶,當時在上家創業公司的最後階段,就是被Feature Phone上面的多執行緒方案導致bug叢生,搞的焦頭爛額。

        (一). 多執行緒的應用
        由於一直做手機應用程式的開發,所以我接觸到的多執行緒方案很少,總結下來只用來解決2類問題:
        問題1. 同步問題
        問題2. 耗時計算問題
        所謂的同步問題是指,比如在多媒體實時系統中,解碼模組與播放模組之間的關係,解碼模組解碼完畢,播放模組就需要進行繪製或者播放聲音。一種方案是非同步方案,解碼完畢通知播放模組播放,或者輪詢方案,解碼後將資料放入緩衝區,播放模組輪詢(用timer機制),這些方案的實時性都無法保證,因此如果對實時性要求較高的應用,用執行緒同步機制是最好的方案:解碼器解碼,解鎖,播放器播放,繼續等待資料。這就是多執行緒模型中的最基本模型:生產者消費者模型。[詳情見2.1]
        而耗時的計算問題則比較普遍,尤其是手機上的圖片應用,比如圖片瀏覽的應用,當用戶快速拖動scroll view的時候,如果載入圖片檔案,解碼,繪製整個過程的時間超過16ms(60fps的要求),就會有卡頓,不夠流暢,這時候就需要用後臺執行緒做這些載入檔案,解碼等費時的操作。

        (二). iOS系統提供的多執行緒方案
        iOS系統提供了n多的執行緒方案,從最底層POSIX庫支援,到NSThread,再到Operation Queue,最後到最方便的GCD。
        1. iOS支援POSIX執行緒庫,以基本的生產者消費者模型為例(見[參考程式碼:
https://github.com/zteshadow/threadStudy
]裡面的POSIX工程,所有的程式碼都是iOS版本的實現,在XCode5.0上面編譯),實現這個模型要注意需要3個lock,一個是用來保護對緩衝區的讀寫操作的,一個是當緩衝區已滿的時候鎖住生產者,一個是當緩衝區為空的時候鎖住消費者。注意iOS不支援匿名鎖,所以需要使用sem_open來建立鎖。POSIX執行緒pthread1995年釋出,60個函式.
        建立:pthread_create
        終止:
            自己返回:隱式終止
            自己呼叫pthread_exit:顯示終止
            被別人呼叫pthread_cancel:被終止
        回收執行緒資源:
            pthread_join:阻塞等待執行緒退出,回收資源:棧,控制結構等
        分離和結合:
            預設建立的執行緒是可結合的,由建立者回收資源,可以呼叫pthread_detach來分離執行緒,讓系統回收資源。
        2. NSThread只是比pthread高一層的objective-c抽象,用起來比較複雜,可能還需要維護一個runloop,runloop的源等等,如果為了解決問題2,是完全沒有必要使用NSThread的。
        NSThread alloc ,start:可以先建立,再配置,再執行
        NSThread detachNewThreadSelector:立即開始執行

        3. Operation Queue
        示例程式碼演示了一個場景:將一個耗時操作(耗時5秒鐘)傳送給主執行緒之外的執行緒來執行(見[參考程式碼:
https://github.com/zteshadow/threadStudy
]裡面的NSOperationQueue工程),該工程裡在controller的建構函式裡面建立了operation queue,在按鈕的響應函式裡面建立operation,然後傳送給queue去處理,如果設定queue的maxConcurrentOperationCount大於1,則會由系統執行緒池中的多個執行緒從queue裡面取出operation併發執行,否則順序執行,Operation Queue是iOS2引入的。
        4. GCD
        解決上面3同樣的問題,可以使用GCD,見[參考程式碼:
https://github.com/zteshadow/threadStudy
]裡面的GCD工程。可以看到只要把建立NSInvocationOperation,再add到queue換成dispatch_async就可以了,要將現有的基於Operation Queue的程式碼改成GCD程式碼,非常簡單——不過似乎也沒有什麼理由這麼做。如果不是遷移Operation Queue的程式碼,而是新建的程式碼,那麼基於GCD要比Operation Queue簡單。dispatch_get_global_queue是個併發執行的queue,如果要順序執行,需要自己用dispatch_queue_create建立queue(iOS4引入的2010年)。

GCD中的一些知識點:
        dispatch_barrier_async工作機制是怎樣的?如何實現?
        queue中的其他block已經開始在各自分配的執行緒執行,當從queue檢出一個barrier時,是等待其他的block都執行完畢再執行barrier,此時不會併發執行其他的block,直到該barrier執行完畢。 這相當於給當前已經執行的bock們加了一個group和notify,在notify裡面執行了barrier,然後再執行barrier下面的block——實際上GCD的實現中的確如此。
        dispatch_queue_create 預設是serial的queue,可以用引數:DISPATCH_QUEUE_CONCURRENT,建立併發queue,序列queue——每個對應會建立一個thread,如果持續的增加序列queue,比如說10000個序列queue,那麼系統就要生成10000個執行緒來進行排程,這會嚴重影響系統性能,甚至導致崩潰,而併發queue,隨著dispatch的block的增加,系統會根據當前的core的數量以及當前CPU的usage等綜合資源,選擇建立新的thread或者是用現有的thread進行排程。

        (三).執行緒間的通訊
        1. 上面的3和4中,主執行緒將task傳送給系統中的輔助執行緒執行,當執行完畢如果需要通知主執行緒的,可以使用:performSelectorOnMainThread,或者使用GCD中的dispatch_async讓block在dispatch_get_main_queue上執行。
        2. 如果自己建立了NSThread,那麼可能需要講一下runloop。顧名思義,runloop就是一個迴圈,如果沒有runloop,那麼我們建立的執行緒一般需要自己做一個迴圈來等待事件的發生,類似於這樣:
        while (wait_for_event)
        {
            do_some_thing
        }
        在沒有事件的時候阻塞,有事件處理事件。iOS為我們提供了一個runloop代替這個自己做的迴圈,我們可以呼叫runloop的run進入迴圈。就像我們自己建立迴圈一樣,runloop也需要一個等待的事件,如果沒有等待事件(runloop的源),那麼runloop就會直接返回,然後執行緒結束了。runloop的源可以是個timer源,也可以是事件源,我們可以自己定義源,也可以使用message port作為輸入源。NSPort有3種類型:NSMachPort,NSMessagePort和NSSocketPort,文件中不建議使用NSMessagePort,而且好像在iOS7中cocoa去掉了對NSPortMessage的支援,原因不詳。這樣就不能用cocoa中的message port了,可以使用core foundation中的message port,不過稍微麻煩一些見[參考程式碼:https://github.com/zteshadow/threadStudy]中的CFMessagePortRef工程。注意外部源用signal來發出訊號,這個和message port稍有不同,signal只有0和1的狀態,而message port有個訊息佇列可以儲存多個訊息,而signal只能發生一次。例如,我們用自定義的源通知輔助執行緒進行耗時計算,signal之後,執行緒執行耗時函式,此時如果主執行緒再發送一個signal進行第二個操作,那麼當上一個耗時操作結束後,輔助執行緒不會進行第二個操作,原因是輔助執行緒得到signal之後進行耗時操作,結束後將signal清除,而沒有訊息佇列的概念,如果需要可以自己實現。而message port則有訊息佇列,能處理多個請求。runloop一般我們顯式用到的地方就2個:NSTimer和URLRequest. runloop是實現performOnThread的基礎——目標thread必須有runloop在執行,因為該實現的原理是基於runloop源的
        3. 同步的代價
        posix_mutext
        NSLock:lock, unlock
        @synchronized
        在iPhone4S上面進行100次的NSMutableArray的插入和取出測試取均值得到如下結果:
        @synchronized為11ms,NSLock 8~9ms,
        用GCD,也就是插入用barrier_async,get用sync,其實是和lock差不多的,大概比synchronized節省40%

        (四).快速拖動圖片的解決方案
        快速拖動的執行緒方案需要和cache配合使用,一個cell需要獲取圖片的時候,先檢查cache,如果命中則使用cache的圖片,否則使用預設圖片,然後線上程中載入圖片,進行解碼,然後在main queue中設定圖片。【注意】這個方案如果不注意會有個bug,由於cell是複用的,而dispatch是併發執行的,那麼可能會有cell的image順序錯亂的問題。有2個方案可以解決:1是每次cell獲取新的圖片時,將舊有的操作cancel掉——雖然block執行起來就無法停止,但是我們還是可以在block的執行程式碼中做判斷控制是否要進行圖片的切換操作,2是用順序佇列執行dispatch。

        (五). Timer
        NSTimer是基於runloop的,建立之後,只有
[NSRunLoop currentRunloop] addTimer:timer1 forMode:NSDefaultRunLoopMode],才能開始工作,對於那些沒有runloop的使用者執行緒,就無法使用這個timer了,這時可以用基於dispatch queue的
dispatch_source_set_timer。

        (六). 總結
        解決問題1,需要使用執行緒,個人沒有用過,但感覺使用POSIX執行緒更清晰明瞭。
        解決問題2,使用Operation Queue和GCD就夠了,Operation Queue是iOS2.0加入的,而GCD是iOS4.0加的新特性,基於block在引數傳遞函式封裝等方面比Operation更靈活,基本可以替換掉Operation Queue——不過,如果需要支援操作的cancel以及各個操作之間的依賴關係,那麼只能使用Operation Queue——這個沒用過。