iOS 多线程(二):GCD

1.为什么用GCD

GCD 是苹果公司为多核的并行运算提出的解决方案。

  • 它会自动利用更多的CPU内核(比如双核、四核),
  • 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)

程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。

2.任务(同步异步) & 队列(串行并发)

任务 :就是你在线程中执行的那段代码。执行任务有两种方式:『同步执行』『异步执行』 。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

  • 同步 在当前线程中执行任务,任务未执行完时,会阻塞线程,不会开辟线程。使用方法如下
objc 复制代码
dispatch_sync(queue, ^{
   // do something
});
  • 异步 可以在新的线程中执行任务,具备开启新线程的能力。使用方法如下
objc 复制代码
dispatch_async(queue, ^{
   // do something
});

GCD 中有两种队列:『串行队列』『并发队列』,两者都符合 FIFO(先进先出)的原则。

  • 串行 只开启一个线程,一个任务执行完毕后,再执行下一个任务。
  • 并发 可以开启多个线程,并且同时执行任务。
objc 复制代码
/*
  第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。
队列的名称推荐使用应用程序 ID 这种逆序全程域名。
  第二个参数用来识别是串行队列还是并发队列。
`DISPATCH_QUEUE_SERIAL` 表示串行队列,`DISPATCH_QUEUE_CONCURRENT` 表示并发队列。
*/
dispatch_queue_t queue = dispatch_queue_create("com.test.company", DISPATCH_QUEUE_SERIAL);

注意:并发队列 的并发功能只有在异步(dispatch_async)方法下才有效。

3.组合使用

同步串行

任务一个接一个执行,不开辟线程

objc 复制代码
/**
 串行队列,同步执行
 */
- (void)createSerialQueueWithSync {
    dispatch_queue_t serialQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_SERIAL);
    dispatch_sync(serialQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    dispatch_sync(serialQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_sync(serialQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

结果如下

同步并发

任务一个接一个执行,不开辟线程

objc 复制代码
/**
 并行队列,同步执行
 */
- (void)createConcurrentQueueWithSync {
    dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_CONCURRENT);
    //    dispatch_queue_t conCurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);//这个是全局并行队列,一般并行任务都会加到这里面去
    
    dispatch_sync(conCurrentQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    });
    dispatch_sync(conCurrentQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_sync(conCurrentQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

打印结果如下

异步串行

任务一个接一个执行,会开辟线程。

objc 复制代码
/**
 串行队列,异步执行
 */
- (void)createSerialQueueWithAsync {
    dispatch_queue_t serialQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_SERIAL);
    dispatch_async(serialQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_async(serialQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

打印结果如下

异步并发

任务乱序执行,开辟线程。

objectivec 复制代码
/**
 并行队列,异步执行
 */
- (void)createConcurrentQueueWithAsync {
    dispatch_queue_t conCurrentQueue = dispatch_queue_create("com.neusoft", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"A==%@", [NSThread currentThread]);
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"B==%@", [NSThread currentThread]);
    });
    dispatch_async(conCurrentQueue, ^{
        NSLog(@"C==%@", [NSThread currentThread]);
    });
    NSLog(@"D==%@", [NSThread currentThread]);
}

打印结果如下

4.GCD其他用法

dispatch_barrier

我们有时需要执行两组操作,且要求第一组(任务1、2、3)操作执行完之后,才能开始执行第二组(任务4、5、6)操作。这时就需要用到栅栏函数,栅栏函数的作用如下图所示

从图中我们可以看出

  • 对于串行队列来说,任务是一个接一个执行的,所以对串行队列使用栅栏函数意义不大
  • 对于并发队列来说,任务是乱序执行的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序

使用

举例如下

objc 复制代码
- (void)test {
    dispatch_queue_t queue = dispatch_queue_create("Felix", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"开始------%@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s的任务1------%@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束------%@", [NSThread currentThread]);
    
//    dispatch_barrier_async(queue, ^{
//        NSLog(@"----------栅栏任务----------%@", [NSThread currentThread]);
//    });
//    NSLog(@"栅栏结束------%@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        sleep(1);
        NSLog(@"延迟1s的任务2------%@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束------%@", [NSThread currentThread]);
}

不使用栅栏函数

ini 复制代码
开始------<NSThread: 0x600002384f00>{number = 1, name = main}
第一次结束------<NSThread: 0x600002384f00>{number = 1, name = main}
第二次结束------<NSThread: 0x600002384f00>{number = 1, name = main}
延迟1s的任务2------<NSThread: 0x6000023ec300>{number = 5, name = (null)}
延迟2s的任务1------<NSThread: 0x60000238c180>{number = 7, name = (null)}

使用栅栏函数

ini 复制代码
开始------<NSThread: 0x600000820bc0>{number = 1, name = main}
第一次结束------<NSThread: 0x600000820bc0>{number = 1, name = main}
栅栏结束------<NSThread: 0x600000820bc0>{number = 1, name = main}
第二次结束------<NSThread: 0x600000820bc0>{number = 1, name = main}
延迟2s的任务1------<NSThread: 0x600000863c80>{number = 4, name = (null)}
----------栅栏任务----------<NSThread: 0x600000863c80>{number = 4, name = (null)}
延迟1s的任务2------<NSThread: 0x600000863c80>{number = 4, name = (null)}

总结一句话就是: 先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务

问题

1、dispatch_barrier_asyncdispatch_barrier_sync有什么区别,该用那个?

两者作用相同,都是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务。但是dispatch_barrier_sync会阻塞线程,影响后面的任务执行(尽量少用)。

比如将上例的dispatch_barrier_async改成dispatch_barrier_sync,结果如下

ini 复制代码
开始------<NSThread: 0x600001040d40>{number = 1, name = main}
第一次结束------<NSThread: 0x600001040d40>{number = 1, name = main}
延迟2s的任务1------<NSThread: 0x60000100ce40>{number = 6, name = (null)}
----------栅栏任务----------<NSThread: 0x600001040d40>{number = 1, name = main}
栅栏结束------<NSThread: 0x600001040d40>{number = 1, name = main}
第二次结束------<NSThread: 0x600001040d40>{number = 1, name = main}
延迟1s的任务2------<NSThread: 0x60000100ce40>{number = 6, name = (null)}

注意:栅栏函数必须是自定义的并发队列才有效,且必须是同一队列中的线程才有效。

读写锁

读写锁 在读多写少的场景效率很高,它的定义是:同一时间有多个读者,同一时间只能有一个写者,读者和写者不能同时存在

工作原理:读写锁 实际是一种特殊的自旋锁 ,如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里, 直到没有任何写者或读者。

写的那个时间段,不能有任何读者+其他写者,因此根据栅栏函数原理,我们可以把读写操作理解成下图

objc 复制代码
- (id)readDataForKey:(NSString*)key {
    __block id value;
    dispatch_sync(_concurrentQueue, ^{
        value = [self valueForKey:key];
    });
    return value;
}

- (void)writeData:(id)data forKey:(NSString*)key {
    dispatch_barrier_async(_concurrentQueue, ^{
        [self setValue:data forKey:key];
    });
}

为什么用dispatch_sync完成读,用dispatch_barrier_async完成写?

  • 读:使用dispatch_sync,会使 value 获取到值后,再返回给读者
    • 若使用dispatch_async则会先返回空的result value,再通过[self valueForKey:key]方法获取到值
  • 写:写的那个时间段,不能有任何读者+其他写者,dispatch_barrier_async满足等队列中前面的读写任务都执行完了再来执行当前任务,而不阻塞当前线程

dispatch_group

调度组有以下能力

  1. 将任务分组执行dispatch_group_enter(group), dispatch_group_leave(group)
  2. 能监听任务组完成dispatch_notify(group, queue, ^{});
  3. 并设置等待时间dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

dispatch_group_async

objc 复制代码
- (void)createGroupQueue {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_async(group, queue, ^{
        NSLog(@"A---%@", [NSThread currentThread]);
    });
    
    dispatch_group_async(group, queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"B---%@", [NSThread currentThread]);
        }
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"C---%@", [NSThread currentThread]);
    });
    
    dispatch_notify(group, queue, ^{
        NSLog(@"队列组任务执行完毕");
    });
}

dispatch_group_enter & dispatch_group_leave

objc 复制代码
- (void)test2 {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"A---%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        for (int i = 0; i < 5; i++) {
            NSLog(@"B---%@", [NSThread currentThread]);
        }
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"C---%@", [NSThread currentThread]);
        dispatch_group_leave(group);
    });
    
    dispatch_notify(group, queue, ^{
        NSLog(@"队列组任务执行完毕");
    });
}

dispatch_group_wait

dispatch_group_wait可以设置调度组的等待时间,这也意味着会阻塞当前线程,等待调度组完成任务,若在指定时间内完成任务,会返回 0;否则返回非 0。

语法:long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)

  • group:需要等待的调度组
  • timeout:等待的超时时间(即等多久)
    • 设置为DISPATCH_TIME_NOW意味着不等待直接判定调度组是否执行完毕
    • 设置为DISPATCH_TIME_FOREVER则会阻塞当前调度组,直到调度组执行完毕
    • 设置具体超时时间dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC))
  • 返回值:为long类型
    • 0 表示在指定时间内调度组完成了任务
    • 非0 表示指定时间内调度组没有按时完成任务

举例如下

objc 复制代码
- (void)test {
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"请求一完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"请求二完成");
        dispatch_group_leave(group);
    });
    
//    long timeout = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    // 设置 1 秒超时,在这 1 秒期间会阻塞当前线程
    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(@"通知,所有请求完成");
    });
}

结果如下

ini 复制代码
timeout=49 // 49 就是非 0,即超时返回错误
超时
请求一完成
请求二完成
通知,所有请求完成

dispatch_semaphore

信号量可用于控制 GCD 最大并发数,还可以当锁使用(信号量锁的效率也很高)。

  • dispatch_semaphore_create(intptr_t value);:创建信号量
  • dispatch_semaphore_wait(semaphore, dispatch_time_t timeout):等待信号量,信号量-1。当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作(如果永久等待 将等到信号(signal)才执行下去)
  • dispatch_semaphore_signal(semaphore);:释放信号量,信号量+1。当信号量>= 0 会执行wait之后的代码

比如AFURLSessionManager.m中的tasksForKeyPath:方法。通过引入信号量的方式,等待异步执行任务结果,获取到 tasks,然后再返回该 tasks。

objc 复制代码
- (NSArray *)tasksForKeyPath:(NSString *)keyPath {
    __block NSArray *tasks = nil;
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
        if ([keyPath isEqualToString:NSStringFromSelector(@selector(dataTasks))]) {
            tasks = dataTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(uploadTasks))]) {
            tasks = uploadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(downloadTasks))]) {
            tasks = downloadTasks;
        } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(tasks))]) {
            tasks = [@[dataTasks, uploadTasks, downloadTasks] valueForKeyPath:@"@unionOfArrays.self"];
        }

        dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    return tasks;
}

dispatch_once

我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 方法。使用 dispatch_once 方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。

objc 复制代码
/**
 * 一次性代码(只执行一次)dispatch_once
 */
- (void)once {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 只执行 1 次的代码(这里面默认是线程安全的)
    });
}

有时dispatch_once可用作优化,类似程序运行期间要多次获取的值,可以只执行一次耗时操作,然后把值存起来供下次直接使用,比如下例中获取设备等级的代码

objc 复制代码
+ (NSInteger)getDeviceLevel {
    static dispatch_once_t deviceLevelOnce;
    static NSInteger deviceLevel;
    
    dispatch_once(&deviceLevelOnce, ^{
        struct utsname systemInfo;
        uname(&systemInfo);
        NSString *deviceModel = [NSString stringWithCString:systemInfo.machine encoding:NSASCIIStringEncoding];
        
        NSBundle *bundle = [NSBundle c4_localizedBundle];
        NSString *path = [bundle pathForResource:@"Apple_mobile_device_types" ofType:@"plist"];
        NSDictionary *deviceDict = [NSDictionary dictionaryWithContentsOfFile:path];
        NSDictionary *dict = deviceDict[deviceModel];
        NSString *level = dict[@"level"];
        if (!level) {
            deviceLevel = 4;
        } else {
            deviceLevel = level.integerValue;
        }
    });
    
    return deviceLevel;
}

dispatch_apply

objc 复制代码
//快速迭代方法
dispatch_apply(size_t iterations,
               dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue,
               DISPATCH_NOESCAPE void (^block)(size_t iteration));

当 queue 是串行队列时,它的作用和 for 循环一样,所以一般用于并行队列。

使用方式如下

objc 复制代码
- (void)testApply {
    NSLog(@"dispatch_apply 开始执行");
    dispatch_apply(6, dispatch_get_global_queue(0, 0), ^(size_t iteration) {
        NSLog(@"%zd---%@", iteration, [NSThread currentThread]);
    });
    NSLog(@"dispatch_apply 结束执行");
}

结果如下

ini 复制代码
dispatch_apply 开始执行
1---<NSThread: 0x600003340d40>{number = 9, name = (null)}
2---<NSThread: 0x600003334000>{number = 11, name = (null)}
3---<_NSMainThread: 0x600003328080>{number = 1, name = main}
4---<NSThread: 0x60000331a4c0>{number = 12, name = (null)}
0---<NSThread: 0x60000331a3c0>{number = 10, name = (null)}
5---<NSThread: 0x600003348f40>{number = 13, name = (null)}
dispatch_apply 结束执行

无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

dispatch_source

常用应用场景是 GCD Timer。

一般使用NSTimer来处理定时逻辑,但NSTimer是依赖 Runloop 的,而 Runloop 可以运行在不同的模式下。如果NSTimer添加在一种模式下,当 Runloop 运行在其他模式下的时候,定时器就挂机了;又如果 Runloop 在阻塞状态,NSTimer触发时间就会推迟到下一个 Runloop 周期。因此NSTimer在计时上会有误差,并不是特别精确,而 GCD 定时器不依赖 Runloop ,计时精度要高很多

常用 API

  • dispatch_source_create: 创建事件源
  • dispatch_source_set_event_handler: 设置数据源回调
  • dispatch_source_merge_data: 设置事件源数据
  • dispatch_source_get_data: 获取事件源数据
  • dispatch_resume: 继续
  • dispatch_suspend: 挂起
  • dispatch_cancle: 取消

创建 GCD Timer 例子,摘自 YYTimer

objc 复制代码
@property (nonatomic, strong) dispatch_source_t timer;
//创建timer
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
//设置timer首次执行时间,间隔,精确度
dispatch_source_set_timer(_source, dispatch_time(DISPATCH_TIME_NOW, (start * NSEC_PER_SEC)), (interval * NSEC_PER_SEC), 0);
//设置timer事件回调
__weak typeof(self) _self = self;
dispatch_source_set_event_handler(_source, ^{
    [_self fire];
});
//默认是挂起状态,需要手动激活
dispatch_resume(_source);
相关推荐
HarderCoder2 小时前
iOS 知识积累第一弹:从 struct 到 APP 生命周期的全景复盘
ios
叽哥13 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h3 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa