關於iOS多線程,我說,你聽,沒準你就懂了!

事出必有因,今天我想和你聊聊線程的原因就是——當然是本着一個共產黨人的思想覺悟,爲人民透析生命,講解你正在蒙圈的知識點,或者想破腦袋才發現如此簡單的技術方案。



很多人學線程,迷迷糊糊;很多人問線程,有所期待;也有很多人寫線程,分享認知給正在努力的年輕人,呦,呦,呦呦。但是,你真的瞭解線程麼?你真的會用多線程麼?你真的學明白,問明白,寫明白了麼?不管你明不明白,反正我不明白,但是,沒準,你看完,你就明白了。



前言

  • 提到線程,那就不得不提CPU,現代的CPU有一個很重要的特性,就是時間片,每一個獲得CPU的任務只能運行一個時間片規定的時間。
  • 其實線程對操作系統來說就是一段代碼以及運行時數據。操作系統會爲每個線程保存相關的數據,當接收到來自CPU的時間片中斷事件時,就會按一定規則從這些線程中選擇一個,恢復它的運行時數據,這樣CPU就可以繼續執行這個線程了。
  • 也就是其實就單核CUP而言,並沒有辦法實現真正意義上的併發執行,只是CPU快速地在多條線程之間調度,CPU調度線程的時間足夠快,就造成了多線程併發執行的假象。並且就單核CPU而言多線程可以解決線程阻塞的問題,但是其本身運行效率並沒有提高,多CPU的並行運算才真正解決了運行效率問題。
  • 系統中正在運行的每一個應用程序都是一個進程,每個進程系統都會分配給它獨立的內存運行。也就是說,在iOS系統中中,每一個應用都是一個進程。
  • 一個進程的所有任務都在線程中進行,因此每個進程至少要有一個線程,也就是主線程。那多線程其實就是一個進程開啓多條線程,讓所有任務併發執行。
  • 多線程在一定意義上實現了進程內的資源共享,以及效率的提升。同時,在一定程度上相對獨立,它是程序執行流的最小單元,是進程中的一個實體,是執行程序最基本的單元,有自己棧和寄存器。
  • 上面這些你是不是都知道,但是我偏要說,哦呵呵。既然我們聊線程,那我們就先從線程開刀。

Pthreads && NSThread

先來看與線程有最直接關係的一套C的API:

Pthreads

POSIX線程(POSIX threads),簡稱Pthreads,是線程的POSIX標準。該標準定義了創建和操縱線程的一整套API。在類Unix操作系統(Unix、Linux、Mac OS X等)中,都使用Pthreads作爲操作系統的線程。

高大上有木有,跨平臺有木有,你沒用過有木有!下面我們來看一下這個看似牛逼但真的基本用不到的Pthreads是怎麼用的:

不如我們來用Pthreads創建一個線程去執行一個任務:

記得引入頭文件`#import "pthread.h"`

-(void)pthreadsDoTask{
    /*
     pthread_t:線程指針
     pthread_attr_t:線程屬性
     pthread_mutex_t:互斥對象
     pthread_mutexattr_t:互斥屬性對象
     pthread_cond_t:條件變量
     pthread_condattr_t:條件屬性對象
     pthread_key_t:線程數據鍵
     pthread_rwlock_t:讀寫鎖
     //
     pthread_create():創建一個線程
     pthread_exit():終止當前線程
     pthread_cancel():中斷另外一個線程的運行
     pthread_join():阻塞當前的線程,直到另外一個線程運行結束
     pthread_attr_init():初始化線程的屬性
     pthread_attr_setdetachstate():設置脫離狀態的屬性(決定這個線程在終止時是否可以被結合)
     pthread_attr_getdetachstate():獲取脫離狀態的屬性
     pthread_attr_destroy():刪除線程的屬性
     pthread_kill():向線程發送一個信號
     pthread_equal(): 對兩個線程的線程標識號進行比較
     pthread_detach(): 分離線程
     pthread_self(): 查詢線程自身線程標識號
     //
     *創建線程
     int pthread_create(pthread_t _Nullable * _Nonnull __restrict, //指向新建線程標識符的指針
     const pthread_attr_t * _Nullable __restrict,  //設置線程屬性。默認值NULL。
     void * _Nullable (* _Nonnull)(void * _Nullable),  //該線程運行函數的地址
     void * _Nullable __restrict);  //運行函數所需的參數
     *返回值:
     *若線程創建成功,則返回0
     *若線程創建失敗,則返回出錯編號
     */
    
    //
    pthread_t thread = NULL;
    NSString *params = @"Hello World";
    int result = pthread_create(&thread, NULL, threadTask, (__bridge void *)(params));
    result == 0 ? NSLog(@"creat thread success") : NSLog(@"creat thread failure");
    //設置子線程的狀態設置爲detached,則該線程運行結束後會自動釋放所有資源
    pthread_detach(thread);
}

void *threadTask(void *params) {
    NSLog(@"%@ - %@", [NSThread currentThread], (__bridge NSString *)(params));
    return NULL;
}

輸出結果:

ThreadDemo[1197:143578] creat thread success
ThreadDemo[1197:143649] <NSThread: 0x600000262e40>{number = 3, name = (null)} - Hello World

從打印結果來看,該任務是在新開闢的線程中執行的,但是感覺用起來超不友好,很多東西需要自己管理,單單是任務隊列以及線程生命週期的管理就夠你頭疼的,那你寫出的代碼還能是藝術麼!其實之所以拋棄這套API很少用,是因爲我們有更好的選擇:NSThread

NSThread

哎呀,它面向對象,再去看看蘋果提供的API,對比一下Pthreads,簡單明瞭,人生彷彿又充滿了陽光和希望,我們先來一看一下系統提供給我們的API自然就知道怎麼用了,來來來,我給你註釋一下啊:

@interface NSThread : NSObject
//當前線程
@property (class, readonly, strong) NSThread *currentThread;
//使用類方法創建線程執行任務
+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;
//判斷當前是否爲多線程
+ (BOOL)isMultiThreaded;
//指定線程的線程參數,例如設置當前線程的斷言處理器。
@property (readonly, retain) NSMutableDictionary *threadDictionary;
//當前線程暫停到某個時間
+ (void)sleepUntilDate:(NSDate *)date;
//當前線程暫停一段時間
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
//退出當前線程
+ (void)exit;
//當前線程優先級
+ (double)threadPriority;
//設置當前線程優先級
+ (BOOL)setThreadPriority:(double)p;
//指定線程對象優先級 0.0~1.0,默認值爲0.5
@property double threadPriority NS_AVAILABLE(10_6, 4_0);
//服務質量
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);
//線程名稱
@property (nullable, copy) NSString *name NS_AVAILABLE(10_5, 2_0);
//棧區大小
@property NSUInteger stackSize NS_AVAILABLE(10_5, 2_0);
//是否爲主線程
@property (class, readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
//獲取主線程
@property (class, readonly, strong) NSThread *mainThread NS_AVAILABLE(10_5, 2_0);
//初始化
- (instancetype)init NS_AVAILABLE(10_5, 2_0) NS_DESIGNATED_INITIALIZER;
//實例方法初始化,需要再調用start方法
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument NS_AVAILABLE(10_5, 2_0);
- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
//線程狀態,正在執行
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);
//線程狀態,正在完成
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);
//線程狀態,已經取消
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);
//取消,僅僅改變線程狀態,並不能像exist一樣真正的終止線程
- (void)cancel NS_AVAILABLE(10_5, 2_0);
//開始
- (void)start NS_AVAILABLE(10_5, 2_0);
//線程需要執行的代碼,一般寫子類的時候會用到
- (void)main NS_AVAILABLE(10_5, 2_0);
@end

另外,還有一個NSObject的分類,瞅一眼:
@interface NSObject (NSThreadPerformAdditions)
//隱式的創建並啓動線程,並在指定的線程(主線程或子線程)上執行方法。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end

上面的介紹您還滿意嗎?小的幫您下載一張圖片,您瞧好:

-(void)creatBigImageView{
    self.bigImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:_bigImageView];
    UIButton *startButton = [UIButton buttonWithType:UIButtonTypeSystem];
    startButton.frame = CGRectMake(0, 0, self.view.frame.size.width / 2, 50);
    startButton.backgroundColor = [UIColor grayColor];
    [startButton setTitle:@"開始加載" forState:UIControlStateNormal];
    [startButton addTarget:self action:@selector(loadImage) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:startButton];
    
    UIButton *jamButton = [UIButton buttonWithType:UIButtonTypeSystem];
    jamButton.frame = CGRectMake(self.view.frame.size.width / 2, 0, self.view.frame.size.width / 2, 50);
    jamButton.backgroundColor = [UIColor grayColor];
    [jamButton setTitle:@"阻塞測試" forState:UIControlStateNormal];
    [jamButton addTarget:self action:@selector(jamTest) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:jamButton];
}

-(void)jamTest{
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"線程阻塞" message:@"" delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil, nil];
    [alertView show];
}


-(void)loadImage{
    NSURL *imageUrl = [NSURL URLWithString:@"http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg"];
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    [self updateImageData:imageData];
}

-(void)updateImageData:(NSData*)imageData{
    UIImage *image = [UIImage imageWithData:imageData];
    self.bigImageView.image = image;
}

運行結果:



我們可以清楚的看到,主線程阻塞了,用戶不可以進行其他操作,你見過這樣的應用嗎?
所以我們這樣改一下:

-(void)creatBigImageView{
    self.bigImageView = [[UIImageView alloc] initWithFrame:self.view.bounds];
    [self.view addSubview:_bigImageView];
    UIButton *startButton = [UIButton buttonWithType:UIButtonTypeSystem];
    startButton.frame = CGRectMake(0, 20, self.view.frame.size.width / 2, 50);
    startButton.backgroundColor = [UIColor grayColor];
    [startButton setTitle:@"開始加載" forState:UIControlStateNormal];
    [startButton addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:startButton];
    
    UIButton *jamButton = [UIButton buttonWithType:UIButtonTypeSystem];
    jamButton.frame = CGRectMake(self.view.frame.size.width / 2, 20, self.view.frame.size.width / 2, 50);
    jamButton.backgroundColor = [UIColor grayColor];
    [jamButton setTitle:@"阻塞測試" forState:UIControlStateNormal];
    [jamButton addTarget:self action:@selector(jamTest) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:jamButton];
}

-(void)jamTest{
    UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"阻塞測試" message:@"" delegate:nil cancelButtonTitle:@"好" otherButtonTitles:nil, nil];
    [alertView show];
}

-(void)loadImageWithMultiThread{
    //方法1:使用對象方法
    //NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    //⚠️啓動一個線程並非就一定立即執行,而是處於就緒狀態,當CUP調度時才真正執行
    //[thread start];
    
    //方法2:使用類方法
    [NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
}

-(void)loadImage{
    NSURL *imageUrl = [NSURL URLWithString:@"http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg"];
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    //必須在主線程更新UI,Object:代表調用方法的參數,不過只能傳遞一個參數(如果有多個參數請使用對象進行封裝),waitUntilDone:是否線程任務完成執行
    [self performSelectorOnMainThread:@selector(updateImageData:) withObject:imageData waitUntilDone:YES];
    
    //[self updateImageData:imageData];
}


-(void)updateImageData:(NSData*)imageData{
    UIImage *image = [UIImage imageWithData:imageData];
    self.bigImageView.image = image;
}

運行結果:


哎呀,用多線程果然能解決線程阻塞的問題,並且NSThread也比Pthreads好用,彷彿你對精通熟練使用多線程又有了一絲絲曙光。假如我有很多不同類型的任務,每個任務之間還有聯繫和依賴,你是不是又懵逼了,上面的你是不是覺得又白看了,其實開發中我覺得NSThread用到最多的就是[NSThread currentThread];了。(不要慌,往下看... ...)


GCD

GCD,全名Grand Central Dispatch,中文名郭草地,是基於C語言的一套多線程開發API,一聽名字就是個狠角色,也是目前蘋果官方推薦的多線程開發方式。可以說是使用方便,又不失逼格。總體來說,他解決我提到的上面直接操作線程帶來的難題,它自動幫你管理了線程的生命週期以及任務的執行規則。下面我們會頻繁的說道一個詞,那就是任務,說白了,任務其實就是你要執行的那段代碼

任務管理方式——隊列

上面說當我們要管理多個任務時,線程開發給我們帶來了一定的技術難度,或者說不方便性,GCD給出了我們統一管理任務的方式,那就是隊列。我們來看一下iOS多線程操作中的隊列:(⚠️不管是串行還是並行,隊列都是按照FIFO的原則依次觸發任務)

兩個通用隊列:
  • 串行隊列:所有任務會在一條線程中執行(有可能是當前線程也有可能是新開闢的線程),並且一個任務執行完畢後,纔開始執行下一個任務。(等待完成)
  • 並行隊列:可以開啓多條線程並行執行任務(但不一定會開啓新的線程),並且當一個任務放到指定線程開始執行時,下一個任務就可以開始執行了。(等待發生)
兩個特殊隊列:
  • 主隊列:系統爲我們創建好的一個串行隊列,牛逼之處在於它管理必須在主線程中執行的任務,屬於有勞保的。
  • 全局隊列:系統爲我們創建好的一個並行隊列,使用起來與我們自己創建的並行隊列無本質差別。

任務執行方式

說完隊列,相應的,任務除了管理,還得執行,要不然有錢不花,掉了白搭,並且在GCD中並不能直接開闢線程執行任務,所以在任務加入隊列之後,GCD給出了兩種執行方式——同步執行(sync)和異步執行(async)。

  • 同步執行:在當前線程執行任務,不會開闢新的線程。必須等到Block函數執行完畢後,dispatch函數纔會返回。
  • 異步執行:可以在新的線程中執行任務,但不一定會開闢新的線程。dispatch函數會立即返回, 然後Block在後臺異步執行。
上面的這些理論都是本人在無數被套路背後總結出來的血淋淋的經驗,與君共享,但是這麼寫我猜你一定還是不明白,往下看,說不定有驚喜呢。

任務隊列組合方式

相信這個標題你看過無數次?是不是看完也不知道到底怎麼用?這麼巧,我也是,請相信下面這些肯定有你不知道並且想要的,我們從兩個最直接的點切入:

1. 線程死鎖

這個你是不是也看過無數次?哈哈哈!你是不是覺得我又要開始複製黏貼了?請往下看:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1========%@",[NSThread currentThread]);
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"2========%@",[NSThread currentThread]);
    });
    NSLog(@"3========%@",[NSThread currentThread]);
}

運行結果:


打印結果:

ThreadDemo[5615:874679] 1========<NSThread: 0x608000072440>{number = 1, name = main}

真不是我套路你,我們還是得分析一下爲什麼會死鎖,因爲總得爲那些沒有飽受過套路的人心裏留下一段美好的回憶,分享代碼,我們是認真的!

事情是這樣的:

我們先做一個定義:- (void)viewDidLoad{} ---> 任務A,GCD同步任務 --->任務B。
總而言之呢,大概是這樣的,首先,任務A在主隊列,並且已經開始執行,在主線程打印出1===... ...,然後這時任務B被加入到主隊列中,並且同步執行,這尼瑪事都大了,系統說,同步執行啊,那我不開新的線程了,任務B說我要等我裏面的Block函數執行完成,要不我就不返回,但是主隊列說了,玩蛋去,我是串行的,你得等A執行完才能輪到你,不能壞了規矩,同時,任務B作爲任務A的內部函數,必須等任務B執行完函數返回才能執行下一個任務。那就造成了,任務A等待任務B完成才能繼續執行,但作爲串行隊列的主隊列又不能讓任務B在任務A未完成之前開始執行,所以任務A等着任務B完成,任務B等着任務A完成,等待,永久的等待。所以就死鎖了。簡單不?下面我們慎重看一下我們無意識書寫的代碼!

2. 這樣不死鎖

不如就寫個最簡單的:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1========%@",[NSThread currentThread]);
    NSLog(@"2========%@",[NSThread currentThread]);
    NSLog(@"3========%@",[NSThread currentThread]);
}

打印結果:

ThreadDemo[5803:939324] 1========<NSThread: 0x600000078340>{number = 1, name = main}
ThreadDemo[5803:939324] 2========<NSThread: 0x600000078340>{number = 1, name = main}
ThreadDemo[5803:939324] 3========<NSThread: 0x600000078340>{number = 1, name = main}

之前有人問:順序打印,沒毛病,全在主線程執行,而且順序執行,那它們一定是在主隊列同步執行的啊!那爲什麼沒有死鎖?蘋果的操作系統果然高深啊!

其實這裏有一個誤區,那就是任務在主線程順序執行就是主隊列。其實一點關係都沒有,如果當前在主線程,同步執行任務,不管在什麼隊列任務都是順序執行。把所有任務都以異步執行的方式加入到主隊列中,你會發現它們也是順序執行的。

相信你知道上面的死鎖情況後,你一定會手賤改成這樣試試:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1========%@",[NSThread currentThread]);
    dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"2========%@",[NSThread currentThread]);
    });
    NSLog(@"3========%@",[NSThread currentThread]);
}

打印結果:

ThreadDemo[5830:947858] 1========<NSThread: 0x60000007bb80>{number = 1, name = main}
ThreadDemo[5830:947858] 2========<NSThread: 0x60000007bb80>{number = 1, name = main}
ThreadDemo[5830:947858] 3========<NSThread: 0x60000007bb80>{number = 1, name = main}

你發現正常執行了,並且是順序執行的,你是不是若有所思,沒錯,你想的和我想的是一樣的,和上訴情況一樣,任務A在主隊列中,但是任務B加入到了全局隊列,這時候,任務A和任務B沒有隊列的約束,所以任務B就先執行嘍,執行完畢之後函數返回,任務A接着執行。

我猜你一定手賤這麼改過:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1========%@",[NSThread currentThread]);
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"2========%@",[NSThread currentThread]);
    });
    NSLog(@"3========%@",[NSThread currentThread]);
}

打印結果:

ThreadDemo[5911:962470] 1========<NSThread: 0x600000072700>{number = 1, name = main}
ThreadDemo[5911:962470] 3========<NSThread: 0x600000072700>{number = 1, name = main}
ThreadDemo[5911:962470] 2========<NSThread: 0x600000072700>{number = 1, name = main}

細心而帥氣的你一定發現不是順序打印了,而且也不會死鎖,明明都是加到主隊列裏了啊,其實當任務A在執行時,任務B加入到了主隊列,注意哦,是異步執行,所以dispatch函數不會等到Block執行完成才返回,dispatch函數返回後,那任務A可以繼續執行,Block任務我們可以認爲在下一幀順序加入隊列,並且默認無限下一幀執行。這就是爲什麼你看到2===... ...是最後輸出的了。(⚠️一個函數的有多個內部函數異步執行時,不會造成死鎖的同時,任務A執行完畢後,這些異步執行的內部函數會順序執行)。

我們說說隊列與執行方式的搭配

上面說了系統自帶的兩個隊列,下面我們來用自己創建的隊列研究一下各種搭配情況。
我們先創建兩個隊列,並且測試方法都是在主線程中調用:

//串行隊列
self.serialQueue = dispatch_queue_create("serialQueue.ys.com", DISPATCH_QUEUE_SERIAL);
//並行隊列
self.concurrentQueue = dispatch_queue_create("concurrentQueue.ys.com", DISPATCH_QUEUE_CONCURRENT);
1. 串行隊列 + 同步執行
-(void)queue_taskTest{
    dispatch_sync(self.serialQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_sync(self.serialQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_sync(self.serialQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}

打印結果:

ThreadDemo[6735:1064390] 1========<NSThread: 0x600000073cc0>{number = 1, name = main}
ThreadDemo[6735:1064390] 2========<NSThread: 0x600000073cc0>{number = 1, name = main}
ThreadDemo[6735:1064390] 3========<NSThread: 0x600000073cc0>{number = 1, name = main}
ThreadDemo[6735:1064390] 4========<NSThread: 0x600000073cc0>{number = 1, name = main}

全部都在當前線程順序執行,也就是說,同步執行不具備開闢新線程的能力。

2. 串行隊列 + 異步執行
-(void)queue_taskTest{
    dispatch_async(self.serialQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_async(self.serialQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_async(self.serialQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}

打印結果:

ThreadDemo[6774:1073235] 4========<NSThread: 0x60800006e9c0>{number = 1, name = main}
ThreadDemo[6774:1073290] 1========<NSThread: 0x608000077000>{number = 3, name = (null)}
ThreadDemo[6774:1073290] 2========<NSThread: 0x608000077000>{number = 3, name = (null)}
ThreadDemo[6774:1073290] 3========<NSThread: 0x608000077000>{number = 3, name = (null)}

先打印了4,然後順序在子線程中打印1,2,3。說明異步執行具有開闢新線程的能力,並且串行隊列必須等到前一個任務執行完才能開始執行下一個任務,同時,異步執行會使內部函數率先返回,不會與正在執行的外部函數發生死鎖。

3. 並行隊列 + 同步執行
-(void)queue_taskTest{
    dispatch_sync(self.concurrentQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_sync(self.concurrentQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_sync(self.concurrentQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}

運行結果:

ThreadDemo[7012:1113594] 1========<NSThread: 0x60800007e340>{number = 1, name = main}
ThreadDemo[7012:1113594] 2========<NSThread: 0x60800007e340>{number = 1, name = main}
ThreadDemo[7012:1113594] 3========<NSThread: 0x60800007e340>{number = 1, name = main}
ThreadDemo[7012:1113594] 4========<NSThread: 0x60800007e340>{number = 1, name = main}

未開啓新的線程執行任務,並且Block函數執行完成後dispatch函數纔會返回,才能繼續向下執行,所以我們看到的結果是順序打印的。

4. 並行隊列 + 異步執行
-(void)queue_taskTest{
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"1========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:1];
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"2========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:2];
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"3========%@",[NSThread currentThread]);
        //[self nslogCount:10000 number:3];
    });
    NSLog(@"4========%@",[NSThread currentThread]);
}

打印結果:

ThreadDemo[7042:1117492] 1========<NSThread: 0x600000071900>{number = 3, name = (null)}
ThreadDemo[7042:1117491] 3========<NSThread: 0x608000070240>{number = 5, name = (null)}
ThreadDemo[7042:1117451] 4========<NSThread: 0x600000067400>{number = 1, name = main}
ThreadDemo[7042:1117494] 2========<NSThread: 0x600000071880>{number = 4, name = (null)}

開闢了多個線程,觸發任務的時機是順序的,但是我們看到完成任務的時間卻是隨機的,這取決於CPU對於不同線程的調度分配,但是,線程不是無條件無限開闢的,當任務量足夠大時,線程是會重複利用的。

劃一下重點啊

1. 對於單核CPU來說,不存在真正意義上的並行,所以,多線程執行任務,其實也只是一個人在幹活,CPU的調度決定了非等待任務的執行速率,同時對於非等待任務,多線程並沒有真正意義提高效率。
2. 線程可以簡單的認爲就是一段代碼+運行時數據。
3. 同步執行會在當前線程執行任務,不具備開闢線程的能力或者說沒有必要開闢新的線程。並且,同步執行必須等到Block函數執行完畢,dispatch函數纔會返回,從而阻塞同一串行隊列中外部方法的執行。
4. 異步執行dispatch函數會直接返回,Block函數我們可以認爲它會在下一幀加入隊列,並根據所在隊列目前的任務情況無限下一幀執行,從而不會阻塞當前外部任務的執行。同時,只有異步執行纔有開闢新線程的必要,但是異步執行不一定會開闢新線程。
5. 只要是隊列,肯定是FIFO(先進先出),但是誰先執行完要看第1條。
6. 只要是串行隊列,肯定要等上一個任務執行完成,才能開始下一個任務。但是並行隊列當上一個任務開始執行後,下一個任務就可以開始執行。
7. 想要開闢新線程必須讓任務在異步執行,想要開闢多個線程,只有讓任務在並行隊列中異步執行纔可以。執行方式和隊列類型多層組合在一定程度上能夠實現對於代碼執行順序的調度。
8. 同步+串行:未開闢新線程,串行執行任務;同步+並行:未開闢新線程,串行執行任務;異步+串行:新開闢一條線程,串行執行任務;異步+並行:開闢多條新線程,並行執行任務;在主線程中同步使用主隊列執行任務,會造成死鎖。
8. 對於多核CPU來說,線程數量也不能無限開闢,線程的開闢同樣會消耗資源,過多線程同時處理任務並不是你想像中的人多力量大。

GCD其他函數用法

1. dispatch_after

該函數用於任務延時執行,其中參數dispatch_time_t代表延時時長,dispatch_queue_t代表使用哪個隊列。如果隊列未主隊列,那麼任務在主線程執行,如果隊列爲全局隊列或者自己創建的隊列,那麼任務在子線程執行,代碼如下:

-(void)GCDDelay{
    //主隊列延時
    dispatch_time_t when_main = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
    dispatch_after(when_main, dispatch_get_main_queue(), ^{
        NSLog(@"main_%@",[NSThread currentThread]);
    });
    //全局隊列延時
    dispatch_time_t when_global = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4.0 * NSEC_PER_SEC));
    dispatch_after(when_global, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"global_%@",[NSThread currentThread]);
    });
    //自定義隊列延時
    dispatch_time_t when_custom = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
    dispatch_after(when_custom, self.serialQueue, ^{
        NSLog(@"custom_%@",[NSThread currentThread]);
    });
}

打印結果:

ThreadDemo[1508:499647] main_<NSThread: 0x60000007cf40>{number = 1, name = main}
ThreadDemo[1508:499697] global_<NSThread: 0x608000262d80>{number = 3, name = (null)}
ThreadDemo[1508:499697] custom_<NSThread: 0x608000262d80>{number = 3, name = (null)}
2. dispatch_once

保證函數在整個生命週期內只會執行一次,看代碼。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSLog(@"%@",[NSThread currentThread]);
    });
}

打印結果:

ThreadDemo[1524:509261] <NSThread: 0x600000262940>{number = 1, name = main}
無論你怎麼瘋狂的點擊,在第一次打印之後,輸出臺便巋然不動。
3. dispatch_group_async & dispatch_group_notify

試想,現在牛逼的你要現在兩張小圖,並且你要等兩張圖都下載完成之後把他們拼起來,你要怎麼做?我根本就不會把兩張圖拼成一張圖啊,牛逼的我怎麼可能有這種想法呢?

其實方法有很多,比如你可以一張一張下載,再比如使用局部變量和Blcok實現計數,但是既然今天我們講到這,那我們就得入鄉隨俗,用GCD來實現,有一個神器的東西叫做隊列組,當加入到隊列組中的所有任務執行完成之後,會調用dispatch_group_notify函數通知任務全部完成,代碼如下:

-(void)GCDGroup{
    //
    [self jointImageView];
    //
    dispatch_group_t group = dispatch_group_create();
    __block UIImage *image_1 = nil;
    __block UIImage *image_2 = nil;
    //在group中添加一個任務
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        image_1 = [self imageWithPath:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1502706256731&di=371f5fd17184944d7e2b594142cd7061&imgtype=0&src=http%3A%2F%2Fimg4.duitang.com%2Fuploads%2Fitem%2F201605%2F14%2F20160514165210_LRCji.jpeg"];
        
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        image_2 = [self imageWithPath:@"https://ss3.bdstatic.com/70cFv8Sh_Q1YnxGkpoWK1HF6hhy/it/u=776127947,2002573948&fm=26&gp=0.jpg"];
    });
    //group中所有任務執行完畢,通知該方法執行
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        self.imageView_1.image = image_1;
        self.imageView_2.image = image_2;
        //
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 100), NO, 0.0f);
        [image_2 drawInRect:CGRectMake(0, 0, 100, 100)];
        [image_1 drawInRect:CGRectMake(100, 0, 100, 100)];
        UIImage *image_3 = UIGraphicsGetImageFromCurrentImageContext();
        self.imageView_3.image = image_3;
        UIGraphicsEndImageContext();
    });
}

-(void)jointImageView{
    self.imageView_1 = [[UIImageView alloc] initWithFrame:CGRectMake(20, 50, 100, 100)];
    [self.view addSubview:_imageView_1];
    
    self.imageView_2 = [[UIImageView alloc] initWithFrame:CGRectMake(140, 50, 100, 100)];
    [self.view addSubview:_imageView_2];
    
    self.imageView_3 = [[UIImageView alloc] initWithFrame:CGRectMake(20, 200, 200, 100)];
    [self.view addSubview:_imageView_3];
    
    self.imageView_1.layer.borderColor = self.imageView_2.layer.borderColor = self.imageView_3.layer.borderColor = [UIColor grayColor].CGColor;
    self.imageView_1.layer.borderWidth = self.imageView_2.layer.borderWidth = self.imageView_3.layer.borderWidth = 1;
}
4. dispatch_barrier_async

柵欄函數,這麼看來它能擋住或者分隔什麼東西,別瞎猜了,反正你又猜不對,看這,使用此方法創建的任務,會查找當前隊列中有沒有其他任務要執行,如果有,則等待已有任務執行完畢後再執行,同時,在此任務之後進入隊列的任務,需要等待此任務執行完成後,才能執行。看代碼,老鐵。(⚠️ 這裏併發隊列必須是自己創建的。如果選擇全局隊列,這個函數和dispatch_async將會沒有差別。)

-(void)GCDbarrier{
    
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務1");
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務2");
    });
    
//    dispatch_barrier_async(self.concurrentQueue, ^{
//        NSLog(@"任務barrier");
//    });
    
//    NSLog(@"big");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務3");
    });
//    NSLog(@"apple");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務4");
    });
}

運行結果:

ThreadDemo[1816:673351] 任務3
ThreadDemo[1816:673353] 任務1
ThreadDemo[1816:673350] 任務2
ThreadDemo[1816:673370] 任務4

是不是如你所料,牛逼大了,下面我們打開第一句註釋:

-(void)GCDbarrier{
    
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務1");
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務2");
    });
    
    dispatch_barrier_async(self.concurrentQueue, ^{
        NSLog(@"任務barrier");
    });
    
//    NSLog(@"big");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務3");
    });
//    NSLog(@"apple");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務4");
    });
}

打印結果:

ThreadDemo[1833:678739] 任務2
ThreadDemo[1833:678740] 任務1
ThreadDemo[1833:678740] 任務barrier
ThreadDemo[1833:678740] 任務3
ThreadDemo[1833:678739] 任務4

這個結果和我們上面的解釋完美契合,我們可以簡單的控制函數執行的順序了,你離大牛又近了一步,如果現在的你不會懷疑還有dispatch_barrier_sync這個函數的話,說明... ...嘿嘿嘿,我們看一下這個函數和上面我們用到的函數的區別,你一定想到了,再打開第二個和第三個註釋,如下:

-(void)GCDbarrier{
    
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務1");
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務2");
    });
    
    dispatch_barrier_async(self.concurrentQueue, ^{
        NSLog(@"任務barrier");
    });
    
    NSLog(@"big");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務3");
    });
    NSLog(@"apple");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務4");
    });
}

運行結果:

ThreadDemo[1853:692434] 任務1
ThreadDemo[1853:692421] 任務2
ThreadDemo[1853:692387] big
ThreadDemo[1853:692421] 任務barrier
ThreadDemo[1853:692387] apple
ThreadDemo[1853:692421] 任務3
ThreadDemo[1853:692434] 任務4

不要着急,我們換一下函數:

-(void)GCDbarrier{
    
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務1");
    });
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務2");
    });
    
    dispatch_barrier_sync(self.concurrentQueue, ^{
        NSLog(@"任務barrier");
    });
    
    NSLog(@"big");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務3");
    });
    NSLog(@"apple");
    dispatch_async(self.concurrentQueue, ^{
        NSLog(@"任務4");
    });
}

打印結果:

ThreadDemo[1874:711841] 任務1
ThreadDemo[1874:711828] 任務2
ThreadDemo[1874:711793] 任務barrier
ThreadDemo[1874:711793] big
ThreadDemo[1874:711793] apple
ThreadDemo[1874:711828] 任務3
ThreadDemo[1874:711841] 任務4

老鐵,發現了嗎?這兩個函數對於隊列的柵欄作用是一樣的,但是對於該函數相對於其他內部函數遵循了最開始說到的同步和異步的規則。你是不是有點懵逼,如果你矇蔽了,那麼請在每一個輸出後面打印出當前的線程,如果你還是懵逼,那麼請你重新看,有勞,不謝!

5. dispatch_apply

該函數用於重複執行某個任務,如果任務隊列是並行隊列,重複執行的任務會併發執行,如果任務隊列爲串行隊列,則任務會順序執行,需要注意的是,該函數爲同步函數,要防止線程阻塞和死鎖哦,老鐵。

串行隊列:
-(void)GCDApply{
    //重複執行
    dispatch_apply(5, self.serialQueue, ^(size_t i) {
        NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
    });
}

運行結果:

ThreadDemo[1446:158101] 第0次_<NSThread: 0x600000079ac0>{number = 1, name = main}
ThreadDemo[1446:158101] 第1次_<NSThread: 0x600000079ac0>{number = 1, name = main}
ThreadDemo[1446:158101] 第2次_<NSThread: 0x600000079ac0>{number = 1, name = main}
ThreadDemo[1446:158101] 第3次_<NSThread: 0x600000079ac0>{number = 1, name = main}
ThreadDemo[1446:158101] 第4次_<NSThread: 0x600000079ac0>{number = 1, name = main}
並行隊列:
-(void)GCDApply{
    //重複執行
    dispatch_apply(5, self.concurrentQueue, ^(size_t i) {
        NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
    });
}

運行結果:

ThreadDemo[1461:160567] 第2次_<NSThread: 0x608000076000>{number = 4, name = (null)}
ThreadDemo[1461:160534] 第0次_<NSThread: 0x60800006d8c0>{number = 1, name = main}
ThreadDemo[1461:160566] 第3次_<NSThread: 0x60000007d480>{number = 5, name = (null)}
ThreadDemo[1461:160569] 第1次_<NSThread: 0x60000007d440>{number = 3, name = (null)}
ThreadDemo[1461:160567] 第4次_<NSThread: 0x608000076000>{number = 4, name = (null)}
死鎖:
-(void)GCDApply{
    //重複執行
    dispatch_apply(5, dispatch_get_main_queue(), ^(size_t i) {
        NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
    });
}

運行結果:

6. dispatch_semaphore_create & dispatch_semaphore_signal & dispatch_semaphore_wait

看這幾個函數的時候你需要拋開隊列,丟掉同步異步,不要把它們想到一起,混爲一談,信號量只是控制任務執行的一個條件而已,相對於上面通過隊列以及執行方式來控制線程的開闢和任務的執行,它更貼近對於任務直接的控制。類似於單個隊列的最大併發數的控制機制,提高並行效率的同時,也防止太多線程的開闢對CPU早層負面的效率負擔。
dispatch_semaphore_create創建信號量,初始值不能小於0;
dispatch_semaphore_wait等待降低信號量,也就是信號量-1;
dispatch_semaphore_signal提高信號量,也就是信號量+1;
dispatch_semaphore_waitdispatch_semaphore_signal通常配對使用。
看一下代碼吧,老鐵。

-(void)GCDSemaphore{
    //
    //dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_apply(5, self.concurrentQueue, ^(size_t i) {
        //dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(self.concurrentQueue, ^{
            NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
            //dispatch_semaphore_signal(semaphore);
        });
    });
}

你能猜到運行結果嗎?沒錯,就是你想的這樣,開闢了5個線程執行任務。

ThreadDemo[1970:506692] 第0次_<NSThread: 0x600000070f00>{number = 3, name = (null)}
ThreadDemo[1970:506711] 第1次_<NSThread: 0x6000000711c0>{number = 4, name = (null)}
ThreadDemo[1970:506713] 第2次_<NSThread: 0x6000000713c0>{number = 5, name = (null)}
ThreadDemo[1970:506691] 第3次_<NSThread: 0x600000070f40>{number = 6, name = (null)}
ThreadDemo[1970:506694] 第4次_<NSThread: 0x600000070440>{number = 7, name = (null)}

下一步你一定猜到了,把註釋的代碼打開:

-(void)GCDSemaphore{
    //
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    dispatch_apply(5, self.concurrentQueue, ^(size_t i) {
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        dispatch_async(self.concurrentQueue, ^{
            NSLog(@"第%@次_%@",@(i),[NSThread currentThread]);
            dispatch_semaphore_signal(semaphore);
        });
    });
}

運行結果:

ThreadDemo[2020:513651] 第0次_<NSThread: 0x608000073900>{number = 3, name = (null)}
ThreadDemo[2020:513651] 第1次_<NSThread: 0x608000073900>{number = 3, name = (null)}
ThreadDemo[2020:513651] 第2次_<NSThread: 0x608000073900>{number = 3, name = (null)}
ThreadDemo[2020:513651] 第3次_<NSThread: 0x608000073900>{number = 3, name = (null)}
ThreadDemo[2020:513651] 第4次_<NSThread: 0x608000073900>{number = 3, name = (null)}

很明顯,我開始說的是對的,哈哈哈哈,信號量是控制任務執行的重要條件,當信號量爲0時,所有任務等待,信號量越大,允許可並行執行的任務數量越多。

GCD就先說到這,很多API沒有涉及到,有興趣的同學們可以自己去看看,重要的是方法和習慣,而不是你看過多少。

NSOperation && NSOperationQueue

如果上面的郭草地如果你學會了,那麼這兩個東西你也不一定能學得會!


NSOperation以及NSOperationQueue是蘋果對於GCD的封裝,其中呢,NSOperation其實就是我們上面所說的任務,但是這個類不能直接使用,我們要用他的兩個子類,NSBlockOperationNSInvocationOperation,而NSOperationQueue呢,其實就是類似於GCD中的隊列,用於管理你加入到其中的任務。

NSOperation

它提供了關於任務的執行,取消,以及隨時獲取任務的狀態,添加任務依賴以及優先級等方法和屬性,相對於GCD提供的方法來說,更直觀,更方便,並且提供了更多的控制接口。(很多時候,蘋果設計的架構是很棒的,不要只是在乎他實現了什麼,可能你學到的東西會更多,一不小心又吹牛逼了,哦呵呵),有幾個方法和屬性我們瞭解一下:

@interface NSOperation : NSObject {
@private
    id _private;
    int32_t _private1;
#if __LP64__
    int32_t _private1b;
#endif
}

- (void)start;//啓動任務 默認在當前線程執行
- (void)main;//自定義NSOperation,寫一個子類,重寫這個方法,在這個方法裏面添加需要執行的操作。

@property (readonly, getter=isCancelled) BOOL cancelled;//是否已經取消,只讀
- (void)cancel;//取消任務

@property (readonly, getter=isExecuting) BOOL executing;//正在執行,只讀
@property (readonly, getter=isFinished) BOOL finished;//執行結束,只讀
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);//是否併發,只讀
@property (readonly, getter=isReady) BOOL ready;//準備執行

- (void)addDependency:(NSOperation *)op;//添加依賴
- (void)removeDependency:(NSOperation *)op;//移除依賴

@property (readonly, copy) NSArray<NSOperation *> *dependencies;//所有依賴關係,只讀

typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};//系統提供的優先級關係枚舉

@property NSOperationQueuePriority queuePriority;//執行優先級

@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);//任務執行完成之後的回調

- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);//阻塞當前線程,等到某個operation執行完畢。

@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);//已廢棄,用qualityOfService替代。

@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);//服務質量,一個高質量的服務就意味着更多的資源得以提供來更快的完成操作。

@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);//任務名稱

@end

然而NSOperation本身是個抽象類,不能直接使用,我們有三種方式賦予它新的生命,就是下面這三個東西,您坐穩看好。

NSOperation自定義子類

這是我要說的第一個任務類型,我們可以自定義繼承於NSOperation的子類,並重寫父類提供的方法,實現一波具有特殊意義的任務。比如我們去下載一個圖片:

.h
#import <UIKit/UIKit.h>

@protocol YSImageDownLoadOperationDelegate <NSObject>
-(void)YSImageDownLoadFinished:(UIImage*)image;

@end

@interface YSImageDownLoadOperation : NSOperation

-(id)initOperationWithUrl:(NSURL*)imageUrl delegate:(id<YSImageDownLoadOperationDelegate>)delegate;

@end

.m
#import "YSImageDownLoadOperation.h"

@implementation YSImageDownLoadOperation{
    NSURL *_imageUrl;
    id _delegate;
}

-(id)initOperationWithUrl:(NSURL*)imageUrl delegate:(id<YSImageDownLoadOperationDelegate>)delegate{
    if (self == [super init]) {
        _imageUrl = imageUrl;
        _delegate = delegate;
    }
    return self;
}

-(void)main{
    @autoreleasepool {
        UIImage *image = [self imageWithUrl:_imageUrl];
        if (_delegate && [_delegate respondsToSelector:@selector(YSImageDownLoadFinished:)]) {
            [_delegate YSImageDownLoadFinished:image];
        }
    }
}

-(UIImage*)imageWithUrl:(NSURL*)url{
    NSData *imageData = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:imageData];
    return image;
}


@end

然後調用:
-(void)YSDownLoadImageOperationRun{
    YSImageDownLoadOperation *ysOper = [[YSImageDownLoadOperation alloc] initOperationWithUrl:[NSURL URLWithString:@"http://img5.duitang.com/uploads/item/201206/06/20120606174422_LZSeE.thumb.700_0.jpeg"] delegate:self];
    [ysOper start];
}

-(void)YSImageDownLoadFinished:(UIImage *)image{
    NSLog(@"%@",image);
}

運行打印結果:

ThreadDemo[4141:1100329] <UIImage: 0x60800009f630>, {700, 1050}

哦呵呵,其實自定義的任務更具有指向性,它可以滿足你特定的需求,但是一般用的比較少,不知道是因爲我太菜還是真的有很多更加方便的方法和思路實現這樣的邏輯。

NSBlockOperation

第二個,就是系統提供的NSOperation的子類NSBlockOperation,我們看一下他提供的API:

@interface NSBlockOperation : NSOperation {
@private
    id _private2;
    void *_reserved2;
}

+ (instancetype)blockOperationWithBlock:(void (^)(void))block;

- (void)addExecutionBlock:(void (^)(void))block;
@property (readonly, copy) NSArray<void (^)(void)> *executionBlocks;

@end

很簡單,就這幾個,我們就用它實現一個任務:

-(void)NSBlockOperationRun{
    NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"NSBlockOperationRun_%@_%@",[NSOperationQueue currentQueue],[NSThread currentThread]);
    }];
    [blockOper start];
}

運行結果:

ThreadDemo[4313:1121900] NSBlockOperationRun_<NSOperationQueue: 0x608000037420>{name = 'NSOperationQueue Main Queue'}_<NSThread: 0x60000006dd80>{number = 1, name = main}

我們發現這個任務是在當前線程順序執行的,我們發現還有一個方法addExecutionBlock:試一下:

-(void)NSBlockOperationRun{
    NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"NSBlockOperationRun_1_%@",[NSThread currentThread]);
    }];
    [blockOper addExecutionBlock:^{
        NSLog(@"NSBlockOperationRun_2_%@",[NSThread currentThread]);
    }];
    [blockOper addExecutionBlock:^{
        NSLog(@"NSBlockOperationRun_3_%@",[NSThread currentThread]);
    }];
    [blockOper addExecutionBlock:^{
        NSLog(@"NSBlockOperationRun_4_%@",[NSThread currentThread]);
    }];
    [blockOper start];
}

打印結果:

ThreadDemo[4516:1169835] NSBlockOperationRun_1_<NSThread: 0x60000006d880>{number = 1, name = main}
ThreadDemo[4516:1169875] NSBlockOperationRun_3_<NSThread: 0x600000070800>{number = 4, name = (null)}
ThreadDemo[4516:1169877] NSBlockOperationRun_4_<NSThread: 0x6080000762c0>{number = 5, name = (null)}
ThreadDemo[4516:1169893] NSBlockOperationRun_2_<NSThread: 0x608000076100>{number = 3, name = (null)}

從打印結果來看,這個4個任務是異步併發執行的,開闢了多條線程。

NSInvocationOperation

第三個,就是它了,同樣也是系統提供給我們的一個任務類,基於一個target對象以及一個selector來創建任務,具體代碼:

-(void)NSInvocationOperationRun{
    NSInvocationOperation *invocationOper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperSel) object:nil];
    [invocationOper start];
}
-(void)invocationOperSel{
    NSLog(@"NSInvocationOperationRun_%@",[NSThread currentThread]);
}

運行結果:

ThreadDemo[4538:1173118] NSInvocationOperationRun_<NSThread: 0x60800006e900>{number = 1, name = main}

運行結果與NSBlockOperation單個block函數的執行方式相同,同步順序執行。的確系統的封裝給予我們關於任務更直觀的東西,但是對於多個任務的控制機制並不完善,所以我們有請下一位,也許你會眼前一亮。

NSOperationQueue

上面說道我們創建的NSOperation任務對象可以通過start方法來執行,同樣我們可以把這個任務對象添加到一個NSOperationQueue對象中去執行,好想有好東西,先看一下系統的API:

@interface NSOperationQueue : NSObject {
@private
    id _private;
    void *_reserved;
}

- (void)addOperation:(NSOperation *)op;//添加任務
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);//添加一組任務

- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);//添加一個block形式的任務

@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;//隊列中所有的任務數組
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);//隊列中的任務數

@property NSInteger maxConcurrentOperationCount;//最大併發數

@property (getter=isSuspended) BOOL suspended;//暫停

@property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);//名稱

@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);//服務質量,一個高質量的服務就意味着更多的資源得以提供來更快的完成操作。

@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);

- (void)cancelAllOperations;//取消隊列中的所有任務

- (void)waitUntilAllOperationsAreFinished;//阻塞當前線程,等到隊列中的任務全部執行完畢。

#if FOUNDATION_SWIFT_SDK_EPOCH_AT_LEAST(8)
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0);//獲取當前隊列
@property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);//獲取主隊列
#endif

@end

來一段代碼開心開心:

-(void)NSOperationQueueRun{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSInvocationOperation *invocationOper = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invocationOperSel) object:nil];
    [queue addOperation:invocationOper];
    NSBlockOperation *blockOper = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"NSBlockOperationRun_%@",[NSThread currentThread]);
    }];
    [queue addOperation:blockOper];
    [queue addOperationWithBlock:^{
        NSLog(@"QUEUEBlockOperationRun_%@",[NSThread currentThread]);
    }];
}

打印結果:

ThreadDemo[4761:1205689] NSBlockOperationRun_<NSThread: 0x600000264480>{number = 4, name = (null)}
ThreadDemo[4761:1205691] NSInvocationOperationRun_<NSThread: 0x600000264380>{number = 3, name = (null)}
ThreadDemo[4761:1205706] QUEUEBlockOperationRun_<NSThread: 0x6000002645c0>{number = 5, name = (null)}

我們發現,加入隊列之後不用調用任務的start方法,隊列會幫你管理任務的執行情況。上訴執行結果說明這些任務在隊列中爲併發執行的。

下面我們改變一下任務的優先級:
invocationOper.queuePriority = NSOperationQueuePriorityVeryLow;

運行結果:

ThreadDemo[4894:1218440] QUEUEBlockOperationRun_<NSThread: 0x608000268880>{number = 3, name = (null)}
ThreadDemo[4894:1218442] NSBlockOperationRun_<NSThread: 0x60000026d340>{number = 4, name = (null)}
ThreadDemo[4894:1218457] NSInvocationOperationRun_<NSThread: 0x60000026d400>{number = 5, name = (null)}

我們發現優先級低的任務會後執行,但是,這並不是絕對的,還有很多東西可以左右CPU分配,以及操作系統對於任務和線程的控制,只能說,優先級會在一定程度上讓優先級高的任務開始執行。同時,優先級只對同一隊列中的任務有效哦。下面我們就看一個會忽視優先級的情況。

添加依賴關係
-(void)NSOperationQueueRun{
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    NSBlockOperation *blockOper_1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 1000; i++) {
            NSLog(@"blockOper_1_%@_%@",@(i),[NSThread currentThread]);
        }
    }];
    
    NSBlockOperation *blockOper_2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 1000; i++) {
            NSLog(@"blockOper_2_%@_%@",@(i),[NSThread currentThread]);
        }
    }];
    
    [blockOper_1 addDependency:blockOper_2];
    [queue addOperation:blockOper_1];
    [queue addOperation:blockOper_2];
}

打印結果:

ThreadDemo[5066:1233824] blockOper_2_0_<NSThread: 0x600000078340>{number = 3, name = (null)}
ThreadDemo[5066:1233824] blockOper_2_1_<NSThread: 0x600000078340>{number = 3, name = (null)}
ThreadDemo[5066:1233824] blockOper_2_2_<NSThread: 0x600000078340>{number = 3, name = (null)}
ThreadDemo[5066:1233824] blockOper_2_3_<NSThread: 0x600000078340>{number = 3, name = (null)}
... ...
ThreadDemo[5066:1233824] blockOper_2_999_<NSThread: 0x600000078340>{number = 3, name = (null)}
ThreadDemo[5066:1233822] blockOper_1_0_<NSThread: 0x60000006ae80>{number = 4, name = (null)}
... ...
ThreadDemo[5066:1233822] blockOper_1_997_<NSThread: 0x60000006ae80>{number = 4, name = (null)}
ThreadDemo[5066:1233822] blockOper_1_998_<NSThread: 0x60000006ae80>{number = 4, name = (null)}
ThreadDemo[5066:1233822] blockOper_1_999_<NSThread: 0x60000006ae80>{number = 4, name = (null)}

通過打印結果我們可以看到,添加依賴之後,依賴任務必須等待被依賴任務執行完畢之後纔會開始執行。⚠️,就算依賴任務的優先級再高,也是被依賴任務先執行,同時,和優先級不同,依賴關係不受隊列的侷限,愛哪哪,只要是我依賴於你,那你必須先執行完,我才執行。

隊列的最大併發數

就是說,這個隊列最多可以有多少任務同時執行,或者說最多開闢多少條線程,如果設置爲1,那就一次只能執行一個任務,但是,不要以爲這和GCD的串行隊列一樣,就算最大併發數爲1,隊列任務的執行順序依然取決於很多因素。

關於NSOperationQueue還有取消啊,暫停啊等操作方式,大家可以試一下,應該注意的是,和學習GCD的方式不同,不要總是站在面向過程的角度看帶這些面向對象的類,因爲它的面相對象化的封裝過程中,肯定有很多你看不到的面相過程的操作,所以你也沒有必要用使用GCD的思想來套用它,否則你可能會迷糊的一塌糊塗。

線程鎖

上面算是把多線程操作的方法講完了,下面說一下線程鎖機制。多線程操作是多個線程並行的,所以同一塊資源可能在同一時間被多個線程訪問,舉爛的例子就是買火車票,在就剩一個座時,如果100個線程同時進入,那麼可能上火車時就有人得幹仗了。爲了維護世界和平,人民安定,所以我們講一下這個線程鎖。我們先實現一段代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.sourceArray_m = [NSMutableArray new];
    [_sourceArray_m addObjectsFromArray:@[@"1",@"2",@"3",@"4",@"5",@"6"]];
    [self threadLock];
}
-(void)threadLock{
    for (int i = 0; i < 8; i++) {
        dispatch_async(self.concurrentQueue, ^{
            NSLog(@"%@",[self sourceOut]) ;
        });
    }
}

-(NSString*)sourceOut{
    NSString *source = @"沒有了,取光了";
    if (_sourceArray_m.count > 0) {
        source = [_sourceArray_m lastObject];
        [_sourceArray_m removeLastObject];
    }
    return source;
}

運行打印結果:

ThreadDemo[5540:1291666] 6
ThreadDemo[5540:1291669] 6
ThreadDemo[5540:1291682] 5
ThreadDemo[5540:1291667] 4
ThreadDemo[5540:1291683] 3
ThreadDemo[5540:1291666] 2
ThreadDemo[5540:1291669] 1
ThreadDemo[5540:1291682] 沒有了,取光了

我們發現6被取出來兩次(因爲代碼簡單,執行效率較快,所以這種情況不實必現,耐心多試幾次),這樣的話就尷尬了,一張票賣了2次,這麼惡劣的行爲是不可能容忍的,所以我們需要正義的衛士——線程鎖,我們就講最直接的兩種(之前說的GCD的很多方法同樣可以等價於線程鎖解決這些問題):

NSLock

代碼這樣寫:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.lock = [[NSLock alloc] init];
    self.sourceArray_m = [NSMutableArray new];
    [_sourceArray_m addObjectsFromArray:@[@"1",@"2",@"3",@"4",@"5",@"6"]];
    [self threadLock];
}
-(void)threadLock{
    for (int i = 0; i < 8; i++) {
        dispatch_async(self.concurrentQueue, ^{
            NSLog(@"%@",[self sourceOut]) ;
        });
    }
}
-(NSString*)sourceOut{
    NSString *source = @"沒有了,取光了";
    [_lock lock];
    if (_sourceArray_m.count > 0) {
        source = [_sourceArray_m lastObject];
        [_sourceArray_m removeLastObject];
    }
    [_lock unlock];
    return source;
}

運行結果:

ThreadDemo[5593:1298144] 5
ThreadDemo[5593:1298127] 6
ThreadDemo[5593:1298126] 4
ThreadDemo[5593:1298129] 3
ThreadDemo[5593:1298146] 2
ThreadDemo[5593:1298144] 1
ThreadDemo[5593:1298127] 沒有了,取光了
ThreadDemo[5593:1298147] 沒有了,取光了

這樣就保證了被Lock的資源只能同時讓一個線程進行訪問,從而也就保證了線程安全。

@synchronized

這個也很簡單,有時候也會用到這個,要傳入一個同步對象(一般就是self),然後將你需要加鎖的資源放入代碼塊中,如果該資源有線程正在訪問時,會讓其他線程等待,直接上代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.sourceArray_m = [NSMutableArray new];
    [_sourceArray_m addObjectsFromArray:@[@"1",@"2",@"3",@"4",@"5",@"6"]];
    [self threadLock];
}
-(void)threadLock{
    for (int i = 0; i < 8; i++) {
        dispatch_async(self.concurrentQueue, ^{
            NSLog(@"%@",[self sourceOut]) ;
        });
    }
}

-(NSString*)sourceOut{
    NSString *source = @"沒有了,取光了";
    @synchronized (self) {
        if (_sourceArray_m.count > 0) {
            source = [_sourceArray_m lastObject];
            [_sourceArray_m removeLastObject];
        }
    }
    return source;
}

運行結果:

ThreadDemo[5625:1301834] 5
ThreadDemo[5625:1301835] 6
ThreadDemo[5625:1301837] 4
ThreadDemo[5625:1301852] 3
ThreadDemo[5625:1301834] 1
ThreadDemo[5625:1301854] 2
ThreadDemo[5625:1301835] 沒有了,取光了
ThreadDemo[5625:1301855] 沒有了,取光了

結語

看來該結束了!!!就到這吧,小弟已經盡力了,帶大家入個門,這條路小弟只能陪你走到這了。

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