GCD探究(一) -- 任务与队列

GCD全程Grand Central Dispath,是苹果提供的一套多核并行运算的解决方案,GCD使用纯C语言的API,提供了非常强大的API,它会自动利用更多的CPU内核(比如双核、四核),自动管理线程的生命周期(创建线程、调度线程、销毁线程),我们只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

函数、队列与任务

GCD常见的用法

dispatch_sync(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"GCD");
});

可以看到GCD的使用分为3个部分,函数dispatch_sync、队列dispatch_get_global_queue(0, 0),以及任务block。

函数

在GCD中可以将执行函数分为同步和异步两种

  • 同步函数:dispatch_sync,必须等待当前任务block执行完毕后才能执行下一语句,同步函数不会开启线程,会在当前线程中执行任务
  • 异步函数:dispatch_async,不用等待当前任务block执行完毕就可以执行下一语句,可以开启线程执行线程

队列

GCD队列是任务的等待队列,遵循先进先出(FIFO)原则,没调度一个任务就从队列中释放一个任务,队列有两种:串行队列和并发队列,两者的主要区别为执行顺序不同,开启线程数不同

串行队列:每次只能有一个任务执行,任务一个接着一个执行,一个任务执行完毕后,在执行下一个任务

dispatch_queue_t queue = dispatch_queue_create("net.gcd.queue", DISPATCH_QUEUE_SERIAL);

并发队列:可以让多个任务并发执行,可以开启多想线程

dispatch_queue_t queue = dispatch_queue_create("net.gcd.queue", DISPATCH_QUEUE_CONCURRENT);

并发队列的并发功能只有在异步函数下才有效
#define DISPATCH_QUEUE_SERIAL NULL,所以dispatch_queue_create("", NULL)得到的是同步队列

主队列

GCD提供了一种特殊的串行队列--主队列,所有放在队列中的任务都会放到主队列中执行

全局并发队列

GCD默认提供了全局并发队列,他需要提供两个参数,第一个是队列优先级,通常用DISPATCH_QUEUE_PRIORITY_DEFAULT,在iOS9之后,已经被服务质量代替,如QOS_CLASS_DEFAULT,第二个参数是苹果的保留字段,一般写0.

dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);

在使用多线程开发时,如果对队列没有特殊需求,可以直接使用全局并发队列

函数与队列

根据函数与队列的两两组合,会出现出现以下四种结果

同步函数并发队列

  • 不会开启线程,在当前线程执行任务
  • 任务串行执行,任务一个接着一个
  • 可能会产生堵塞

同步函数并发队列

  • 不会开启线程,在当前线程执行任务
  • 任务一个接着一个

异步函数串行队列

  • 开启一条新线程
  • 任务一个接着一个

异步函数并发队列

  • 开启多个线程
  • 异步执行任务,没有顺序,与CPU调度有关

GCD的其他方法

除了开启线程的能力,GCD还提供了许多api,我们可以利用这些api实现单例、多读单写、计时器等功能

栅栏函数 dispatch_barrier_async

如果我们需要异步执行两组操作,而且第一组操作执行完成之后才能执行第二组操作,我们就需要一个栅栏一样的东西将这两组操作分割开来,而GCD的dispatch_barrier_async便可以实现着这一功能。

dispatch_barrier_async会等待前边追加到并发队列的任务全部执行完毕后,再将指定的任务追加到该异步队列中,然后在dispatch_barrier_async追加的任务执行完毕之后,异步队列才恢复为正常执行。

例如:

dispatch_queue_t queue = dispatch_queue_create("barrier_test", DISPATCH_QUEUE_CONCURRENT);
    
dispatch_async(queue, ^{
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1");
    }
});
    
dispatch_async(queue, ^{
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2");
    }
});
    
dispatch_barrier_async(queue, ^{
   for (int i = 0; i < 2; i++) {
       NSLog(@"barrier");
   }
});
    
dispatch_async(queue, ^{
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"3");
    }
});
    
dispatch_async(queue, ^{
    for (int i = 0; i < 2; i++) {
        [NSThread sleepForTimeInterval:2];
        NSLog(@"4");
    }
});

执行结果为

2020-11-25 16:05:29.664252+0800 GCDTest[91450:4599536] 1
2020-11-25 16:05:29.664294+0800 GCDTest[91450:4599535] 2
2020-11-25 16:05:31.668346+0800 GCDTest[91450:4599535] 2
2020-11-25 16:05:31.668442+0800 GCDTest[91450:4599536] 1
2020-11-25 16:05:31.668615+0800 GCDTest[91450:4599536] barrier
2020-11-25 16:05:31.668735+0800 GCDTest[91450:4599536] barrier
2020-11-25 16:05:33.670656+0800 GCDTest[91450:4599536] 3
2020-11-25 16:05:33.670617+0800 GCDTest[91450:4599535] 4
2020-11-25 16:05:35.675116+0800 GCDTest[91450:4599535] 4
2020-11-25 16:05:35.675144+0800 GCDTest[91450:4599536] 3

利用这一特性,我们可以使用栅栏函数实现多读单写。

- (void)setName:(NSString *)name {
    dispatch_barrier_async(self.queue, ^{
        self->_name = [name copy];
    });
}

- (NSString *)name {
    __block NSString *tempName;
    dispatch_sync(self.queue, ^{
        tempName = self->_name;
    });
    return tempName;
}

- (dispatch_queue_t)queue {
    if (!_queue) {
        _queue = dispatch_queue_create("barrier_test", DISPATCH_QUEUE_CONCURRENT);
    }
    return _queue;
}

队列组 dispatch_group

有时候我们会有这样的需求,分别执行两个异步耗时任务,然后当两个耗时任务都执行完毕后再回到主线程执行任务,这时候可以用到GCD的队列组。

GCD的队列组有几个关键的api

  • dispatch_group_create 创建队列组
  • dispatch_group_enter 加入队列组
  • dispatch_group_leave 离开队列组
  • dispatch_group_async 将任务加入队列组
  • dispatch_group_notify 回到指定队列执行任务
  • dispatch_group_wait 同上,但会阻塞当前线程
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"1");
});
dispatch_group_async(group, queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"2");
});
dispatch_group_notify(group, queue, ^{
    NSLog(@"3");
});

执行结果:

2020-11-26 17:33:44.591118+0800 GCDTest[6468:5517558] 2
2020-11-26 17:33:44.591118+0800 GCDTest[6468:5517552] 1
2020-11-26 17:33:44.591338+0800 GCDTest[6468:5517552] 3

GCD一次性代码 dispatch_once

我们在创建单例、或者有整个运行过程中只执行一次的代码时,我们可以使用GCD的dispatch_once实现,即使在多线程的环境,dispatch_once也可以保证线程安全。

- (void)once {
    static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
        // 只执行一次的代码
    });
}

GCD计时器 dispatch_source

dispatch source 是一种处理事件的数据类型,这些被处理的事件为操作系统中的底层级别,而计时器类型是其支持的其中一种类型。

_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_timer, ^{
    NSLog(@"source");
});
dispatch_resume(_timer);
  • dispatch_source_create :创建source,计时器类型使用DISPATCH_SOURCE_TYPE_TIMER,而且可以指定队列
  • dispatch_source_set_timer:设置定时器的相关参数,第一个参数为定时器,第二个为开始时间,第三个为回调时间间隔,第四个允许误差范围
  • dispatch_source_set_event_handler:设置事件处理句柄,一个句柄可以是一个block或者是一个函数,dispatch source会把句柄投放到队列中执行。
  • dispatch_resume:timer默认是挂起的,需要手动开启

NSTimer相比,GCD的timer不需要依赖runloop,不会因为runloop的繁忙而导致及时不准。

GCD快速迭代方法 dispatch_apply

和for循环类似,dispatch_apply可以快速循环遍历。而相比于普通的for循环,dispatch_apply按照指定次数将任务追加到指定的队列中,并等待全部队列执行结束。

如果在串行队列中使用dispatch_apply,那么就和for循环一样按照顺序同步执行,没有快速迭代的意义,我们可以利用并发队列队形进行异步执行,让dispatch_apply的任务多个线程中不是异步执行,并且无论在串行队列还是异步队列,dispatch_apply都会等到全部任务执行完成,有点类似dispatch_group_wait方法。

可以用下面的例子清楚了解:

NSLog(@"start");
dispatch_apply(10, queue, ^(size_t i) {
    NSLog(@"%zd-----%@", i, [NSThread currentThread]);
});
NSLog(@"end");

执行结果为:

2020-11-27 15:35:23.341807+0800 GCDTest[44897:5974416] start
2020-11-27 15:35:23.341930+0800 GCDTest[44897:5974416] 0-----{number = 1, name = main}
2020-11-27 15:35:23.341998+0800 GCDTest[44897:5974416] 2-----{number = 1, name = main}
2020-11-27 15:35:23.342015+0800 GCDTest[44897:5974466] 1-----{number = 2, name = (null)}
2020-11-27 15:35:23.342048+0800 GCDTest[44897:5974416] 3-----{number = 1, name = main}
2020-11-27 15:35:23.342096+0800 GCDTest[44897:5974416] 5-----{number = 1, name = main}
2020-11-27 15:35:23.342093+0800 GCDTest[44897:5974466] 4-----{number = 2, name = (null)}
2020-11-27 15:35:23.342180+0800 GCDTest[44897:5974466] 7-----{number = 2, name = (null)}
2020-11-27 15:35:23.342188+0800 GCDTest[44897:5974416] 6-----{number = 1, name = main}
2020-11-27 15:35:23.342316+0800 GCDTest[44897:5974466] 8-----{number = 2, name = (null)}
2020-11-27 15:35:23.342346+0800 GCDTest[44897:5974464] 9-----{number = 3, name = (null)}
2020-11-27 15:35:23.342849+0800 GCDTest[44897:5974416] end

GCD信号量 dispatch_semaphore

GCD中的信号量指的是dispatch_semaphore,是持有计数的信号,类似高速路收费站的栏杆,可以通过时,打开栏杆,不可以通过时,关闭栏杆。在dispatch_semaphore中,使用计数来完成这个功能,计数为0时等待,不可通过,计数为1或大于1时,计数减1且不等待,可通过。

dispatch_semaphore提供了三个函数。

  • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
  • dispatch_semaphore_signal:发送一个信号,让信号总量加1
  • dispatch_semaphore_wait:可以使总信号量减1,当信号量为0时就会一直等待(阻塞所有线程),否则可以正常执行

基于dispatch_semaphore的这一性质,它常常被用来作为锁来实现线程同步,如下

NSLog(@"current thread: %@", [NSThread currentThread]);
    
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
__block int num = 0;
    
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"current thread: %@", [NSThread currentThread]);
    num = 100;
    dispatch_semaphore_signal(semaphore);
});
    
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"num = %d", num);

如果没有加入dispatch_semaphore,那么NSLog(@"num = %d", num);便不会等待num = 100;执行完成,而是直接打印当前数值0,而加入dispatch_semaphore_t后,开始semaphore为0,执行到
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);时,因为semaphore为0,所以下面的代码不会执行,而子线程中的任务执行num = 100;完成后,会调用一遍dispatch_semaphore_signal(semaphore);semaphore加一变为1,此时会通知到dispatch_semaphore,从而继续往下执行NSLog(@"num = %d", num);

你可能感兴趣的:(GCD探究(一) -- 任务与队列)