「iOS」GCD

「iOS」GCD

文章目录

一、基础知识

1. 队列

定义:队列,又称为伫列(queue),是先进先出 的线性表,在具体应用中通常用链表 或者数组 来实现。装载线程任务的队形结构。队列只允许在后端(称为rear)进行插入 操作,在前端(称为front)进行删除操作。队列的操作方式和堆栈类似,唯一的区别在于队列只允许新数据在后端进行添加。

**类型:**队列的类型决定了任务的执行方式(并发、串行),队列包括以下几种:

  • 并发队列: 线程执行可以同时一起进行执行,不需要上一个执行完,就能执行下一个的。
  • 串行队列: 线程执行只能依次逐一先后有序的执行,等待上一个执行完,再执行下一个。
  • 主队列:绑定主线程,所有任务都在主线程中执行,有经过特殊处理的串行队列。
  • 全局队列:系统提供的并发队列。

2. 同步、异步

同步 sync: 只能在当前线程按先后顺序依次执行任务,不具备开启新线程的能力。

异步 async: 在新的线程中执行任务,具备开启新线程的能力。

二、iOS的多线程方案

  1. pthread:是线程的POSIX标准,是一套通用的多线程API,可以在Unix/Linux/Windows等平台跨平台使用。iOS中基本不使用。

  2. NSThread:苹果封装的面向对象的线程类,可以直接操作线程,比起GCD,NSThread效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。

  3. GCD:全称Grand Central Dispatch,由C语言实现,是苹果为多核的并行运算提出的解决方案,CGD会自动利用更多的CPU内核,自动管理线程的生命周期,程序员只需要告诉GCD需要执行的任务,无需编写任何管理线程的代码。GCD也是iOS使用频率最高的多线程技术。

  4. NSOperation:基于GCD封装的面向对象的多线程技术,常配合NSOperationQueue使用,使用频率较高。

三、GCD

1. 优势

  • GCD 是苹果公司为多核的并行运算提出的解决方案
  • GCD 会自动利用更多的CPU内核(比如双核、四核)
  • GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码

2. 使用

同步执行API

objc 复制代码
//queue:队列   block:任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

异步执行

objc 复制代码
//queue:队列   block:任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

dispatch_block_t任务

无参数、无返回值的 Block 回调,用于存放具体需要执行的线程任务,底层定义如下:

objc 复制代码
typedef void (^dispatch_block_t)(void);

dispatch_queue_t队列

任务存放的队列容器。自定义队列通过 dispatch_queue_create 创建,参数分别为队列名称 、队列类型 ;其中串行队列宏 DISPATCH_QUEUE_SERIAL 等价于 NULL,传 NULL 即为串行队列。

我们已经知道队列在iOS中主要分为4种

自定义的队列都是调用dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr)方法来进行创建,两个参数分别为自定义队列名称和队列类型。其中串行队列DISPATCH_QUEUE_SERIAL为宏定义的NULL,所以传NULL也表示为串行队列。

现在,我们来看这2种执行模式,3种队列是相互搭配是什么效果:

同步 + (主队列或自定义串行队列)(死锁)

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1:%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_sync(queue, ^{
        NSLog(@"任务2",[NSThread currentThread]);
    });
    NSLog(@"任务3:%@",[NSThread currentThread]);
}

运行结果

复制代码
打印任务1后死锁崩溃

原因分析:主队列是串行队列,遵循 FIFO 规则。当前 viewDidLoad 本身是主队列的任务,未执行完毕;此时通过同步方式向主队列新增任务2,同步执行会阻塞主线程,等待任务2执行完成。但任务2需要等待 viewDidLoad 原有任务执行完毕才能执行,二者相互等待,最终形成死锁。

异步 + (主队列或自定义串行队列)

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1:%@",[NSThread currentThread]);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_async(queue, ^{
        NSLog(@"任务2:%@",[NSThread currentThread]);
    });
    NSLog(@"任务3:%@",[NSThread currentThread]);
}

运行结果

复制代码
任务1:<_NSMainThread: 0x60000170c000>{number = 1, name = main}
任务3:<_NSMainThread: 0x60000170c000>{number = 1, name = main}
任务2:<_NSMainThread: 0x60000170c000>{number = 1, name = main}

异步执行不会阻塞当前主线程,主线程执行完任务1后,无需等待任务2完成,直接执行任务3。主队列是串行队列,所有任务仍在主线程排队执行,因此任务3执行完成后,再执行任务2,不会开启新线程、不会死锁。

同步+(全局队列或者自定义并发队列) (顺序执行、无新线程)

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_sync(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务一:%@",[NSThread currentThread]);
        }
    });
    
    dispatch_sync(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务二:%@",[NSThread currentThread]);
        }
    });
}

运行结果

复制代码
任务一:<_NSMainThread: 0x600001704000>{number = 1, name = main}
任务一:<_NSMainThread: 0x600001704000>{number = 1, name = main}
任务一:<_NSMainThread: 0x600001704000>{number = 1, name = main}
任务二:<_NSMainThread: 0x600001704000>{number = 1, name = main}
任务二:<_NSMainThread: 0x600001704000>{number = 1, name = main}
任务二:<_NSMainThread: 0x600001704000>{number = 1, name = main}

同步执行本身不具备开启新线程的能力,任务直接在主线程执行,且会阻塞线程,必须等待上一个任务完成才会执行下一个任务。虽然队列是并发队列,但受同步执行特性限制,任务只能顺序执行,且不会阻塞主线程正常运行。

异步+(全局队列或者自定义并发队列)(并发执行、开启新线程)

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务一:%@",[NSThread currentThread]);
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i<3; i++) {
            NSLog(@"任务二:%@",[NSThread currentThread]);
        }
    });
}

运行结果

复制代码
任务二:<NSThread: 0x600001726340>{number = 6, name = (null)}
任务一:<NSThread: 0x60000170dd00>{number = 5, name = (null)}
任务二:<NSThread: 0x600001726340>{number = 6, name = (null)}
任务一:<NSThread: 0x60000170dd00>{number = 5, name = (null)}
任务二:<NSThread: 0x600001726340>{number = 6, name = (null)}
任务一:<NSThread: 0x60000170dd00>{number = 5, name = (null)}

可以看出任务1和任务2交错执行,并非同步执行那样执行完任务1再执行任务2。而且通过线程编号可以看出,的确开启了新的线程。说明异步执行具备开启新线程的能力。

但是通过上面异步+主队列的打印结果我们可以发现,在主队列时,异步执行也并没有开启新的线程,而仍然是同一个主线程。说明如果在主队列中异步执行,是不会开启新线程的

原因是因为主队列是串行队列,必须执行完一个任务再执行另一个任务,异步执行只是没有造成堵塞,但是在主队列还是是要一步步走的。

同一串行队列嵌套

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{ 
        NSLog(@"任务2");
        dispatch_sync(queue, ^{ 
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果

复制代码
任务1
任务5
任务2
然后崩溃

这里造成死锁崩溃的原因和上面同步+串行队列的原因一样。由于主队列是串行的,所以代码必然是从上到下依次执行的。

打印完任务1后,执行dispatch_async为异步,不会阻塞线程,但是会加入自定义的串行队列中,所以会执行任务5,接着执行异步代码块中逻辑,打印任务2,接着执行dispatch_sync代码块,但是由于其是同步,串行队列中已经有了dispatch_async任务未执行完毕,此时dispatch_sync阻塞当前线程等待任务3执行,造成了相互等待,所以就死锁崩溃了

不同串行队列嵌套

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("myQueue2", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue2, ^{
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果

复制代码
任务1,任务5,任务2,任务3,任务4

当嵌套执行时,同步异步在不同的串行队列执行,并不会造成死锁崩溃。而是按照串行的顺序,执行代码。这事因为dispatch_sync阻塞的并不是当前的线程,而是其他的线程,所以不会造成死锁等待。

同一并发队列嵌套

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue, ^{
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果

复制代码
任务1,任务5,任务2,任务3,任务4

主线程执行任务1之后,需要异步dispatch_async执行任务2;所以先执行主线程的任务5,然后执行任务2;接着需要在并发队列中同步dispatch_sync执行任务3,所以会造成阻塞,等待其执行完成,然后执行并发队列中的任务4。

不同并发、串行队列嵌套

objc 复制代码
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"任务1");
    dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t queue2 = dispatch_queue_create("myQueue2", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"任务2");
        dispatch_sync(queue2, ^{ 
            NSLog(@"任务3");
        });
        NSLog(@"任务4");
    });
    NSLog(@"任务5");
}

运行结果:

复制代码
任务1,任务5,任务2,任务3,任务4

创建了新的队列,所以是不会造成当前队列的阻塞的。

四、总结

  • 同步执行(sync) :无新开线程能力,阻塞当前线程,任务严格顺序执行;同一串行队列嵌套同步任务,必然触发死锁
  • 异步执行(async):具备新开线程能力,不阻塞当前线程;在并发队列中任务无序并行执行,在串行队列中任务有序排队执行
  • 主队列特殊规则:无论同步还是异步执行,均不会开启新线程;同步执行会死锁,异步执行仅延迟执行、不崩溃
  • 队列嵌套核心:同一串行队列嵌套同步任务必死锁;不同队列、并发队列嵌套,均不会触发死锁
相关推荐
我是小白呀1 小时前
编排还是事件协作?用 AcmeFlow 讲清企业 Workflow 的控制权
java·开发语言
ACP广源盛139246256731 小时前
接口适配方案选型|GSV6125@ACP HDMI2.0 转 Type‑C/DP1.4 桥接芯片工程评估
大数据·c语言·开发语言·硬件架构·硬件工程·国产芯片
黑科技iOS上架1 小时前
小蟹iOS编译级混淆与定向加固配合使用完整教程
ios·审核·ios混淆·执行程序差异化
传奇开心果编程1 小时前
【Flutter入门练中学】第3课:滚动与列表
android·学习·flutter·ui·ios
小羊没烦恼!1 小时前
【译】利用Asp.net MVC处理文件的上传下载
开发语言·缓存·c#·word·powerpoint·.net
for_ever_love__1 小时前
iOS: CABasicAnimation动画
macos·ios·cocoa
时间的拾荒人2 小时前
Qt 信号与槽机制详解(二):连接方式详解与面试指南
开发语言·qt·面试
霸道流氓气质2 小时前
LangGraph式图编排引擎Java实现:状态机模型、Checkpoint恢复与生产级落地实战
java·开发语言·数据库
2601_962218612 小时前
C++ AVL树概念与实现详解
开发语言·c++