[iOS笔记]GCD介绍和基本使用

GCD是什么

  • 全称是 Grand Central Dispatch
  • 是纯C语言,提供了非常多强大的函数
  • 是 libdispatch 的市场名称,而 libdispatch 作为 Apple 的一个库,为并发代码在多核硬件(跑 iOS 或 OS X )上执行提供有力支持
  • 这部分代码苹果已开源

GCD的优势

  • 自动利用更多的CPU内核
  • 自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能
  • 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱
  • 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力

GCD的两个核心

  • 任务:执行什么操作
  • 队列:用来存放任务

GCD使用的基本步骤

  • 创建任务:确定要执行的内容
  • 将任务添加到队列中

队列的类型与创建

  • 串行队列(Serial Dispatch Queue)
  • 按顺序让一个任务执行完毕后,再执行下一个任务的队列
  • 并发队列(Concurrent Dispatch Queue)
  • 按顺序都出列都执行(可以同时执行),执行的顺序受执行任务的方式影响的队列
// 创建函数
dispatch_queue_t dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr);
  • 第一个参数:标识C语言字符串(反向DNS样式命名惯例)
  • 第二个参数:队列的样式(两个)
  • DISPATCH_QUEUE_SERIAL 串行队列
  • DISPATCH_QUEUE_CONCURRENT 并发队列

执行任务的方式

  • 同步执行
  • 按队列出列的顺序在的当前线程执行的执行方式,不具有开线程的能力
void dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
  • 异步执行
  • 按队列出列的顺序在新线程上执行的执行方式,具有开线程的能力
  • 常用于处理后台任务
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

队列和执行方法的组合

串行队列 + 同步执行 (毫无用处)

  • 按顺序添加进队列执行完再离开,不开线程
NSLog(@"串行队列同步执行");
for(int i = 0; i < 10; i++) {
    dispatch_sync(self.serialQueue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"test1: %zd, %@", i, [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
        });
}
// 0 1 2 3 4 5 6 7 8 9
// <NSThread: 0x600000066b80>{number = 1, name = main}

串行队列 + 异步执行 (非常有用)

  • 按顺序添加进队列先离开再执行,开线程,按序执行
NSLog(@"串行队列异步执行");
for(int i = 0; i < 10; i++) {
    dispatch_async(self.serialQueue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"test2 %zd, %@", i, [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    });
}
// 0 1 2 3 4 5 6 7 8 9
// <NSThread: 0x6000002746c0>{number = 3, name = (null)}

并发队列 + 同步执行 (有用,但是很容易出错)

  • 按顺序添加进队列执行完再离开,不开线程
NSLog(@"并行队列同步执行");
for(int i = 0; i < 10; i++) {
    dispatch_sync(self.concurrentQueue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"test3 %zd, %@", i, [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    });
}
// 0 1 2 3 4 5 6 7 8 9
// <NSThread: 0x600000066b80>{number = 1, name = main}

并发队列 + 异步执行 (几乎没用)

  • 按顺序添加进队列先离开再执行,开线程,乱序执行
NSLog(@"并行队列异步执行");
for(int i = 0; i < 10; i++) {
    dispatch_async(self.concurrentQueue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"test4 %zd, %@", i, [NSThread currentThread]);
    [NSThread sleepForTimeInterval:1];
    });
}
// 1 0 2 3 4 5 6 7 8 9
// 线程 4 3 5 6 7 8 9 10 11 12
// 瞬间完成

同步和异步决定了是否开启新的线程
串行和并发决定了任务的执行方式


系统提供的五个队列:

  • **主队列(main queue) 一条 **
  • 本质是串行队列
  • 主队列特点:如果主线程正在执行代码暂时不调度任务,等主线程执行结束后在执行任务
  • 主队列又叫 全局串行队列
  • 主队列 + 同步执行 = 死锁 (主队列等待操作完成,操作也在等待主队列操作完成,即互相等待造成死锁)
  • 主线程是唯一可用于更新 UI 的线程
void dispatch_get_main_queue(void)
  • 全局调度队列(Global Dispatch Queues)四条
  • 本质是并发队列
  • 并发队列有名称,可以跟踪错误,全局队列没有
void dispatch_get_global_queue(long identifier, unsigned long flags);
  • 第一个参数:服务质量(quality of service)
    • DISPATCH_QUEUE_PRIORITY_HIGH 最高优先级(2)
    • DISPATCH_QUEUE_PRIORITY_DEFAULT 正常优先级(0)
    • DISPATCH_QUEUE_PRIORITY_LOW 较低优先级 (-2)
    • DISPATCH_QUEUE_PRIORITY_BACKGROUND 最低优先级 (INT16_MIN)
  • 第二个参数:应该永远指定为0(标记是为了未来使用保留)

延迟操作 dispatch_after

void dispatch_after(dispatch_time_t when,
    dispatch_queue_t queue,
    dispatch_block_t block);
  • dispatch_after能让我们添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue
  • 一般都使用主队列上使用这个函数

单次执行 dispatch_once

void dispatch_once(dispatch_once_t *predicate,
        DISPATCH_NOESCAPE dispatch_block_t block);
  • 该函数的作用是保证block在程序的生命周期范围内只执行一次
  • 最常用的场景是单例模式
+ (instancetype)sharedManager {
    static XXManager *sharedXXManager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedXXManager = [[PhotoManager alloc] init];
    });
    return sharedXXManager;
}
  • dispatch_once()以线程安全的方式执行且仅执行其代码块一次。试图访问临界区(即传递给 dispatch_once 的代码)的不同的线程会在临界区已有一个线程的情况下被阻塞,直到临界区完成为止。

多次执行 dispatch_apply

// iterations  执行的次数
// queue       提交到的队列
// block       执行的任务
void dispatch_apply(size_t iterations, 
                    dispatch_queue_t queue, 
                    DISPATCH_NOESCAPE void (^block)(size_t));
  • 把一项任务提交到队列中多次执行,具体是并行执行还是串行执行由队列本身决定。
  • 注意,dispatch_apply不会立刻返回,在执行完毕后才会返回,是同步的调用。
  • dispatch_apply 表现得就像一个 for 循环,但它能并发地执行不同的迭代。这个函数是同步的,所以和普通的 for 循环一样,它只会在所有工作都完成后才会返回。
  • 何时才适合用 dispatch_apply 呢?
  • 自定义串行队列:串行队列会完全抵消 dispatch_apply 的功能;还不如直接使用普通的 for 循环。
  • 主队列(串行):与上面一样,在串行队列上不适合使用dispatch_apply。还是用普通的 for 循环吧。
  • 并发队列:对于并发循环来说是很好选择,特别是需要追踪任务的进度时。
  • 创建并行运行线程而付出的开销,很可能比直接使用 for 循环要多。若你要以合适的步长迭代非常大的集合,那才应该考虑使用 dispatch_apply

栅栏 dispatch_barrier

void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
  • dispatch_barrier_async用于等待前面的任务执行完毕后自己才执行,而它后面的任务需等待它完成之后才执行。典型的例子就是数据的读写,结合着上文中的单例,我们不由想到单例中的对象有可能会被多个线程中的操作“同时”访问或者修改,下面就举一个🌰,实现在单例中的一个属性的setter和getter方法
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);

- (NSString*)someString {
    __block NSString *localSomeString;
    dispatch_sync(_syncQueue, ^{
        localSomeString = someString;
    });
    return localSomeString;
}

- (void)setSomeString:(NSString*)someString {
    dispatch_barrier_async(_syncQueue, ^{
        _someString = someString;
    });
}
// 读取操作可以并行,但是修改操作必须单独执行
  • 当然也有dispatch_barrier_sync,但是是使用同步执行还是异步执行需要测试每种做法的性能来选出最适合当前场景的方案。

调度组 dispatch_group

// 创建调度组
dispatch_group_t dispatch_group_create(void);

// 进入调度组
void dispatch_group_enter(dispatch_group_t group);
// 离开调度组
void dispatch_group_leave(dispatch_group_t group);
// 一进一出,这两个函数的执行次数要匹配,不然会有奇怪是Bug。

// 等待调度组(第二个参数是等待的时间)
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
// 通知调度组(是一个宏来的,三个参数:调度组、block的执行队列、block)
dispatch_notify(<#ibject#>, <#queue#>, <#notification_block#>)
  • 当我们想在GCD Queue中所有的任务执行完毕之后做些特定事情的时候,也就是队列的同步问题。
  • 队列是串行的话,那将该操作最后添加到队列中即可
  • 队列是并行队列的话,这时候就可以利用dispatch_group来实现了,dispatch_group能很方便的解决同步的问题
  • 当组中所有的事件都完成时,GCD 的 API 提供了两种通知方式。
  • dispatch_group_wait
    dispatch_group_wait会同步地等待group中所有的block执行完毕后才继续执行,类似于dispatch_barrier会堵塞线程。(通常需要用dispatch_async将整个方法放入后台队列以避免阻塞主线程)
  • dispatch_group_notify
    功能与dispatch_group_wait类似,不过该过程是异步的,不会阻塞该线程。
//  下面有两种写法是等价的
dispatch_group_async(group, queue, ^{ 
});
// 等价于
dispatch_group_enter(group);
dispatch_async(queue, ^{
  dispatch_group_leave(group);
});
  • 一个开发中比较常用的场景就在网络下载中,多任务下载的时候,需要在当前任务都完成后执行其他操作(如果是更新UI需要回到主线程)。

挂起队列 dispatch_suspend/ 恢复队列dispatch_resume

// 挂起队列
void dispatch_suspend(dispatch_object_t object);
// 恢复队列
void dispatch_resume(dispatch_object_t object);
  • dispatch_suspenddispatch_resume可以暂停、恢复队列上的任务。
  • 但是这里的“挂起”,只能挂起队列后面正在排队的任务,并不能让当前正在执行的任务停止

信号量 dispatch_semaphore

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号量。

但是这段文字可能过于抽象了,一般我们用停车的例子来阐释信号量控制线程的过程。

以一个停车场的运作为例。简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
停车场中的剩余车位个数就是信号亮数

在GCD中有三个函数是semaphore的操作, 分别是:

// 创建信号
dispatch_semaphore_t dispatch_semaphore_create(long value);

// 发信号
long dispatch_semaphore_signal(dispatch_semaphore_t dsema);

// 等待 (有信号可用的时候返回0)
long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout);
// 通常可以这样写
if (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0) {
    NSLog(@"完成");
}
  • dispatch_semaphore_create 创建一个semaphore(创建车位,可以传入参数创建多少个车位)
  • dispatch_semaphore_signal 发送一个信号(告诉看门人有一辆车走了,空出来一个车位)
  • 给信号量 +1
  • dispatch_semaphore_wait等待信号(看门人看到车来了,如果有空车位 让车进去,没有空车位,让车按顺序等待空车位,可以设置等待时间,设置之后过了等待时间,车直接开走,不再进入车库)
  • 等着给信号量 -1 (信号量为0时必定阻塞)
  • 🌰
- (void)dispatch_semaphore
{
    NSLog(@"开始");
    //创建一个信号量容器
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    //进入调度组
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"A开始 %@",[NSThread currentThread]);
        //模拟请求耗时
        sleep(2);
        NSLog(@"A完成 %@",[NSThread currentThread]);
        //事件完成 离开调度组
        dispatch_semaphore_signal(semaphore);
    });
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"B开始 %@",[NSThread currentThread]);
        sleep(1);
        NSLog(@"B完成 %@",[NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"C开始 %@",[NSThread currentThread]);
        sleep(3);
        NSLog(@"C完成 %@",[NSThread currentThread]);
        dispatch_semaphore_signal(semaphore);
    });
    
    if (dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) == 0) {
        NSLog(@"完成");
    }
   
}
// 开始
// A开始 <NSThread: 0x604000272700>{number = 3, name = (null)}
// A完成 <NSThread: 0x604000272700>{number = 3, name = (null)}
// B开始 <NSThread: 0x604000272700>{number = 3, name = (null)}
// B完成 <NSThread: 0x604000272700>{number = 3, name = (null)}
// C开始 <NSThread: 0x604000272700>{number = 3, name = (null)}
// C完成 <NSThread: 0x604000272700>{number = 3, name = (null)}
// XPC connection interrupted 在使用线程休眠的有时会打印出来
// 完成

// 如果不使用信号量 任务的执行执行顺序和完成顺序都会不一样 只是单纯的并发队列异步执行

本文只是本人学习时候的记录和基本用法的介绍,有差错欢迎各位指正,更多的内容可以看下面一些大神的详细内容

iOS多线程:『GCD』详尽总结
GCD 深入理解:第一部分
GCD 深入理解:第二部分

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容