iOS 多線程的實現方式及應用示例

概述


優點:
  • 把程序中耗時的任務放到後臺去處理,如圖片、視頻的下載等;
  • 充分發揮多核處理器的優勢,併發執行讓系統運行的更快、更流暢、用戶體驗更佳。
不足:
  • 大量的線程操作會降低代碼的可讀性;
  • 大量的線程需要更多的內存空間;
  • 當多個線程對同一資源出現爭奪的時候會出現線程安全問題。

目前實現多線程的技術有四種:pthread、NSThread、GCD和NSOperation。

  • pthread:是一套基於C語言的通用多線程API,線程的生命週期需要我們手動管理,使用難度較大,所以我們幾乎不會使用。
  • NSThread:是一套面向對象的API,線程的生命週期需要我們手動管理,簡單易用,可直接操作線程對象。
  • GCD:是一套底層基於C語言的多線程API,自動管理生命週期,充分利用了多核處理器的優點。
  • NSOperation:是一套底層基於GCD面向對象的多線程API,自動管理生命週期,並且比GCD多了一些更簡單實用的功能。

在這裏我們暫且不討論pthread的使用,主要看看後面三個方案都是怎麼應用的。

NSThread


一個NSThread對象就代表一個線程,使用NSThread有多種創建線程的方式:

1. 先創建再啓動


    // 創建
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"Axe"];
    // 啓動
    [thread start];

2. 直接創建並啓動線程


    [NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"Axe"];

3. 創建並啓動

    [self performSelectorInBackground:@selector(run:) withObject:@"Axe"];
    [NSThread sleepForTimeInterval:2.0];

從三種創建線程的方法可以看到:方法2和3都可以更方便的創建一個線程,並且自啓動。而方法一可以很方便的拿到線程對象。

4.應用示例


以下載一張圖片爲例:

    NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg"];
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(downloadImageWithURL:) object:url];
    [thread start];
    - (void)downloadImageWithURL:(NSURL *)url {
        NSError *error;
        NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
        if (error) {
            NSLog(@"error = %@",error);
        } else {
            UIImage *image = [UIImage imageWithData:data];
            [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
        }

    }

GCD


GCD全稱是Grand Central Dispatch,可譯爲“中樞調度器”。GCD是蘋果公司爲多核的並行運算提出的解決方案,它會自動利用更多的CPU內核來開啓線程執行任務。
在瞭解GCD之前先明白同步/異步、並行/串行這幾個名詞的概念。
  • 同步線程:在當前線程可立即執行任務,不具備開啓線程的能力,會阻塞當前的線程,必須等待同步線程中的任務執行完並返回後,纔會執行下一個任務。
  • 異步線程:在當前線程結束後執行任務,具備開啓新的線程的能力。
  • 並行隊列:允許多個任務同時執行。
  • 串行隊列:只有等上一個任務執行完畢後,下一個任務纔會被執行。

創建串行隊列


    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

創建並行隊列

    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

創建同步+串行隊列

    //在當前線程,立即執行任務
    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"%@",[NSThread currentThread]);
        }
    });

創建同步+並行隊列

    //在當前線程,立即執行任務
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"%d --- %@",i, [NSThread currentThread]);
        }
    });

創建異步+串行隊列

    //另開啓線程,多個任務順序執行
    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(serialQueue, ^{
        
        dispatch_async(serialQueue, ^{
            for (int i = 0; i < 10; i++) {
                NSLog(@"%d --- %@",i, [NSThread currentThread]);
            }
        });
        
        dispatch_async(serialQueue, ^{
            for (int i = 0; i < 10; i++) {
                NSLog(@"%d --- %@",i, [NSThread currentThread]);
            }
        });
    });

創建異步+並行隊列

    //另開啓線程,多個任務一起執行,不分先後
    dispatch_queue_t serialQueue = dispatch_queue_create("com.serial.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        
        dispatch_async(serialQueue, ^{
            for (int i = 0; i < 10; i++) {
                NSLog(@"%d --- %@",i, [NSThread currentThread]);
            }
        });
        
        dispatch_async(serialQueue, ^{
            for (int i = 0; i < 10; i++) {
                NSLog(@"%d --- %@",i, [NSThread currentThread]);
            }
        });
    });

應用示例

還是以下載一張圖片爲例
    NSURL *url = [NSURL URLWithString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg"];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.download.image", DISPATCH_QUEUE_CONCURRENT);
    
    __weak typeof(self) weakSelf = self;
    
    dispatch_async(concurrentQueue, ^{
        
        NSError *error;
        
        NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:&error];
        
        if (error) {
            NSLog(@"error = %@",error);
        } else {
            UIImage *image = [UIImage imageWithData:data];
            dispatch_async(dispatch_get_main_queue(), ^{
                weakSelf.imageView.image = image;
            });
        }
    });

GCD其他函數的應用


dispatch_barrier 柵欄


功能描述:先執行柵欄前面的隊列,再執行柵欄中的隊列,等待柵欄中的隊列執行完畢後,纔會執行柵欄後面的隊列。
注意事項:柵欄在並行隊列中使用纔有它的意義,強行在無序執行的隊列中創造出順序執行的隊列任務,當不能使全局的並行隊列,一般會自己創建我們的並行隊列
代碼示例:
    dispatch_queue_t concurrentQueue = dispatch_queue_create("com.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"1-->%d --- %@",i, [NSThread currentThread]);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"2-->%d --- %@",i, [NSThread currentThread]);
        }
    });
    
    dispatch_barrier_async(concurrentQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"3-->%d --- %@",i, [NSThread currentThread]);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 10; i++) {
            NSLog(@"4-->%d --- %@",i, [NSThread currentThread]);
        }
    });
通過log輸出可以看到,1和2隊列的任務會先無序執行,在其兩個隊列中的任務執行完畢後,纔會執行柵欄隊列中的任務,最後才執行隊列4。

dispatch_after 延遲


    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
    });

dispatch_once 執行一次


    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //  執行一次
    });

 
應用:常用用於單例中。

dispatch_apply   快速迭代


    NSArray *array = @[@"a",@"b",@"c",@"d",@"e"];
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply(array.count, globalQueue, ^(size_t idx) {
        NSLog(@"array[%zu] = %@",idx, array[idx]);
    });


dispatch_group 隊列組


功能描述:將多個隊列添加到一個隊列組中,當隊列組中的任務都執行完畢後,會通知我們執行結果。

代碼示例:

    dispatch_group_t group = dispatch_group_create();
    
    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_group_async(group, globalQueue, ^{
        NSLog(@"任務1");
    });
    
    dispatch_group_async(group, globalQueue, ^{
        NSLog(@"任務3");
    });
    
    dispatch_group_async(group, globalQueue, ^{
        NSLog(@"任務2");
    });
    
    dispatch_group_notify(group, globalQueue, ^{
        NSLog(@"所有任務都已完成");
    });
    
    dispatch_group_async(group, globalQueue, ^{
        NSLog(@"任務4");
    });

GCD定時器


    dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, globalQueue);
    
    // 3s後定時器啓動
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
    
    // 每1秒執行一次回調
    dispatch_source_set_timer(timer, start, 2.0 * NSEC_PER_SEC, 0);
    
    // 計時
    __block int count = 0;
    
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"%d", count);
        if (count > 10) {
            dispatch_cancel(timer);
        }
        count++;
    });
    dispatch_resume(timer);

NSOperation


NSOperation是一個抽象類,並不具備封裝操作的能力,我們必須使用它的子類,爲此係統也提供了NSInvocationOperation和NSBlockOperation兩個子類供我們使用。當然我們也可以繼承NSOperation,創建我們的子類,實現內部相應的方法。

NSInvocationOperation

    NSInvocationOperation *invocationOperation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(downloadImageWithURL:) object:url];
    [invocationOperation start];
默認情況下,調用start方法後,並不會開啓一個新的線程去執行selector,而是在當前線程同步的執行操作,只有將NSOperation添加到NSOperationQueue中,纔會執行異步操作。

NSBlockOperation

    NSBlockOperation *blockOperation = [NSBlockOperation blockOperationWithBlock:^{
        // 在主線程中執行
        NSLog(@"操作1 --- %@", [NSThread currentThread]);
    }];
    
    [blockOperation addExecutionBlock:^{
        // 在子線程中執行
        NSLog(@"操作2 --- %@", [NSThread currentThread]);
    }];
    
    // 添加額外的任務數大於1纔會異步執行
    
    [blockOperation addExecutionBlock:^{
        // 在子線程中執行
        NSLog(@"操作3 --- %@", [NSThread currentThread]);
    }];
    
    [blockOperation addExecutionBlock:^{
        // 在子線程中執行
        NSLog(@"操作4 --- %@", [NSThread currentThread]);
    }];
    
    [blockOperation start];

Operation的其它用法


執行操作


執行一個Operation有兩種方式,一種是手動調用start,這種方法調用會在當前線程進行同步執行,因此在主線程裏面一定要小心調用,不然就會堵塞主線程。另一種是自動執行,只要將operation添加到operationQueue中,就會盡快執行操作。

取消操作


NSOperation開始執行操作後,會默認一直到操作完成,當然中途我們也可以調用cancel取消操作。
在調用cancel方法的時候,只是將cancelled設置爲了YES,因此在每個操作開始前,或者在每個有意義的實際操作完成後,都要先檢測isCancelled是否被設置成了YES, 如果已經取消了,那麼後面的操作就不用在執行了。

操作完成後的操作


如果想在NSOperation執行完操作後做一些事情,可以調用completionBlock設置,在操作完成後就會回調block裏面的內容。

自定義Operation


如果系統提供的NSInvocationOperation和NSBlockOperation兩個子類不能滿足需求時,我們通過繼承自定義之類,並添加需要執行的操作。

這裏我們仍然以下載圖片爲例:
//
//  MyOperation.h
//

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef void(^MyDownloadFinishedBlock)(UIImage *image);

@interface MyOperation : NSOperation
@property (copy, nonatomic, readonly) NSString *imageURL;

- (instancetype)initWithURLString:(NSString *)urlString downloadFinishedBlock:(MyDownloadFinishedBlock)downloadFinishedBlock;
@end
//
//  MyOperation.m
//

#import "MyOperation.h"

@interface MyOperation ()
@property (copy, nonatomic) MyDownloadFinishedBlock downloadFinishedBlock;
@end

@implementation MyOperation

- (instancetype)initWithURLString:(NSString *)urlString downloadFinishedBlock:(MyDownloadFinishedBlock)downloadFinishedBlock {
    self = [super init];
    if (self) {
        _imageURL = urlString;
        _downloadFinishedBlock = downloadFinishedBlock;
    }
    return self;
}

- (void)main {
    
    // 如果時在異步線程中執行操作,即main方法在異步線程中調用,那麼將無法訪問主線程的自動釋放池,因此創建一個屬於當前線程的自動釋放池
    
    @autoreleasepool {
        
        // 在main方法中定期的調用isCancelled方法檢測操作是否已經被取消
        // 在執行任何實際的工作之前,也就是main方法的開頭,就要檢測操作是否已經被取消
        // 在執行一段耗時的操作後也需要檢測操作是否已經被取消
        
        if (self.isCancelled) {
            return;
        }
        
        NSURL *url = [NSURL URLWithString:_imageURL];
        
        // 此處就是通過網絡獲取圖片data,是比較耗時的操作,所以在後面就需要檢測操作是否已經被取消
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        
        if (self.isCancelled) {
            url = nil;
            _imageURL = nil;
            
            return;
        }
        
        UIImage *image = [UIImage imageWithData:imageData];
        
        if (self.isCancelled) {
            image = nil;
            
            return;
        }
        
        if (_downloadFinishedBlock) {
            _downloadFinishedBlock(image);
        }
        
    }
    
}
@end
    MyOperation *operation = [[MyOperation alloc] initWithURLString:@"http://f.hiphotos.baidu.com/image/pic/item/203fb80e7bec54e753da379aba389b504fc26a7b.jpg" downloadFinishedBlock:^(UIImage *image) {
        NSLog(@"%@",[NSThread currentThread]);
        
        [_imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
    }];
    
    [operation start];
說明:如果你要執行一個同步操作,那只需要重寫main方法,在裏面添加必要的操作。如果執行一個異步的操作,那就需要重寫start方法,因爲在你把操作添加進去後系統會自動調用start方法,這時將不在調用mian裏面的操作。

NSOperationQueue


一個NSOperation對象可以通過start方法來執行任務,默認是同步執行的。也可以將其添加到NSOperationQueue操作隊列中去執行,它是異步執行的。

添加NSOperation到NSOperationQueue中


不管通過那種方式添加,只要將operation添加到operationQueue中,通常短時間內就會得到執行(異步)。但是如果存在依賴,或者整個operationQueue被暫停等原因,也可能需要等待。

添加一個operation

    [operationQueue addOperation:invocationOperation];

添加一組operation

    [operationQueue addOperations:@[invocationOperation, blockOperation] waitUntilFinished:YES];

添加一個block形式的operation

    [operationQueue addOperationWithBlock:^{
        NSLog(@"任務");
    }];

添加依賴

概述


所謂依賴就是說,當某個operation對象需要依賴與其它operation對象才能完成時,就可通過addDependency方法添加一個或者多個依賴對象,只有所有依賴的對象都已經完成操作後,當前的operation對象纔開始執行操作,當然也可以通過removeDependency方法來移除這種依賴關係。


依賴方式


既可以在同一個operationQueue中不同operation對象添加依賴,也可以在不同的operationQueue之間不同operation對象之間添加依賴,operation對象會管理自己的依賴關係。

限制依賴


雖然可以在一個queue中添加依賴,也可以在不同的queue中添加依賴,但是要特別注意的是,不能添加環形依賴,即a依賴b,b也依賴a。

應用示例:


這裏以修改用戶頭像爲例,模擬從用戶上傳頭像到顯示頭像的過程。

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    
    NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1.上傳頭像");
    }];
    
    NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2.從服務器請求上傳頭像的url");
    }];
    
    NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"3.通過url下載頭像");
    }];
    
    [operationQueue addOperation:blockOperation1];
    [operationQueue addOperation:blockOperation2];
    [operationQueue addOperation:blockOperation3];

打印順序如下:2 - 1 - 3 或者 3 - 2 -1 

這樣的順序顯然不符合正常的邏輯順序,這時候,我們就可以通過添加依賴,達到我們說期望的順序邏輯。

    NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
    
    NSBlockOperation *blockOperation1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"1.上傳頭像");
    }];
    
    NSBlockOperation *blockOperation2 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"2.從服務器請求上傳頭像的url");
    }];
    
    NSBlockOperation *blockOperation3 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"3.通過url下載頭像");
    }];
    
    
    // 3 -> 2  2 -> 1 (3 - 2 - 1)
    [blockOperation3 addDependency:blockOperation2];
    [blockOperation2 addDependency:blockOperation1];
    
    
    [operationQueue addOperation:blockOperation1];
    [operationQueue addOperation:blockOperation2];
    [operationQueue addOperation:blockOperation3];


打印順序如下:1 - 2 - 3
         
這樣就符合我們期望的邏輯,即用戶先上傳圖片並完成後,再從服務器獲取該用戶頭像的url地址,最後通過url地址下載相應的頭像並顯示。



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