多线程四部曲之GCD

GCD是什么?

GCD为Grand Central Dispatch的缩写。Grand Central Dispatch (GCD)是Apple开发的一个多核编程的较新的解决方法。取名灵感来自于纽约中央火车站(Grand Central Terminal)。
开发者只需要定义想执行的任务,并追加到特定的队列中,GCD就会根据情况来决定是否开辟新线程来执行任务。
在这里插入图片描述

dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

GCD主要分为sync同步以及async异步。
queue:队列。
block:想执行的任务。

GCD的同步和异步以及其组合

dispatch_sync和dispatch_async

同步执行(sync):
1. 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
2. 只能在当前线程中执行任务,不具备开启新线程的能力。
异步执行(async):
1. 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
2. 可以在新的线程中执行任务,具备开启新线程的能力。

区别是否等待队列的任务执行结束,以及是否具备开启新线程的能力

串行队列和并发队列、主队列和全局队列

  • 队列(Dispatch Queue):队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

串行队列(Serial Dispatch Queue)和并发队列(Concurrent Dispatch Queue)。

他两的区别是:
执行顺序不同,以及开启线程数不同。

串行队列(Serial Dispatch Queue):每次只执行一个任务,当前一个任务执行完成后才执行下一个任务。
并发队列(Concurrent Dispatch Queue):多个任务并发执行,所以先执行的任务可能最后才完成(因为具体的执行过程导致)。

在这里插入图片描述
在这里插入图片描述

GCD队列的先进先出体现的是拿任务的先后顺序,而不是任务执行完成的先后顺序。

这两个队列都是根据 dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)函数创建的。

dispatch_queue_create(const char *_Nullable label,
		dispatch_queue_attr_t _Nullable attr);
// 两个参数的含义,第一个是创造出来的队列的唯一标识符,第二个参数则是队列的类型。
// 返回类型为dispatch_queue_t

主队列和全局队列

  • 主队列
 // 获取主队列
    dispatch_queue_t mainQueue = dispatch_get_main_queue();

主队列是一个特殊的串行队列,我们追加到主队列的任务都会被放到主线程中执行。

  • 全局队列
// 获取全局队列
    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

全局队列其实就是系统为我们创建好的并发队列,我们平常利用全局队列就可以了。

他们的六种组合

从上面的分析可以看出,我们有两种执行任务方式,以及三种执行队列,所以有六种组合方式。

串行队列SerialQueue 并发队列ConcurrentQueue 主队列mainQueue
dispatch_sync 会阻塞当前线程,不会开辟新线程。队列里的任务在当前线程上串行执行。 会阻塞当前线程,不会开辟新线程。队列里的任务在当前线程上串行执行。 会阻塞当前线程,不会开辟新线程。队列里的任务在主线程上串行执行。
dispatch_async 不会阻塞当前线程,会开辟新线程,会开辟一个新线程(因为是串行),队列的任务在新线程执行 不会阻塞当前线程,会开辟多个线程,(因为是并发处理需要多个线程,队列任务在这些新线程并发执行 不会阻塞当前线程,不会开辟新线程(因为主线程的任务都会被放入主线程执行),队列的任务执行在主线程串行执行。

可以看到想要用GCD实现多线程开发,就必须使用到dispatch_async异步执行
在实际使用这六种组合时,需要考虑四个角度:

  • 会不会阻塞当前线程?由syncasync决定,同步肯定会堵塞当前线程,异步肯定不会堵塞当前线程。
  • 会不会开辟新线程?这个是由组合决定的,使用dispatch_sync一定不会开辟新线程,而使用dispatch_async+主队列也不开辟新线程,其余都可能会开辟新线程。
  • 在开辟线程的前提下,开辟数量?此时完全由队列决定,如果我们是并发则会开辟多个新线程,而串行队列只会开辟一个。
  • 队列里的任务在哪个线程执行,执行方法? 这个是共同决定的,但队列占主导地位,串行队列的任务肯定是串行执行,开辟了新线程时则在新线程上串行执行,没开辟则在当前线程执行,并发同理,主线程的任务永远在主线程执行

下面测试组合分析一下

1.dispatch_sync + 并发队列

- (void)testSyncConcurrent {
    
    
    NSLog(@"begin-- 并发队列 + 同步执行");
    // 创建一个并发队列
    dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_CONCURRENT);
    // 执行两次任务
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"1--%@", [NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"2--%@", [NSThread currentThread]);
        }
    });
    NSLog(@"end-- 并发队列 + 同步执行");
}

打印结果如下:
在这里插入图片描述
可以看到这里虽然是并发队列,但因为dispatch_sync不具备开辟新线程的能力,所以还是在主线程中执行任务,且按顺序处理。

2.dispatch_async + 并发队列

我们尝试异步执行。

- (void)asyncConcurrent {
    
    
    NSLog(@"begin-- 并发队列 + 异步执行");
    dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"1------%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"2------%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"3------%@",[NSThread currentThread]);
        }
    });
    
    NSLog(@"end-- 并发队列 + 异步执行");
}

在这里插入图片描述
可以得到以下结果:
开辟了新的三个线程,并且任务是交替同时进行的。

所有任务是在打印的begin-- 和end-- 之后才开始执行的,说明任务不是马上执行,而是将所有任务添加到队列之后才开始异步执行,另外当前线程并没有等待,而是直接开启了新的线程,在新线程中执行任务,由于异步执行不做等待,所以可以继续执行其他任务.

3.dispatch_sync + 串行队列

- (void)syncSerial {
    
    
    NSLog(@"begin-- 串行队列 + 同步执行");
    dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"1------%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"2------%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"3------%@",[NSThread currentThread]);
        }
    });
    NSLog(@"end-- 串行队列 + 同步执行");
}

在这里插入图片描述
可以得到以下结果:

  • 所有任务都是在主线程中执行的,并没有开启新的线程(同步执行不具备开启新线程的能力),由于串行队列,所以按顺序一个一个执行下去。
  • 所有任务都在打印的begin-- 和end-- 之间,这说明任务是添加到队列中马上执行的,而且同步任务需要等待队列的任务执行结束,才可以执行下一个任务。
  • 任务是按顺序执行的,这说明串行队列每次只有一个任务被执行,任务一个接一个按顺序执行下去。

4.dispatch_async + 串行队列

- (void)asyncSerial {
    
    

    NSLog(@"begin-- 串行队列 + 异步执行");
    dispatch_queue_t queue = dispatch_queue_create("net.test.queue", DISPATCH_QUEUE_SERIAL);
    
    dispatch_async(queue, ^{
    
    
        // 追加任务1
        for (int i = 0; i < 2; ++i) {
    
    
               NSLog(@"1------%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
    
    
        // 追加任务2
        for (int i = 0; i < 2; ++i) {
    
    
               NSLog(@"2------%@",[NSThread currentThread]);
        }
    });
    dispatch_async(queue, ^{
    
    
        // 追加任务3
        for (int i = 0; i < 2; ++i) {
    
    
       NSLog(@"3------%@",[NSThread currentThread]);
        }
    });
    NSLog(@"end-- 串行队列 + 异步执行");

}

在这里插入图片描述
从打印中可以总结如下几点:

  • 开启了一条新线程(异步执行具备开启新线程的能力,但是串行队列只能开启一个线程),但是任务由于是串行的,所以任务还是一个一个的执行下去(串行队列每次只能有一个任务被执行,任务一个接着一个顺序执行)。
  • 所有任务是在打印的begin-- 和end-- 之后才开始执行的,说明任务是将所有任务添加到队列之后才开始异步执行,而不是马上执行(异步执行不会做任何等待,可以继续执行任务)。

dispatch_sync + 主线程

- (void)syncMain {
    
    
    NSLog(@"begin-- 主队列 + 同步执行");
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"1------%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"2------%@",[NSThread currentThread]);
        }
    });
    dispatch_sync(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"3------%@",[NSThread currentThread]);
        }
    });
    NSLog(@"end-- 主队列 + 同步执行");
}

在这里插入图片描述
可以看到在追加任务时,程序崩溃了。
这个现象称之为GCD死锁。

GCD死锁问题

通过上面的例子可以看出,造成死锁的条件是,使用dispatch_sync,并且往他自己所在的这个队列追加任务,且当前队列为串行队列。(他自己所在的这个队列是指dispatch_sync追加任务操作本身的队列).
可是为什么这样就会死锁?

死锁原因

  • 任务添加阶段:程序启动后,系统会自动把主线程里要执行的任务都添加到主队列,那么这个例子中,系统会自动添加viewDidload这个任务,然后再将block任务追加到viewDidload后面。
  • 任务执行阶段:首先程序拿来viewDidload来执行,在执行过程,dispatch_sync会堵塞主线程来执行block任务,但是block任务是被追加到viewDidload后面的,主队列是串行队列,必须等待上一个任务执行完毕才可以执行下一个任务,所以block任务想要执行就必须等待viewDidload执行完毕,而viewDidload想要执行就必须等block任务执行完毕,两个任务互相等待,就造成了死锁。

打破死锁

打破死锁只要改变上面的一种条件就可以。

  • 方案一:用dispatch_async追加任务
- (void)syncMain {
    
    
    NSLog(@"begin-- 主队列 + 同步执行");
    dispatch_queue_t queue = dispatch_get_main_queue();
    
    dispatch_async(queue, ^{
    
    
        for (int i = 0; i < 2; i++) {
    
    
            NSLog(@"1------%@",[NSThread currentThread]);
        }
    });
    NSLog(@"end-- 主队列 + 同步执行");
}

这样任务就可以执行了,这里的block依旧被添加到了主队列,而且依旧在viewDidload之后,但是dispatch_async不会阻塞主线程viewDidload不必等block执行完之后才能往下执行,直接执行完毕之后就可以执行block。

  • 方法二:将dispatch_sync追加的任务放到别的队列里(串行,并发都可以).
// 我们先创建了一个异步并发队列,然后在队列中同步追加block。
    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
        dispatch_sync(queue, ^{
    
    
            NSLog(@"当前线程:%@",[NSThread currentThread]);
        });
        // 这里全局队列也是可以的。

block被追加到别的队列,不存在等待关系,自然不会死锁。

  • 方法三dispatch_sync自身所在的队列是并发队列
// 我们先获取一个全局队列,然后在队列中异步追加dispatch_sync这个任务。
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_async(queue, ^{
    
    
            NSLog(@"任务1当前线程:%@",[NSThread currentThread]);
            dispatch_sync(queue, ^{
    
    
                NSLog(@"block任务线程%@", [NSThread currentThread]);
            });
            NSLog(@"任务2当前线程:%@", [NSThread currentThread]);
        });

在这里插入图片描述
首先已经和主队列不是一个队列,不会和viewDidload冲突,并且新的队列是并发队列,任务2不需要等待block任务执行完毕就可以拿出来执行。
所以如果我们把他追加到别的串行队列,只能是不与原来主线程死锁,但会可能和新的任务锁住。

GCD的一些常用API

  1. dispatch_once
    dispatch_once 用来保证一段代码在程序的整个生命周期中执行一次,所以里面的代码是百分之百线程安全的。
static dispatch_once_t onceToken;
- (void)viewDidLoad {
    
    
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    NSLog(@"%ld", onceToken);
    for (int i = 0; i < 10; i++) {
    
    
        [self onceTest];
    }
}
- (void)onceTest {
    
    
    dispatch_once(&onceToken, ^{
    
    
        NSLog(@"%ld", onceToken);
    });
}

运行结果确实是调用一次,这里盲猜是判断了onceToken决定是否运行过,如果后续了解到来补充。

  1. dispatch_after
    dispatch_after用来延迟多长时间后执行某个任务,和GCD定时器的原理相同,都是基于系统内核实现的。而performSelector:afterDelay和NSTimer都是基于RunLoop实现的。
- (void)timeAfter {
    
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    
    
        NSLog(@"1");
    });
}
  1. GCD定时器
    我们知道NSTimer是基于RunLoop实现的,它可能不准时,所以我们想要定时精准,可以使用GCD定时器。它不是基于RunLoop实现的,不是Timer事件源,而是基于内核。
- (void)GCDTimer {
    
    
    // 创建定时器
    // 最后一个参数的含义:定时器的回调要放在什么队列里
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    // 设置时间
    // start:多少秒后开始
    // intervalInSeconds:时间间隔
    // leewayInSeconds:误差,0
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(timer, ^{
    
    
        NSLog(@"定时器回调");
        
    });
    dispatch_resume(timer);
    // 强引用保住timer
    self.timer = timer;
}

执行结果:
在这里插入图片描述

  1. GCD信号量
    dispatch_semaphore–GCD信号量用来控制GCD线程的最大并发数,它有一个初始值,这个值就用来指定GCD线程的最大并发数。
    dispatch_semaphore的原理是利用dispatch_semaphore_wait
    dispatch_semaphore_signal两个函数来实现的。具体地说,每当一个线程进来执行
    dispatch_semaphore_wait函数,函数内部就会判断信号量的值,如果发现信号量的值>0,就让信号量的值-1,并让这条线程往下执行代码,如果发现信号量的值<=0,就会让线程阻塞在这里休眠,等待信号量的值再次>0时被唤醒;而每当一个线程进来执行
    dispatch_semaphore_signal函数,就代表这个线程执行完任务了,函数内部会让信号量的值+1,如果上面有休眠的线程,就可以唤醒那个休眠的线程进来执行代码了。这样就达到了控制线程最大并发数的效果。

猜你喜欢

转载自blog.csdn.net/chabuduoxs/article/details/125182152