iOS多線程

進程與線程

進程:計算機操作系統(tǒng)分配資源的單位塑径,是指系統(tǒng)中正在運行的應用程序衔彻,進程之間相互獨立,運行在受保護的內存空間幅疼,比如同時打開XCode米奸、QQ,系統(tǒng)就會啟動兩個進程爽篷;

線程:進程的基本執(zhí)行單元悴晰,一個進程中的任務都在線程中執(zhí)行;

并發(fā)與并行

并發(fā):并發(fā)的關鍵是具有處理多個任務的能力逐工,不一定要同時铡溪;

并行:并行的關鍵是你有同時處理多個任務的能力。

你吃飯吃到一半泪喊,電話來了棕硫,你一直到吃完了以后才去接,這就說明你不支持并發(fā)也不支持并行袒啼。
你吃飯吃到一半哈扮,電話來了,你一手筷子蚓再,一手電話滑肉,說一句話,咽一口飯摘仅。這說明你支持并發(fā)靶庙。
你吃飯吃到一半,電話來了娃属,你一邊打電話一邊吃飯六荒,這就需要兩張嘴,也就是多核CPU矾端,這說明你支持并行掏击。

同步與異步

同步方法就是我們平時調用的哪些方法。因為任何有編程經(jīng)驗的人都知道秩铆,比如在第一行調用foo()方法铐料,那么程序運行到第二行的時候,foo方法肯定是執(zhí)行完了豺旬。

所謂的異步钠惩,就是允許在執(zhí)行某一個任務時,函數(shù)立刻返回族阅,但是真正要執(zhí)行的任務稍后完成篓跛。比如我們在點擊保存按鈕之后,要先把數(shù)據(jù)寫到磁盤坦刀,然后更新UI愧沟。同步方法就是等到數(shù)據(jù)保存完再更新UI蔬咬,而異步則是立刻從保存數(shù)據(jù)的方法返回并向后執(zhí)行代碼,同時真正用來保存數(shù)據(jù)的指令將在稍后執(zhí)行沐寺。

多線程優(yōu)缺點

  • 優(yōu)點:能適當提高程序的執(zhí)行效率林艘,能適當提高資源利用率(CPU、內存利用率)
  • 缺點:創(chuàng)建線程是有開銷的混坞,iOS下主要成本包括:內核數(shù)據(jù)結構(大約1KB)狐援、棧空間(子線程512KB究孕、主線程1MB啥酱,也可以使用-setStackSize:設置,但必須是4K的倍數(shù)厨诸,而且最小是16K)镶殷,創(chuàng)建線程大約需要90毫秒的創(chuàng)建時間
    如果開啟大量的線程,會降低程序的性能微酬,線程越多绘趋,CPU在調度線程上的開銷就越大。
    程序設計更加復雜:比如線程之間的通信颗管、多線程的數(shù)據(jù)共享等問題埋心。

iOS中多線程解決方案

1. pthread

pthread 是一套通用的多線程的 API,可以在Unix / Linux / Windows 等系統(tǒng)跨平臺使用忙上,使用 C 語言編寫,需要程序員自己管理線程的生命周期闲坎,使用較為復雜疫粥,我們在 iOS 開發(fā)中幾乎不使用 pthread,我們可以稍作了解腰懂。

2. NSThread

NSThread 是蘋果官方提供的梗逮,使用起來比 pthread 更加面向對象,簡單易用绣溜,可以直接操作線程對象慷彤。不過也需要需要程序員自己管理線程的生命周期(主要是創(chuàng)建),我們在開發(fā)的過程中偶爾使用 NSThread怖喻。比如我們會經(jīng)常調用[NSThread currentThread]來顯示當前的進程信息底哗。

創(chuàng)建方式

  • 先創(chuàng)建再啟動

      NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething1:) object:@"NSThread1"];
      [thread1 start];
    
  • 創(chuàng)建線程后自動啟動

      [NSThread detachNewThreadSelector:@selector(doSomething2:) toTarget:self withObject:@"NSThread2"];
    
  • 隱式創(chuàng)建線程,直接啟動

      [self performSelectorInBackground:@selector(doSomething3:) withObject:@"NSThread3"];
    

相關方法

  • 類方法

      // 當前線程
      [NSThread currentThread];
      // 打印結果:{number = 1, name = main}
      NSLog(@"%@",[NSThread currentThread]);
      //休眠多久
      [NSThread sleepForTimeInterval:2];
      //休眠到指定時間
      [NSThread sleepUntilDate:[NSDate date]];
      //退出線程
      [NSThread exit];
      //判斷當前線程是否為主線程
      [NSThread isMainThread];
      //判斷當前線程是否是多線程
      [NSThread isMultiThreaded];
      //主線程的對象
      NSThread *mainThread = [NSThread mainThread];
    
  • 實例方法

      //線程是否在執(zhí)行
      thread.isExecuting;
      //線程是否被取消
      thread.isCancelled;
      //線程是否完成
      thread.isFinished;
      //是否是主線程
      thread.isMainThread;
      //線程的優(yōu)先級,取值范圍0.0到1.0锚沸,默認優(yōu)先級0.5跋选,1.0表示最高優(yōu)先級,優(yōu)先級高哗蜈,CPU調度的頻率高
      thread.threadPriority;
    

線程間通信

在開發(fā)中前标,線程往往不是孤立存在的坠韩,多個線程之間需要經(jīng)常進行通信我們經(jīng)常會在子線程進行耗時操作,操作結束后再回到主線程去刷新 UI炼列。這就涉及到了子線程和主線程之間的通信只搁。我們先來了解一下官方關于 NSThread 的線程間通信的方法。

// 在主線程上執(zhí)行操作
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array;
  // equivalent to the first method with kCFRunLoopCommonModes

// 在指定線程上執(zhí)行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

// 在當前線程上執(zhí)行操作俭尖,調用 NSObject 的 performSelector:相關方法
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

下面通過一個經(jīng)典的下載圖片 DEMO 來展示線程之間的通信氢惋。具體步驟如下:

1.開啟一個子線程亏狰,在子線程中下載圖片跌帐。
2.回到主線程刷新 UI,將圖片展示在 UIImageView 中簸州。
代碼如下:

/**
 * 創(chuàng)建一個線程下載圖片
 */
- (void)downloadImageOnSubThread {
    // 在創(chuàng)建的子線程中調用downloadImage下載圖片
    [NSThread detachNewThreadSelector:@selector(downloadImage) toTarget:self withObject:nil];
}

/**
 * 下載圖片缭付,下載完之后回到主線程進行 UI 刷新
 */
- (void)downloadImage {
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    // 1. 獲取圖片 imageUrl
    NSURL *imageUrl = [NSURL URLWithString:@"https://ysc-demo-1254961422.file.myqcloud.com/YSC-phread-NSThread-demo-icon.jpg"];
    
    // 2. 從 imageUrl 中讀取數(shù)據(jù)(下載圖片) -- 耗時操作
    NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
    // 通過二進制 data 創(chuàng)建 image
    UIImage *image = [UIImage imageWithData:imageData];
    
    // 3. 回到主線程進行圖片賦值和界面刷新
    [self performSelectorOnMainThread:@selector(refreshOnMainThread:) withObject:image waitUntilDone:YES];
}

/**
 * 回到主線程進行圖片賦值和界面刷新
 */
- (void)refreshOnMainThread:(UIImage *)image {
    NSLog(@"current thread -- %@", [NSThread currentThread]);
    
    // 賦值圖片到imageview
    self.imageView.image = image;
}
線程狀態(tài)

線程安全

多線程安全隱患的原因:一塊資源可能會被多個線程共享柿估,也就是多個線程可能會訪問同一塊資源。當多個線程訪問同一塊資源時陷猫,很容易引發(fā)數(shù)據(jù)錯亂和數(shù)據(jù)安全問題秫舌。就好比幾個人在同一時修改同一個表格,造成數(shù)據(jù)的錯亂绣檬。
解決方法:

  1. 添加互斥鎖:

     @synchronized(鎖對象) {
     // 需要鎖定的代碼
      }
    

iOS 實現(xiàn)線程加鎖有很多種方式足陨。@synchronized、 NSLock娇未、NSRecursiveLock墨缘、NSCondition、NSConditionLock等等各種方式零抬。判斷的時候鎖對象要存在镊讼,如果代碼中只有一個地方需要加鎖,大多都使用self作為鎖對象平夜,這樣可以避免單獨再創(chuàng)建一個鎖對象蝶棋。加了互斥做的代碼,當新線程訪問時忽妒,如果發(fā)現(xiàn)其他線程正在執(zhí)行鎖定的代碼玩裙,新線程就會進入休眠。

  1. 自旋鎖:
    加了自旋鎖段直,當新線程訪問代碼時吃溅,如果發(fā)現(xiàn)有其他線程正在鎖定代碼,新線程會用死循環(huán)的方式鸯檬,一直等待鎖定的代碼執(zhí)行完成罕偎。相當于不停嘗試執(zhí)行代碼,比較消耗性能京闰。
    屬性修飾atomic本身就有一把自旋鎖:
nonatomic 非原子屬性,同一時間可以有很多線程讀和寫
atomic 原子屬性(線程安全)颜及,保證同一時間只有一個線程能夠寫入(但是同一個時間多個線程都可以取值)甩苛,atomic 本身就有一把鎖(自旋鎖)
atomic:線程安全,需要消耗大量的資源
nonatomic:非線程安全俏站,不過效率更高讯蒲,一般使用nonatomic

下面通過一個售票實例來看一下鎖的作用:

#import "ViewController.h"

@interface ViewController ()

@property(nonatomic,strong)NSThread *thread01;
@property(nonatomic,strong)NSThread *thread02;
@property(nonatomic,strong)NSThread *thread03;
@property(nonatomic,assign)NSInteger numTicket;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // 總票數(shù)為30
    self.numTicket = 30;
    self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread01.name = @"售票員01";
    self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread02.name = @"售票員02";
    self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
    self.thread03.name = @"售票員03";
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self.thread01 start];
    [self.thread02 start];
    [self.thread03 start];
}
// 售票
-(void)saleTicket
{
    while (1) {
        // 鎖對象,本身就是一個對象肄扎,所以self就可以了
        // 鎖定的時候墨林,其他線程沒有辦法訪問這段代碼
        @synchronized (self) {
            // 模擬售票時間,我們讓線程休息0.05s 
            [NSThread sleepForTimeInterval:0.05];
            if (self.numTicket > 0) {
                self.numTicket -= 1;
                NSLog(@"%@賣出了一張票犯祠,還剩下%zd張票",[NSThread currentThread].name,self.numTicket);
            }else{
                NSLog(@"票已經(jīng)賣完了");
                break;
            }
        }
    }
}
@end
加鎖前

我們可以看到?jīng)]有加鎖時有的票被多賣了旭等,顯然不對,接下來看看加鎖的結果:


加鎖后

加上互斥鎖后衡载,就不會出現(xiàn)數(shù)據(jù)錯亂的情況了搔耕。

GCD

GCD是蘋果公司為多核的并行運算提出的解決方案,它可以自動管理線程的生命周期(創(chuàng)建線程痰娱、調度任務弃榨、銷毀線程),我們只需要告訴GCD想要執(zhí)行什么任務梨睁,不需要編寫任何線程管理代碼鲸睛。

GCD中的任務與隊列

任務:GCD以block為基本單位,一個block中的代碼可以為一個任務坡贺。
任務有兩種執(zhí)行方式: 同步函數(shù) 和 異步函數(shù)官辈,他們之間的區(qū)別是:

  • 同步:只能在當前線程中執(zhí)行任務,不具備開啟新線程的能力遍坟,任務立刻馬上執(zhí)行拳亿,會阻塞當前線程并等待 Block中的任務執(zhí)行完畢,然后當前線程才會繼續(xù)往下運行
  • 異步:可以在新的線程中執(zhí)行任務政鼠,具備開啟新線程的能力,但不一定會開新線程队魏,當前線程會直接往下執(zhí)行公般,不會阻塞當前線程

隊列:裝載線程任務的隊形結構。(系統(tǒng)以先進先出的方式調度隊列中的任務執(zhí)行)胡桨。在GCD中有兩種隊列:串行隊列和并發(fā)隊列官帘。

  • 串行隊列(Serial Dispatch Queue):讓任務一個接著一個地執(zhí)行(一個任務執(zhí)行完畢后,再執(zhí)行下一個任務)
  • 并發(fā)隊列(Concurrent Dispatch Queue):可以讓多個任務并發(fā)(同時)執(zhí)行(自動開啟多個線程同時執(zhí)行任務)昧谊,并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效刽虹。

GCD的使用分為兩步:

  1. 添加任務;
  2. 將任務放到指定的隊列中呢诬,GCD自動將任務取出放到對應的線程中執(zhí)行涌哲。
GCD的創(chuàng)建
  1. 創(chuàng)建隊列
    使用dispatch_queue_create來創(chuàng)建隊列對象胖缤,傳入兩個參數(shù),第一個參數(shù)表示隊列的唯一標識符阀圾,可為空哪廓。第二個參數(shù)用來表示隊列的類型,串行隊列(DISPATCH_QUEUE_SERIAL)或并發(fā)隊列(DISPATCH_QUEUE_CONCURRENT)初烘。
    串行隊列:
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);

并發(fā)隊列:

dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);

全局并發(fā)隊列:GCD默認已經(jīng)提供了全局并發(fā)隊列涡真,供整個應用使用,可以無需手動創(chuàng)建:

 /** 
     第一個參數(shù):優(yōu)先級 也可直接填后面的數(shù)字
     #define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
     #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默認
     #define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
     #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后臺
     第二個參數(shù): 預留參數(shù)  0
     */
    dispatch_queue_t quque1 =  dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

主隊列:GCD 提供了的一種特殊的串行隊列肾筐,主隊列負責在主線程上調度任務哆料,如果在主線程上已經(jīng)有任務正在執(zhí)行,主隊列會等到主線程空閑后再調度任務吗铐。通常是返回主線程更新UI的時候使用东亦。dispatch_get_main_queue():

dispatch_async(dispatch_get_global_queue(0, 0), ^{
      // 耗時操作放在這里

      dispatch_async(dispatch_get_main_queue(), ^{
          // 回到主線程進行UI操作

      });
  });
  1. 執(zhí)行任務
    同步(Synchronize)使用dispatch_sync;
/*
     第一個參數(shù):隊列
     第二個參數(shù):block,在里面封裝任務
     */
    dispatch_sync(queue, ^{
        
    });

異步(asynchronous)使用dispatch_async抓歼;

dispatch_async(queue, ^{

    });

GCD的使用:隊列和任務的組合

組合使用

當在主隊列中加入同步函數(shù)的時候讥此,會造成死鎖。

//1.獲得主隊列
dispatch_queue_t queue =  dispatch_get_main_queue();
//2.同步函數(shù)
dispatch_sync(queue, ^{
       NSLog(@"---download1---%@",[NSThread currentThread]);
});

主隊列在執(zhí)行dispatch_sync谣妻,隨后隊列中新增一個任務block萄喳。因為主隊列是同步隊列,所以block要等dispatch_sync執(zhí)行完才能執(zhí)行蹋半,但是dispatch_sync是同步派發(fā)他巨,要等block執(zhí)行完才算是結束。在主隊列中的兩個任務互相等待减江,導致了死鎖染突。
解決方案:其實在通常情況下我們不必要用dispatch_sync,因為dispatch_async能夠更好的利用CPU辈灼,提升程序運行速度份企。只有當我們需要保證隊列中的任務必須順序執(zhí)行時,才考慮使用dispatch_sync巡莹。在使用dispatch_sync的時候應該分析當前處于哪個隊列司志,以及任務會提交到哪個隊列。
注意:GCD中開多少條線程是由系統(tǒng)根據(jù)CUP繁忙程度決定的降宅,如果任務很多骂远,GCD會開啟適當?shù)淖泳€程,并不會讓所有任務同時執(zhí)行腰根。

  • GCD線程間的通信非常簡單激才,使用同步或異步函數(shù),傳入主隊列即可(就像上面介紹主隊列時那樣):
-(void)downloadImage
{
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        // 獲得圖片URL
        NSURL *url = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
        // 將圖片URL下載為二進制文件
        NSData *data = [NSData dataWithContentsOfURL:url];
        // 將二進制文件轉化為image
        UIImage *image = [UIImage imageWithData:data];
        NSLog(@"%@",[NSThread currentThread]);
        // 返回主線程 這里用同步函數(shù)不會發(fā)生死鎖,因為這個方法在子線程中被調用瘸恼。
        // 也可以使用異步函數(shù)
        dispatch_sync(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@",[NSThread currentThread]);
        });
    }); 
}

GCD其他常用方法

1. 柵欄函數(shù)(控制任務的執(zhí)行順序)

當任務需要異步進行劣挫,但是這些任務需要分成兩組來執(zhí)行,第一組完成之后才能進行第二組的操作钞脂。這時候就用了到GCD的柵欄方法dispatch_barrier_async:

-(void)barrier
{
    //1.創(chuàng)建隊列(并發(fā)隊列)
    dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
        }
    });
    //柵欄函數(shù)
    dispatch_barrier_async(queue, ^{
        NSLog(@"這是一個柵欄函數(shù)揣云,34任務在12之后進行");
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
        for (NSInteger i = 0; i<3; i++) {
            NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
        }
    });
}
輸出結果
2. 延遲執(zhí)行
/*
     第一個參數(shù):延遲時間
     第二個參數(shù):要執(zhí)行的代碼
     如果想讓延遲的代碼在子線程中執(zhí)行,也可以更改在哪個隊列中執(zhí)行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
     */
     dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"---%@",[NSThread currentThread]);
    });

當然冰啃,除了GCD以外我們還有其他的方法:

[self performSelector:@selector(doSomething) withObject:nil afterDelay:2.0];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(doSomething) userInfo:nil repeats:YES];
3.使代碼只執(zhí)行一次
//整個程序運行過程中只會執(zhí)行一次
//onceToken用來記錄該部分的代碼是否被執(zhí)行過
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"-----");
});

這個用法一般用在單例模式中邓夕。

4.dispatch_apply(快速迭代)

dispatch_apply函數(shù)是dispatch_sync函數(shù)和Dispatch Group的關聯(lián)API,該函數(shù)按指定的次數(shù)將指定的Block追加到指定的Dispatch Queue中,并等到全部的處理執(zhí)行結束:

- (void)dispatchApplyTest1 {
    //生成全局隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    /**
     *  @param 10    指定重復次數(shù)  指定10次
     *  @param queue 追加對象的Dispatch Queue
     *  @param index 帶有參數(shù)的Block, index的作用是為了按執(zhí)行的順序區(qū)分各個Block
     *
     */
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"%zu-----%@", index, [NSThread currentThread]);
    });
    NSLog(@"finished");
}
打印結果

可以看到該函數(shù)開啟了多個線程執(zhí)行block里的操作,我們可以利用這個特性模擬循環(huán)完成快速迭代遍歷(無序):

- (void)dispatchApplyTest2 {
    //1.創(chuàng)建NSArray類對象
    NSArray *array = @[@"a", @"b", @"c", @"d", @"e", @"f", @"g", @"h", @"i", @"j"];
    
    //2.創(chuàng)建一個全局隊列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //3.通過dispatch_apply函數(shù)對NSArray中的全部元素進行處理,并等待處理完成,
    dispatch_apply([array count], queue, ^(size_t index) {
        NSLog(@"%zu: %@", index, [array objectAtIndex:index]);
    });
    NSLog(@"finished");
}
隊列組

異步執(zhí)行幾個耗時操作阎毅,當這幾個操作都完成之后再回到主線程進行操作焚刚,這是就要用到隊列組了,隊列組可以用來管理隊列中任務的執(zhí)行扇调。

// 創(chuàng)建隊列組
    dispatch_group_t group = dispatch_group_create();
    // 創(chuàng)建并行隊列 
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 執(zhí)行隊列組任務
    dispatch_group_async(group, queue, ^{   
    });
    //隊列組中的任務執(zhí)行完畢之后矿咕,執(zhí)行該函數(shù)
    dispatch_group_notify(group, queue, ^{
    });

將兩張圖片分別下載完成后,合成一張圖片并顯示的例子:

-(void)GCDGroup
{
    //下載圖片1
    //創(chuàng)建隊列組
    dispatch_group_t group =  dispatch_group_create();
    //1.開子線程下載圖片
    //創(chuàng)建隊列(并發(fā))
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_group_async(group, queue, ^{
        //1.獲取url地址
        NSURL *url = [NSURL URLWithString:@"https://upload-images.jianshu.io/upload_images/1689172-61b8a20c108f539d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
        //2.下載圖片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二進制數(shù)據(jù)轉換成圖片
        self.image1 = [UIImage imageWithData:data];
        NSLog(@"1---%@",self.image1);
    });
    //下載圖片2
    dispatch_group_async(group, queue, ^{
        //1.獲取url地址
        NSURL *url = [NSURL URLWithString:@"https://upload-images.jianshu.io/upload_images/1689172-2a0505c7992fd970.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
        //2.下載圖片
        NSData *data = [NSData dataWithContentsOfURL:url];
        //3.把二進制數(shù)據(jù)轉換成圖片
        self.image2 = [UIImage imageWithData:data];
        NSLog(@"2---%@",self.image2);
    });
    //合成狼钮,隊列組執(zhí)行完畢之后執(zhí)行
    dispatch_group_notify(group, queue, ^{
        //開啟圖形上下文
        UIGraphicsBeginImageContext(CGSizeMake(200, 200));
        //畫1
        [self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
        //畫2
        [self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
        //根據(jù)圖形上下文拿到圖片
        UIImage *image =  UIGraphicsGetImageFromCurrentImageContext();
        //關閉上下文
        UIGraphicsEndImageContext();
        //回到主線程刷新UI
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
            NSLog(@"%@--刷新UI",[NSThread currentThread]);
        });
    });
}
GCD信號量(dispatch_semaphore)

信號量:就是一種可用來控制訪問資源的數(shù)量的標識碳柱,設定了一個信號量,在線程訪問之前熬芜,加上信號量的處理莲镣,則可告知系統(tǒng)按照我們指定的信號量數(shù)量來執(zhí)行多個線程。其實涎拉,這有點類似鎖機制了瑞侮,只不過信號量都是系統(tǒng)幫助我們處理了,我們只需要在執(zhí)行線程之前鼓拧,設定一個信號量值半火,并且在使用時,加上信號量處理方法就行了季俩。主要有三個方法:

//創(chuàng)建信號量钮糖,參數(shù):信號量的初值,如果小于0則會返回NULL
dispatch_semaphore_create(long value)
 
//等待,降低信號量
dispatch_semaphore_wait(dispatch_semaphore_t semaphore, dispatch_time_t timeout)
 
//提高信號量,這個函數(shù)會使傳入的信號量dsema的值加1
dispatch_semaphore_signal(dispatch_semaphore_t semaphore)

關于信號量酌住,可以用停車來比喻店归。
停車場剩余4個車位,那么即使同時來了四輛車也能停的下赂韵。如果此時來了五輛車娱节,那么就有一輛需要等待挠蛉。
信號量的值就相當于剩余車位的數(shù)目祭示,dispatch_semaphore_wait函數(shù)就相當于來了一輛車,dispatch_semaphore_signal
就相當于走了一輛車。停車位的剩余數(shù)目在初始化的時候就已經(jīng)指明了(dispatch_semaphore_create(long value))质涛,調用一次dispatch_semaphore_signal稠歉,剩余的車位就增加一個;調用一次dispatch_semaphore_wait剩余車位就減少一個汇陆;當剩余車位為0時怒炸,再來車(即調用dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位毡代。有些車主沒有耐心阅羹,給自己設定了一段等待時間,這段時間內等不到停車位就走了教寂,如果等到了就開進去停車捏鱼。而有些車主就像把車停在這,所以就一直等下去酪耕。
我們看個例子导梆,假設現(xiàn)在系統(tǒng)有兩個空閑資源可以被利用,但同一時間卻有三個線程要進行訪問迂烁,這時利用GCD信號量代碼如下:

-(void)dispatchSignal{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(2);
    dispatch_queue_t quene = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //任務1
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 1");
        sleep(1);
        NSLog(@"complete task 1");
        dispatch_semaphore_signal(semaphore);
    });
    //任務2
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 2");
        sleep(1);
        NSLog(@"complete task 2");
        dispatch_semaphore_signal(semaphore);
    });
    //任務3
    dispatch_async(quene, ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        NSLog(@"run task 3");
        sleep(1);
        NSLog(@"complete task 3");
        dispatch_semaphore_signal(semaphore);
    });
}
結果

我們可以看到任務1和任務3首先搶到了這兩塊資源看尼,有任務完成后才輪到任務二。
接下來我們還是以售賣車票為例盟步,用信號量怎么實現(xiàn)加鎖功能:

dispatch_semaphore_t semaphore;
- (void)viewDidLoad {
    [super viewDidLoad];
    // 總票數(shù)為30
    self.numTicket = 35;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread currentThread].name = @"售票員1";
        [self saleTicket];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread currentThread].name = @"售票員2";
        [self saleTicket];
    });
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread currentThread].name = @"售票員3";
        [self saleTicket];
    });
    semaphore = dispatch_semaphore_create(1);
}

// 售票
-(void)saleTicket
{
    while (1) {
        
        [NSThread sleepForTimeInterval:0.05];
        if (self.numTicket > 0) {
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            self.numTicket -= 1;
            NSLog(@"%@賣出了一張票藏斩,還剩下%zd張票",[NSThread currentThread].name,self.numTicket);
        }else{
            NSLog(@"票已經(jīng)賣完了");
            break;
        }
        dispatch_semaphore_signal(semaphore);
    }
}

信號量屬于底層工具。它非常強大址芯,但在多數(shù)需要使用它的場合灾茁,最好從設計角度重新考慮,看是否可以不用谷炸。應該優(yōu)先考慮是否可以使用諸如操作隊列這樣的高級工具北专。通常可以通過增加一個分派隊列dispatch_suspend旬陡,或者通過其他方式分解操作來避免使用信號量拓颓。信號量并非不好,只是它本身是鎖描孟,能不用鎖就不要用驶睦。盡量用cocoa框架中的高級抽象,信號量非常接近底層匿醒。但有時候场航,例如需要把異步任務轉換為同步任務時,信號量是最合適的工具廉羔。

NSOperation

NSOperation 是蘋果公司對 GCD 的封裝溉痢,完全面向對象,并比GCD多了一些更簡單實用的功能。NSOperation需要配合NSOperationQueue來實現(xiàn)多線程孩饼。NSOperation 和NSOperationQueue 分別對應 GCD 的 任務 和 隊列髓削。
使用步驟:

  1. 將需要執(zhí)行的操作封裝到一個NSOperation對象中;
  2. 將NSOperation對象添加到NSOperationQueue中镀娶,系統(tǒng)會自動將NSOperationQueue中的NSOperation取出來立膛,并將取出的NSOperation封裝的操作放到一條新線程中執(zhí)行。

NSOperation是個抽象類梯码,實際運用時中需要使用它的子類宝泵,有三種方式:

  1. 使用子類NSInvocationOperation
  2. 使用子類NSBlockOperation
  3. 定義繼承自NSOperation的子類,通過實現(xiàn)內部相應的方法來封裝任務轩娶。
NSOperation 的創(chuàng)建
  1. NSInvocationOperation
/*
     第一個參數(shù):目標對象
     第二個參數(shù):選擇器,要調用的方法
     第三個參數(shù):方法要傳遞的參數(shù)
     */
NSInvocationOperation *op  = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//啟動操作
[op start];
  1. NSBlockOperation
//1.封裝操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
       //要執(zhí)行的操作鲁猩,在主線程中執(zhí)行
       NSLog(@"1------%@",[NSThread currentThread]); 
}];
//2.追加操作,追加的操作在子線程中執(zhí)行罢坝,可以追加多條操作
[op addExecutionBlock:^{
        NSLog(@"---download2--%@",[NSThread currentThread]);
    }];
[op start];

NSBlockOperation 還提供了一個方法 addExecutionBlock:廓握,通過 addExecutionBlock: 就可以為 NSBlockOperation 添加額外的操作。這些操作(包括 blockOperationWithBlock 中的操作)可以在不同的線程中同時(并發(fā))執(zhí)行嘁酿。只有當所有相關的操作已經(jīng)完成執(zhí)行時隙券,才視為完成。如果添加的操作多的話闹司,blockOperationWithBlock: 中的操作也可能會在其他線程(非當前線程)中執(zhí)行娱仔,這是由系統(tǒng)決定的,并不是說添加到 blockOperationWithBlock: 中的操作一定會在當前線程中執(zhí)行游桩。

  1. 自定義繼承自 NSOperation 的子類

如果使用子類 NSInvocationOperation牲迫、NSBlockOperation 不能滿足日常需求,我們可以使用自定義繼承自 NSOperation 的子類借卧№镌鳎可以通過重寫 main 或者 start 方法 來定義自己的 NSOperation 對象。重寫main方法比較簡單铐刘,我們不需要管理操作的狀態(tài)屬性 isExecuting 和 isFinished陪每。當 main 執(zhí)行完返回的時候,這個操作就結束了镰吵。
定義:

// YSCOperation.h 文件
#import <Foundation/Foundation.h>

@interface JYHOperation : NSOperation

@end

//JYHOperation.m 文件
#import "JYHOperation.h"

@implementation JYHOperation

- (void)main {
    if (!self.isCancelled) {
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:3];
            NSLog(@"%@", [NSThread currentThread]);
        }
    }
}

@end

使用

- (void)useCustomOperation {
    // 1.創(chuàng)建 Operation 對象
    JYHOperation *op = [[JYHOperation alloc] init];
    // 2.調用 start 方法開始執(zhí)行
    [op start];
}

在沒有使用 NSOperationQueue檩禾、在主線程單獨使用自定義繼承自 NSOperation 的子類以及使用NSInvocationOperation的情況下疤祭,是在主線程執(zhí)行操作,并沒有開啟新線程戏售,接下來看看怎么將操作添加到隊列中去啦辐。

創(chuàng)建NSOperationQueue

一共有兩種隊列:

  1. 主隊列:通過mainQueue獲得,凡是放到主隊列中的任務都將在主線程執(zhí)行;
  2. 非主隊列:通過 alloc init創(chuàng)建续挟,非主隊列同時具備了并發(fā)和串行的功能,通過設置最大并發(fā)數(shù)屬性來控制任務是并發(fā)執(zhí)行還是串行執(zhí)行

將操作添加到隊列的方式也有兩種:

  1. 先創(chuàng)建操作诗祸,再將創(chuàng)建好的操作加入到創(chuàng)建好的隊列中去:
-(void)addOperation:(NSOperation *)op;
  1. 無需先創(chuàng)建操作跑芳,在 block 中添加操作,直接將包含操作的 block 加入到隊列中:
- (void)addOperationWithBlock:(void (^)(void))block;

將操作加入到操作隊列后能夠開啟新線程博个,并發(fā)執(zhí)行功偿。并且將操作添加到NSOperationQueue中械荷,就會自動啟動,不需要再自己啟動了吨瞎。

NSOperationQueue控制串行颤诀、并行

NSOperationQueue有個關鍵屬性 maxConcurrentOperationCount,叫做最大并發(fā)操作數(shù),用來控制一個特定隊列中可以有多少個操作同時參與并發(fā)執(zhí)行遗淳。

  • maxConcurrentOperationCount默認為-1洲脂,直接并發(fā)執(zhí)行剧包,所以加入到‘非隊列’中的任務默認就是并發(fā)疆液,開啟多線程。
  • 當maxConcurrentOperationCount為1時潘飘,則表示不開線程卜录,也就是串行。
  • 當maxConcurrentOperationCount大于1時筐高,進行并發(fā)執(zhí)行。
  • 系統(tǒng)對最大并發(fā)數(shù)有一個限制柑土,所以即使把maxConcurrentOperationCount設置的很大稽屏,系統(tǒng)也會自動調整西乖。所以把最大并發(fā)數(shù)設置的很大是沒有意義的获雕。
  • maxConcurrentOperationCount 控制的不是并發(fā)線程的數(shù)量,而是一個隊列中同時能并發(fā)執(zhí)行的最大操作數(shù)被廓。
NSOperation 操作依賴

NSOperation能添加操作之間的依賴關系萝玷。通過操作依賴球碉,我們可以很方便的控制操作之間的執(zhí)行先后順序。
NSOperation 提供管理依賴的接口:

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

比如說有 A挎春、B 兩個操作豆拨,其中 A 執(zhí)行完操作施禾,B 才能執(zhí)行操作。
如果使用依賴來處理的話邮绿,那么就需要讓操作 B 依賴于操作 A:

- (void)addDependency {

    // 1.創(chuàng)建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];

    // 2.創(chuàng)建操作
    NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];
    NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模擬耗時操作
            NSLog(@"2---%@", [NSThread currentThread]); // 打印當前線程
        }
    }];

    // 3.添加依賴
    [op2 addDependency:op1]; // 讓op2 依賴于 op1,則先執(zhí)行op1顾腊,在執(zhí)行op2

    // 4.添加操作到隊列中
    [queue addOperation:op1];
    [queue addOperation:op2];
}
NSOperation杂靶、NSOperationQueue 常用屬性和方法
  • NSOpreation
// 開啟線程
- (void)start;
- (void)main;
// 判斷線程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消當前線程
- (void)cancel;
//NSOperation任務是否在運行
@property (readonly, getter=isExecuting) BOOL executing;
//NSOperation任務是否已結束
@property (readonly, getter=isFinished) BOOL finished;
// 添加依賴
- (void)addDependency:(NSOperation *)op;
// 移除依賴
- (void)removeDependency:(NSOperation *)op;
// 優(yōu)先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
};
// 操作監(jiān)聽
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 阻塞當前線程冠骄,直到該NSOperation結束凛辣≈吧眨可用于線程執(zhí)行順序的同步
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
// 獲取線程的優(yōu)先級
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
// 線程名稱
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
  • NSOperationQueue
// 獲取隊列中的操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 隊列中的操作數(shù)
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
// 最大并發(fā)數(shù)蚀之,同一時間最多只能執(zhí)行三個操作
@property NSInteger maxConcurrentOperationCount;
// 暫停 YES:暫停 NO:繼續(xù)
@property (getter=isSuspended) BOOL suspended;
// 取消所有操作
- (void)cancelAllOperations;
// 阻塞當前線程直到此隊列中的所有任務執(zhí)行完畢
- (void)waitUntilAllOperationsAreFinished;
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末足删,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子讶泰,更是在濱河造成了極大的恐慌拂到,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件狼犯,死亡現(xiàn)場離奇詭異悯森,居然都是意外死亡绪撵,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門汹来,熙熙樓的掌柜王于貴愁眉苦臉地迎上來收班,“玉大人,你說我怎么就攤上這事社付×诟” “怎么了兄世?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鸥拧。 經(jīng)常有香客問我削解,道長氛驮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任媳握,我火速辦了婚禮蛾找,結果婚禮上赵誓,老公的妹妹穿的比我還像新娘。我一直安慰自己幻枉,他們只是感情好诡蜓,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布蔓罚。 她就那樣靜靜地躺著瞻颂,像睡著了一般贡这。 火紅的嫁衣襯著肌膚如雪厂榛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天辈双,我揣著相機與錄音湃望,去河邊找鬼局义。 笑死冗疮,一個胖子當著我的面吹牛术幔,可吹牛的內容都是我干的。 我是一名探鬼主播四敞,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼忿危,長吁一口氣:“原來是場噩夢啊……” “哼没龙!你這毒婦竟也來了?” 一聲冷哼從身側響起解滓,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤洼裤,失蹤者是張志新(化名)和其女友劉穎溪王,沒想到半個月后值骇,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雷客,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡搅裙,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年裹芝,在試婚紗的時候發(fā)現(xiàn)自己被綠了嫂易。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡颅和,死狀恐怖峡扩,靈堂內的尸體忽然破棺而出障本,到底是詐尸還是另有隱情,我是刑警寧澤案训,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布强霎,位于F島的核電站蓉冈,受9級特大地震影響,放射性物質發(fā)生泄漏椿争。R本人自食惡果不足惜熟嫩,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柠逞。 院中可真熱鬧景馁,春花似錦合住、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽萨蚕。三九已至,卻和暖如春奕翔,著一層夾襖步出監(jiān)牢的瞬間寒随,已是汗流浹背帮坚。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工试和, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留阅悍,地道東北人芍锚。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓舅踪,卻偏偏與公主長得像工禾,于是被迫代替她去往敵國和親悯周。 傳聞我的和親對象是個殘疾皇子杆烁,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內容

  • iOS多線程編程 基本知識 1. 進程(process) 進程是指在系統(tǒng)中正在運行的一個應用程序烤芦,就是一段程序的執(zhí)...
    陵無山閱讀 6,043評論 1 14
  • 一.概述 1.基本概念 同步與異步的概念 同步 必須等待當前語句執(zhí)行完畢析校,才可以執(zhí)行下一個語句智玻。 異步 不用等待當...
    Jt_Self閱讀 473評論 0 1
  • 前言: 最近想回顧一下多線程問題,看到一篇文章寫的非常詳細,為了便于以后查找以及加深印象,就照著原文摘錄了下文,原...
    FM_0138閱讀 953評論 1 1
  • 1.26日精進:敬畏—進入—體驗—交給—持續(xù) 1,缺啥補啥尚困,怕啥練啥; 2,一切為我所用谬泌,所用為團隊家逻谦; 3邦马,我...
    京心達周莎閱讀 164評論 0 0
  • 同時保有兩種截然相反的觀念随闽,還能正常行事掘宪,這是第一流智慧的標志。出自《了不起的蓋茨比》 『So we beat o...
    淺吟低唱_可否閱讀 344評論 0 0