天天看點

iOS 開發 多線程詳解 NSThread NSOperation GCD 線程同步 總結

常用的多線程開發有三種方式: 1.NSThread 2.NSOperation 3.GCD

線程狀态分為isExecuting(正在執行)、isFinished(已經完成)、isCancellled(已經取消)三種。其中取消狀态程式可以幹預設定,隻要調用線程的cancel方法即可。但是需要注意在主線程中僅僅能設定線程狀态,并不能真正停止目前線程,如果要終止線程必須線上程中調用exist方法,這是一個靜态方法,調用該方法可以退出目前線程。

NSThread

NSThread是輕量級的多線程開發,使用起來也并不複雜,但是使用NSThread需要自己管理線程生命周期。

NSThread有兩種方法建立線程: 1、使用類方法: + (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument 直接将操作添加到線程中并啟動。

2、使用對象方法 - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 建立一個線程對象,然後調用start方法啟動線程。

通過NSThread的currentThread可以取得目前操作的線程,其中會記錄線程名稱name和編号number,需要注意主線程編号永遠為1。多個線程雖然按順序啟動,但是實際執行未必按照順序加載照片(loadImage:方法未必依次建立,可以通過在 loadImage: 中列印索引檢視),因為線程啟動後僅僅處于就緒狀态,實際是否執行要由CPU根據目前狀态排程。

為了簡化多線程開發過程,蘋果官方對NSObject進行分類擴充(本質還是建立NSThread),對于簡單的多線程操作可以直接使用這些擴充方法。

在背景執行一個操作,本質就是重新建立一個線程執行目前方法。 - (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg:

在指定的線程上執行一個方法,需要使用者建立一個線程對象。 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait:

在主線程上執行一個方法(前面已經使用過)。 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:

NSOperation

使用NSOperation和NSOperationQueue進行多線程開發類似于C#中的線程池,隻要将一個NSOperation(實際開中需要使用其子類NSInvocationOperation、NSBlockOperation)放到NSOperationQueue這個隊列中線程就會依次啟動。NSOperationQueue負責管理、執行所有的NSOperation,在這個過程中可以更加容易的管理線程總數和控制線程之間的依賴關系。

NSOperation有兩個常用子類用于建立線程操作:NSInvocationOperation和NSBlockOperation,兩種方式本質沒有差別,但是是後者使用Block形式進行代碼組織,使用相對友善。

開一個線程的方法:

-( void )loadImageWithMultiThread{

    NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];

    //建立完NSInvocationOperation對象并不會調用,它由一個start方法啟動操作,但是注意如果直接調用start方法,則此操作會在主線程中調用,一般不會這麼操作,而是添加到NSOperationQueue中

//    [invocationOperation start];

    //建立操作隊列

    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];

    //注意添加到操作隊後,隊列會開啟一個線程執行此操作

    [operationQueue addOperation:invocationOperation];

}

開多個線程下載下傳圖檔 #pragma mark 多線程下載下傳圖檔 -( void )loadImageWithMultiThread{

    int  count=ROW_COUNT*COLUMN_COUNT;

    //建立操作隊列

    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];

    operationQueue.maxConcurrentOperationCount=5; //設定最大并發線程數

    //建立多個線程用于填充圖檔

    for  ( int  i=0; i<count; ++i) {

        //方法1:建立操作塊添加到隊列

//        //建立多線程操作

//        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{

//            [self loadImage:[NSNumber numberWithInt:i]];

//        }];

//        //建立操作隊列

//

//        [operationQueue addOperation:blockOperation];

        //方法2:直接使用操隊列添加操作

        [operationQueue addOperationWithBlock:^{

            [self loadImage:[NSNumber numberWithInt:i]];

        }];

    } } #pragma mark 将圖檔顯示到界面

-(void)updateImageWithData:(NSData *)data andIndex:(int )index{

    UIImage *image=[UIImage imageWithData:data];

    UIImageView *imageView= _imageViews[index];

    imageView.image=image;

}

#pragma mark 請求圖檔資料

-(NSData *)requestData:(int )index{

    NSURL *url=[NSURL URLWithString:_imageNames[index]];

    NSData *data=[NSData dataWithContentsOfURL:url];

    return data;

}

#pragma mark 加載圖檔

-(void)loadImage:(NSNumber *)index{

    int i=[index integerValue];

    //請求資料

    NSData *data= [self requestData:i];

    NSLog(@"%@",[NSThread currentThread]);

    //更新UI界面,此處調用了主線程隊列的方法(mainQueue是UI主線程)

    [[NSOperationQueue mainQueue] addOperationWithBlock:^{

        [self updateImageWithData:data andIndex:i];

    }]; }

1、使用NSBlockOperation方法,所有的操作不必單獨定義方法,同時解決了隻能傳遞一個參數的問題。 2、調用主線程隊列的addOperationWithBlock:方法進行UI更新,不用再定義一個參數實體(之前必須定義一個KCImageData解決隻能傳遞一個參數的問題)。 3、使用NSOperation進行多線程開發可以設定最大并發線程,有效的對線程進行了控制(上面的代碼運作起來你會發現列印目前程序時隻有有限的線程被建立,如上面的代碼設定最大線程數為5,則圖檔基本上是五個一次加載的)。

線程執行順序

前面使用NSThread很難控制線程的執行順序,但是使用NSOperation就容易多了,每個NSOperation可以設定依賴線程。假設操作A依賴于操作B,線程操作隊列在啟動線程時就會首先執行B操作,然後執行A。對于前面優先加載最後一張圖的需求,隻要設定前面的線程操作的依賴線程為最後一個操作即可。修改圖檔加載方法如下:

-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    //建立操作隊列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;//設定最大并發線程數
    
    NSBlockOperation *lastBlockOperation=[NSBlockOperation blockOperationWithBlock:^{
        [self loadImage:[NSNumber numberWithInt:(count-1)]];
    }];
    //建立多個線程用于填充圖檔
    for (int i=0; i<count-1; ++i) {
        //方法1:建立操作塊添加到隊列
        //建立多線程操作
        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        //設定依賴操作為最後一張圖檔加載操作
        [blockOperation addDependency:lastBlockOperation];
        
        [operationQueue addOperation:blockOperation];
        
    }
    //将最後一個圖檔的加載操作加入線程隊列
    [operationQueue addOperation:lastBlockOperation];
}

      

GCD

GCD(Grand Central Dispatch)是基于C語言開發的一套多線程開發機制,也是目前蘋果官方推薦的多線程開發方法。前面也說過三種開發中GCD抽象層次最高,當然是用起來也最簡單, 隻是它基于C語言開發,并不像NSOperation是面向對象的開發,而是完全面向過程的 。 這種機制相比較于前面兩種多線程開發方式最顯著的優點就是它對于多核運算更加有效。

GCD中也有一個類似于NSOperationQueue的隊列,GCD統一管理整個隊列中的任務。但是GCD中的隊列分為并行隊列和串行隊列兩類:

  • 串行隊列:隻有一個線程,加入到隊列中的操作按添加順序依次執行。
  • 并發隊列:有多個線程,操作進來之後它會将這些隊列安排在可用的處理器上,同時保證先進來的任務優先處理。

其實在GCD中還有一個特殊隊列就是主隊列,用來執行主線程上的操作任務(從前面的示範中可以看到其實在NSOperation中也有一個主隊列)。

串行隊列

因為目前隊列中隻有一個線程,是以串行隊列會按順序執行。 使用串行隊列時首先要建立一個串行隊列,然後調用異步調用方法,在此方法中傳入串行隊列和線程操作即可自動執行。

#pragma  mark 多線程下載下傳圖檔

-( void )loadImageWithMultiThread{

    int  count=ROW_COUNT*COLUMN_COUNT;

    dispatch_queue_t serialQueue = dispatch_queue_create( "myThreadQueue1" , DISPATCH_QUEUE_SERIAL); //注意queue對象不是指針類型 

    //建立多個線程用于填充圖檔

    for  ( int  i=0; i<count; ++i) {

        //異步執行隊列任務

        dispatch_async(serialQueue, ^{

            [self loadImage:[NSNumber numberWithInt:i]];

        });

    }

    //非ARC環境請釋放

//    dispatch_release(seriQueue);

}

并行隊列

并發隊列同樣是使用dispatch_queue_create()方法建立,隻是最後一個參數指定為DISPATCH_QUEUE_CONCURRENT進行建立,但是在實際開發中我們通常不會重新建立一個并發隊列而是使用dispatch_get_global_queue()方法取得一個全局的并發隊列(當然如果有多個并發隊列可以使用前者建立)。下面通過并行隊列示範一下多個圖檔的加載。代碼與上面串行隊列加載類似,隻需要修改照片加載方法如下:

-( void )loadImageWithMultiThread{

    int  count=ROW_COUNT*COLUMN_COUNT;

    dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    //建立多個線程用于填充圖檔

    for  ( int  i=0; i<count; ++i) {

        //異步執行隊列任務

        dispatch_async(globalQueue, ^{

            [self loadImage:[NSNumber numberWithInt:i]];

        });

    }

}

其他任務執行方法

GCD執行任務的方法并非隻有簡單的同步調用方法和異步調用方法,還有其他一些常用方法: 1、dispatch_apply():重複執行某個任務,但是注意這個方法沒有辦法異步執行(為了不阻塞線程可以使用dispatch_async()包裝一下再執行)。

dispatch_async ( dispatch_get_global_queue ( DISPATCH_QUEUE_PRIORITY_DEFAULT , 0 ), ^{

        dispatch_apply ( 100 , dispatch_queue_create ( "myThread" , DISPATCH_QUEUE_PRIORITY_DEFAULT ), ^( size_t index) {

            NSLog ( @"index == %zu and thread == %@" ,index,[ NSThread currentThread ]);

        });

            });

單次執行一個任務,此方法中的任務隻會執行一次,重複調用也沒辦法重複執行(單例模式中常用此方法)。 2、 dispatch_once():

static dispatch_once_t __singletonToken;

    static id __singleton__;

    dispatch_once ( &__singletonToken, ^{

        __singleton__ = [[ self alloc ] init ];

    } );

        return __singleton__;

延遲一定的時間後執行。 3、dispatch_time():

   double delayInSeconds = 1.0 ;

    __block SeanGCDViewController * bself = self ;

    dispatch_time_t popTime = dispatch_time ( DISPATCH_TIME_NOW , ( int64_t )(delayInSeconds * NSEC_PER_SEC ));     dispatch_after(popTime, dispatch_get_main_queue(), ^(void){         // code     });

  使用此方法建立的任務首先會檢視隊列中有沒有别的任務要執行,如果有,則會等待已有任務執行完畢再執行;   同時在此方法後添加的任務必須等待此方法中任務執行後才能執行。 (利用這個方法可以控制執行順序,例如前面先加載最後一張圖檔的需求就可以先使用這個方法将最後一張圖檔加載的操作添加到隊列,然後調用dispatch_async()添加其他圖檔加載任務) 4、dispatch_barrier_async(): dispatch_queue_t concurrentQueue = dispatch_queue_create ( "my.concurrent.queue" , DISPATCH_QUEUE_CONCURRENT );

    dispatch_async (concurrentQueue, ^(){

        NSLog ( @"dispatch-1" );

    });

    dispatch_async (concurrentQueue, ^(){

        NSLog ( @"dispatch-2" );

    });

    dispatch_barrier_async (concurrentQueue, ^(){

        NSLog ( @"dispatch-barrier" );

    });

    dispatch_async (concurrentQueue, ^(){

        NSLog ( @"dispatch-3" );

    });

    dispatch_async (concurrentQueue, ^(){

        NSLog ( @"dispatch-4" );     });

上面代碼的執行步驟: //    dispatch_barrier_async 作用是在并行隊列中,等待前面兩個操作并行操作完成,這裡是并行輸出

//   

//    dispatch-1 , dispatch-2

//   

//   

//    然後執行

//   

//    dispatch_barrier_async 中的操作, ( 現在就隻會執行這一個操作 ) 執行完成後,即輸出

//   

//    "dispatch-barrier ,

//    最後該并行隊列恢複原有執行狀态,繼續并行執行

//    //    dispatch-3,dispatch-4

實作對任務分組管理,如果一組任務全部完成可以通過dispatch_group_notify()方法獲得完成通知(需要定義dispatch_group_t作為分組辨別) 5、dispatch_group_async():

    dispatch_queue_t dispatchQueue = dispatch_queue_create ( "ted.queue.next" , DISPATCH_QUEUE_CONCURRENT );

    dispatch_group_t dispatchGroup = dispatch_group_create ();

    dispatch_group_async (dispatchGroup, dispatchQueue, ^(){

        NSLog ( @"dispatch-1" );

    });

    dispatch_group_async (dispatchGroup, dispatchQueue, ^(){

        NSLog ( @"dspatch-2" );

    });

    dispatch_group_notify (dispatchGroup, dispatch_get_main_queue (), ^(){

        NSLog ( @"end" );

    });

//    上面的 log1 和 log2 輸出順序不定,因為是在并行隊列上執行,當并行隊列全部執行完成後,最後到 main 隊列上執行一個操作,保證 “end” 是最後輸出。    

線程同步

說到多線程就不得不提多線程中的鎖機制,多線程操作過程中往往多個線程是并發執行的,同一個資源可能被多個線程同時通路,造成資源搶奪,這個過程中如果沒有鎖機制往往會造成重大問題。

要解決資源搶奪問題在iOS中有常用的有兩種方法: 一種是使用NSLock同步鎖,另一種是使用@synchronized代碼塊。兩種方法實作原理是類似的,隻是在處理上代碼塊使用起來更加簡單。

總結

1>無論使用哪種方法進行多線程開發,每個線程啟動後并不一定立即執行相應的操作,具體什麼時候由系統排程(CPU空閑時就會執行)。

2>更新UI應該在主線程(UI線程)中進行,并且推薦使用同步調用,常用的方法如下:

  • - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
  • (或者-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL) wait;方法傳遞主線程[NSThread mainThread])
  • [NSOperationQueue mainQueue] addOperationWithBlock:
  • dispatch_sync(dispatch_get_main_queue(), ^{})

3>NSThread适合輕量級多線程開發,控制線程順序比較難,同時線程總數無法控制(每次建立并不能重用之前的線程,隻能建立一個新的線程)。

4>對于簡單的多線程開發建議使用NSObject的擴充方法完成,而不必使用NSThread。

5>可以使用NSThread的currentThread方法取得目前線程,使用 sleepForTimeInterval:方法讓目前線程休眠。

6>NSOperation進行多線程開發可以控制線程總數及線程依賴關系。

7>建立一個NSOperation不應該直接調用start方法(如果直接start則會在主線程中調用)而是應該放到NSOperationQueue中啟動。

8>相比NSInvocationOperation推薦使用NSBlockOperation,代碼簡單,同時由于閉包性使它沒有傳參問題。

9>NSOperation是對GCD面向對象的ObjC封裝,但是相比GCD基于C語言開發,效率卻更高,建議如果任務之間有依賴關系或者想要監聽任務完成狀态的情況下優先選擇NSOperation否則使用GCD。

10>在GCD中串行隊列中的任務被安排到一個單一線程執行(不是主線程),可以友善地控制執行順序;并發隊列在多個線程中執行(前提是使用異步方法),順序控制相對複雜,但是更高效。

11>在GCD 中一個操作是多線程執行還是單線程執行取決于目前隊列類型和執行方法,隻有隊列類型為并行隊列并且使用異步方法執行時才能在多個線程中執行(如果是并行隊列使用同步方法調用則會在主線程中執行)。

12>相比使用NSLock,@synchronized更加簡單,推薦使用後者。