【IOS】GCD学习

GCD学习

前言

前期对于GCD一直没有一个系统的学习,仅仅是使用了GCD进行网络申请时解决异步问题,本篇文章来总结一下GCD,学习总结一下没有学到的地方。

GCD简介

GCD全称Grand Central Dispatch,它是全C代码,其提供了非常多强大的函数。

GCD的优势:

  • 其为苹果公司为多核的并行运算提出的解决方案。
  • 其会自动利用更多的CPU内核。
  • 其会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 我们在使用GCD的时候,只需告诉他我们想要执行什么任务,不需要编写任务线程管理代码

概括一下GCD就是:将任务添加到队列,并且制定执行任务的函数

函数

在GCD中执行任务的方式有两种,同步执行和异步执行 ;他们分别对应同步函数dispatch_sync与异步函数dispatch_async,下面看看两者对比:

  • 同步执行,对应同步函数dispatch_sync
    • 必须等待当前语句执行完成之后,才能执行下一条语句
    • 不会开启线程,不具备开启线程的能力
    • 在当前线程中执行block任务
  • 异步执行,对应异步函数dispatch_async
    • 不用等待当前语句执行完成,就可以执行下一条语句
    • 会开启线程执行block任务,具备开启新线程的能力
    • 异步即为多线程的代名词

综上所述,我们来总结一下两种执行方式的主要区别:

  • 是否等待队列的任务执行完毕
  • 是否具备开启新线程的能力

任务与队列

下面来看看GCD的两个核心:任务与队列:

  • 任务:需要执行什么操作
  • 队列:用于存放任务

使用步骤:

  • 创建任务:确定要做的事情

  • 将任务添加到队列中去

    • GCD会自动将队列中的任务取出来,而后放到对应的线程中去执行。
    • 任务的取出遵循FIFO原则:先进先出,后进后出

队列分为串行队列和并发队列,这里给一张图先来看看:

  • 串行队列:每次只有一个任务被执行,等上一个结束了再执行下一个,即只开启了一个线程。
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_SERIAL);创建串行队列
    • 其中DISPATCH_QUEUE_SERIAL可以用NULL表示,这两种表达方式都表示默认的串行队列。
  • 并发队列:一次可以并发执行多个任务,即开启多个线程,并且同时执行任务。
    • 使用dispatch_queue_create("xxx", DISPATCH_QUEUE_CONCURRENT);创建并发队列
    • 注意:并发队列的并发功能只有在异步函数下才有效。

主队列和全局并发队列

在GCD中,针对以上两种队列,分别提供了主队列和全局并发队列:

  • 主队列(Main Dispatch Queue):GCD中提供的特殊的串行队列

    • 专门用来在主线程上调度任务的串行队列,依赖于主线程、主Runloop,在main函数调用之前自动创建。
    • 不会开启线程
    • 如果当前主线程正在有任务执行,那么无论主队列中被添加什么任务,都不会被调度。
    • 使用dispatch_get_main_queue()获得主队列
    • 通常在返回主线程更新UI的时候使用
    objc 复制代码
    dispatch_queue_t serialQueue = dispatch_get_main_queue();
  • 全局并发队列(Global Dispatch Queue):GCD提供的默认并发队列。

    • 为了方便程序员的使用,苹果提供了全局队列
    • 在使用多线程开发时,如果对队列没有特殊需求,在执行异步任务时,可以直接使用全局队列
    • 使用dispatch_get_global_queue获取全局并发队列,最简单的是dispatch_get_global_queue(0, 0)
      • 第一个参数表示队列优先级,默认优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT=0,在ios9之后,已经被服务质量(quality-of-service)取代
      • 第二个参数使用0
objc 复制代码
//全局并发队列的获取方法
dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);

//优先级从高到低(对应的服务质量)依次为
- DISPATCH_QUEUE_PRIORITY_HIGH       -- QOS_CLASS_USER_INITIATED
- DISPATCH_QUEUE_PRIORITY_DEFAULT    -- QOS_CLASS_DEFAULT
- DISPATCH_QUEUE_PRIORITY_LOW        -- QOS_CLASS_UTILITY
- DISPATCH_QUEUE_PRIORITY_BACKGROUND -- QOS_CLASS_BACKGROUND

函数与队列的不同组合

下面来看看不同的组合方式:

串行队列 + 同步函数

objectivec 复制代码
-(void) test01 {
    NSLog(@"串行 + 同步:%@", [NSThread currentThread]);
    dispatch_queue_t serialQueue = dispatch_get_main_queue();
//    dispatch_queue_create("com.GCD.Queue", NULL);
    for (int i = 0; i < 5; i++) {
        NSLog(@"串行 + 同步:%d - %@", i, [NSThread currentThread]);
    }
}

运行结果:

通过打印结果可以看出并没有开辟新的线程,任务一个接一个在当前线程中执行

串行队列 + 异步函数

objectivec 复制代码
-(void) test02 {
    NSLog(@"串行 + 异步:%@", [NSThread currentThread]);
    dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_SERIAL);
    for (int i = 0; i < 5; i++) {
        dispatch_async(serialQueue, ^{
            NSLog(@"串行 + 异步:%d - %@", i, [NSThread currentThread]);
        });
    }
}

运行结果:

可以看到,任务还是一个接一个的执行,但是这里开辟了新的线程

并发队列 + 同步函数

objc 复制代码
-(void) test03 {
    NSLog(@"并发 + 同步:%@", [NSThread currentThread]);
    dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; i++) {
        dispatch_sync(serialQueue, ^{
            NSLog(@"并发 + 同步:%d - %@", i, [NSThread currentThread]);
        });
    }
}

运行结果:

并发队列 + 异步函数

objc 复制代码
-(void) test04 {
    NSLog(@"并发 + 异步:%@", [NSThread currentThread]);
    dispatch_queue_t serialQueue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i < 5; i++) {
        dispatch_async(serialQueue, ^{
            NSLog(@"并发 + 异步:%d - %@", i, [NSThread currentThread]);
        });
    }
}

运行结果:

这里我们对比这来看并发队列使用同步、异步函数时的区别:

  • 同步函数没有开辟新的线程
  • 异步函数开辟了新的线程

在上文中我们已经说了并发队列的并发功能只能在异步函数中使用,这究竟是为什么呢?

  • 首先同步函数会阻塞当前线程,直至任务完成
  • 而异步函数是由系统线程池管理的,没不会阻塞当前线程,当任务位于队列中的时候,会直接分配给其他线程来执行,这就已经造就了两者之间的差别。

下面附一张图来帮助理解:

主队列 + 同步函数

objective-c 复制代码
-(void) test05 {
    NSLog(@"主队列 + 同步:%@", [NSThread currentThread]);
    dispatch_queue_t serialQueue = dispatch_get_main_queue();
    for (int i = 0; i < 5; i++) {
        dispatch_sync(serialQueue, ^{
            NSLog(@"主队列 + 同步:%d - %@", i, [NSThread currentThread]);
        });
    }
}

运行结果:

我们可以看到这里任务相互等待,造成了死锁。

原因分析

  • 主队列有两个任务,顺序为:NSLog任务 - 同步block
  • 执行NSLog任务后,执行同步Block,会将任务1(即i=1时)加入到主队列,主队列顺序为:NSLog任务 - 同步block - 任务1
  • 任务1的执行需要等待同步block执行完毕才会执行,而同步block的执行需要等待任务1执行完毕,所以就造成了任务互相等待的情况,即造成死锁崩溃

主队列 + 异步函数

objc 复制代码
-(void) test04 {
    NSLog(@"主队列 + 异步:%@", [NSThread currentThread]);
    dispatch_queue_t serialQueue = dispatch_get_main_queue();
    for (int i = 0; i < 5; i++) {
        dispatch_async(serialQueue, ^{
            NSLog(@"主队列 + 异步:%d - %@", i, [NSThread currentThread]);
        });
    }
}

运行结果:

这里我们可以看到任务一个接一个的执行,没有开辟新的线程

全局并发队列 + 同步函数

objc 复制代码
-(void) test04 {
    NSLog(@"全局并发队列 + 同步:%@", [NSThread currentThread]);
    for (int i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"全局并发队列 + 同步:%d - %@", i, [NSThread currentThread]);
        });
    }
}

运行结果:

任务按顺序一个一个执行,没有开辟新的线程

全局并发队列 + 异步函数

objc 复制代码
-(void) test04 {
    NSLog(@"全局并发队列 + 异步:%@", [NSThread currentThread]);
    for (int i = 0; i < 5; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"全局并发队列 + 异步:%d - %@", i, [NSThread currentThread]);
        });
    }
}

运行结果:

任务乱序执行,开辟可新的线程

总结

函数与队列 串行队列 并发队列 主队列 全局并发队列
同步函数 顺序执行,不开辟线程 顺序执行,不开辟线程 死锁 顺序执行,不开辟线程
异步函数 顺序执行,开辟线程 乱序执行,开辟线程 顺序执行,不开辟线程 乱序执行,开辟线程

死锁

死锁是指两个线程A和B都卡住了,A在等B,B在等A,相互等待对方完成某些操作。A不能完成是因为它在等待B完成。但B也不能完成,因为他在等待B完成。但B也不能完成,因为它在等待A完成。于是大家都完不成,导致了死锁。

死锁产生的四个必要条件

  • 互斥访问,再有互斥访问的一个情况,线程才会等待
  • 持有并等待,线程持有一些资源,并等待一些资源
  • 资源非抢占:一旦一个资源被持有,除非持有者主动放弃,否则其他竞争者无法获取这个资源。
  • 循环等待:循环等待是指存在一系列线程T0、T1...Tn,T0等待T1,T1等待T2,T2等待Tn,这样出现了一个循环。

dispatch_after

这是GCD提供的延迟执行函数,并非严格在指定时间后立即执行,而是将任务追加到目标队列的时间点

objc 复制代码
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"3秒后");
});

dispatch_once

objc 复制代码
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    NSLog(@"%@", [NSThread currentThread]);
});

dispatch_once保证在APP运行期间,block中的代码仅仅执行一次,通常在单例以及方法交换中应用。

dispatch_apply

这是GCD提供给我们的一种快速迭代方法,该方法按照指定的次数将指定的任务追加到指定队列中去,等到全部的任务执行结束以后,系统会根据实际情况自动分配和管理线程 。

这里我们先来看一个使用其的例子

objc 复制代码
-(void) test06 {
    NSLog(@"-- begin --");
    NSArray *arr = @[@"a", @"b", @"c", @"d", @"e"];
    dispatch_queue_t queue = dispatch_queue_create("com.jarypan.gcdsummary", DISPATCH_QUEUE_CONCURRENT);
    /*
    arr.count    指定重复次数  这里数组内元素个数是 5 个,也就是重复 5 次
    queue        追加任务的队列
    index        带有参数的 Block, index 的作用是为了按执行的顺序区分各个 Block
    */
    dispatch_apply(arr.count, queue, ^(size_t index) {
        NSLog(@"index = %zu, str = %@ -- %@", index, arr[index], [NSThread currentThread]);
    });
    NSLog(@"-- end --");
}

运行结果:

这里我们可以看到,在dispatch_apply中任务是并发执行的,但是会阻塞当前线程,直至所有任务全部结束以后才会打印end,所以我们可以在使用的时候给外面套上一层dispatch_async来解决阻塞当前线程的问题。

应用场景:用来拉取网络数据后提前算出各个空间的大小,防止绘制时计算,提高表单滑动流畅性。

dispatch_group_t

dispatch_group_t调度组将任务分组执行,能够监听任务组完成,并等待时间。

应用场景:多个接口请求之后的刷新页面

使用dispatch_group_async + dispatch_group_notify

objc 复制代码
-(void) test07 {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务一:%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务二:%@", [NSThread currentThread]);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"刷新页面");
    });
}

使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify

objectivec 复制代码
-(void) test08 {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务一:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        NSLog(@"任务二:%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新页面");
    });
}

这里成组的使用dispatch_group_enter以及dispatch_group_leave可以令进出组的逻辑更加清晰。

在进出组的基础上使用 dispatch_group_wait

objectivec 复制代码
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
    NSLog(@"任务一:%@", [NSThread currentThread]);
    dispatch_group_leave(group);
});

dispatch_group_enter(group);
dispatch_group_async(group, queue, ^{
    NSLog(@"任务二:%@", [NSThread currentThread]);
    dispatch_group_leave(group);
});

long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC));
NSLog(@"timeout = %ld", timeout);
if (timeout == 0) {
    NSLog(@"按时完成任务");
} else {
    NSLog(@"超时");
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"刷新页面");
});

栅栏函数

栅栏函数主要由两种使用场景:串行队列、并发队列。使用场景:同步锁

等栅栏前追加到队列中的任务执行完毕以后,再将栅栏后的人物追加到队列中去,简而言之就是先执行栅栏函数前的任务,而后在执行栅栏任务,最后执行栅栏后的任务。

串行队列使用栅栏函数:

objc 复制代码
-(void) test09 {
    dispatch_queue_t queue = dispatch_queue_create("com.GCD.Queue", NULL);
    NSLog(@"开始------%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s执行的任务------%@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束------%@", [NSThread currentThread]);
    dispatch_barrier_async(queue, ^{
        NSLog(@"--------栅栏任务-------------\n%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束------%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"延迟1s的任务------%@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束------%@", [NSThread currentThread]);
}

运行结果:

栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1任务2

结论:由于串行队列异步执行任务是一个接一个执行完毕的,所以使用栅栏函数没意义

并发队列使用栅栏函数

objective-c 复制代码
dispatch_queue_t queue = dispatch_queue_create("com.GCD.Queue", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始------%@", [NSThread currentThread]);
dispatch_async(queue, ^{
    sleep(2);
    NSLog(@"延迟2s执行的任务------%@", [NSThread currentThread]);
});
NSLog(@"第一次结束------%@", [NSThread currentThread]);
dispatch_barrier_async(queue, ^{
    NSLog(@"--------栅栏任务-------------\n%@", [NSThread currentThread]);
});
NSLog(@"栅栏结束------%@", [NSThread currentThread]);
dispatch_async(queue, ^{
    sleep(1);
    NSLog(@"延迟1s的任务------%@", [NSThread currentThread]);
});
NSLog(@"第二次结束------%@", [NSThread currentThread]);

运行结果:

结论:由于并发队列异步执行任务是乱序执行完毕的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序

dispatch_barrier_sync/dispatch_barrier_async区别

  • dispatch_barrier_async:前面任务执行完毕才会来到这里
  • dispatch_barrier_sync:作用相同,但是会堵塞线程,影响后面任务的执行

将上个案例中的dispatch_barrier_async改为dispatch_barrier_sync之后运行结果:

我们可以发现,dispatch_barrier_async可以控制队列中任务的执行顺序,而dispatch_barrier_sync不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用)

栅栏函数注意点
  • 1.尽量使用自定义的并发队列:
    • 使用全局队列起不到栅栏函数的作用
    • 使用全局队列时由于对全局队列造成堵塞,可能致使系统其他调用全局队列的地方也堵塞从而导致崩溃(并不是只有你在使用这个队列)
  • 2.栅栏函数只能控制同一并发队列:打个比方,平时在使用AFNetworking做网络请求时为什么不能用栅栏函数起到同步锁堵塞的效果,因为AFNetworking内部有自己的队列

dispatch_semaphore_t

信号量主要用作同步锁,用于控制GCD最大并发数

  • dispatch_semaphore_create():创建信号量
  • dispatch_semaphore_wait():等待信号量,信号量减1.当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作------如果永久等待 将等到信号(signal)才执行下去
  • dispatch_semaphore_signal():释放信号量,信号量加1.当信号量>= 0 会执行wait之后的代码。
objc 复制代码
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
dispatch_queue_t queue = dispatch_queue_create("hello", DISPATCH_QUEUE_CONCURRENT);

for (int i = 0; i < 10; i++) {
    NSLog(@"当前线程%d------------线程%@", i, [NSThread currentThread]);
    dispatch_semaphore_signal(sem);
}
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);

上述代码展示的是一个使用信号量按序号输出的例子

如果当创建信号量时传入值为1又会怎么样呢?

  • i=0时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为0不会阻塞线程,所以进入i=1
  • i=1时有可能先打印,也可能会先发出wait信号量-1,但是wait之后信号量为-1阻塞线程,等待signal再执行下去

设置最大开辟线程数

当我们下载图片的时候,进行并发异步,每一个下载都会开辟一个新的线程,这里我们担心太多线程会导致内存开销太大,以及线程的上下文切换会给我们的CPU带来的开销太大导致的问题,我们可以设置一个最大开辟线程数:

objc 复制代码
dispatch_semaphore_t currSingal = dispatch_semaphore_create(3);// 创建信号量,如果小于0则会返回NULL
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(currSingal, DISPATCH_TIME_FOREVER);
        NSLog(@"执行任务, %d %@", i, [NSThread currentThread]);
        sleep(1);
        NSLog(@"完成任务, %d %@", i, [NSThread currentThread]);
        dispatch_semaphore_signal(currSingal);
    });
}

这里的任务我们在执行的时候可以发现是一次三个三个的执行,即最多只开辟了三个线程。

但是这里我们打印NSTread的时候,会出现下面这些内容:

这个报错是由于这里出现了一个优先级反转的问题。

这里先介绍一下优先级反转:优先级:线程C>线程B>线程A。优先级较低的线程B,通过压制优先级更低的线程A,比高优先级的线程C先执行了。

解释:假如线程A拿到一个资源后加锁,线程C因为也需要这个资源于是挂起等待A执行结束。这一段符合逻辑没问题,但是此时线程B因为优先级比线程A高,直接抢占CPU,线程B执行完后,线程A执行,A解锁释放后,C再执行。这就导致原本优先级较低的线程B,通过压制线程A,比高优先级的线程C先执行了。

这里给一张经典的图:

相关推荐
阿阳微客5 小时前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏
Chef_Chen10 小时前
从0开始学习R语言--Day18--分类变量关联性检验
学习
SY.ZHOU10 小时前
Significant Location Change
macos·ios·cocoa
海的诗篇_10 小时前
前端开发面试题总结-JavaScript篇(一)
开发语言·前端·javascript·学习·面试
AgilityBaby11 小时前
UE5 2D角色PaperZD插件动画状态机学习笔记
笔记·学习·ue5
AgilityBaby11 小时前
UE5 创建2D角色帧动画学习笔记
笔记·学习·ue5
武昌库里写JAVA12 小时前
iview Switch Tabs TabPane 使用提示Maximum call stack size exceeded堆栈溢出
java·开发语言·spring boot·学习·课程设计
一弓虽13 小时前
git 学习
git·学习
Moonnnn.15 小时前
【单片机期末】串行口循环缓冲区发送
笔记·单片机·嵌入式硬件·学习