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);
相关推荐
二流小码农9 小时前
鸿蒙开发:异步并发操作
android·ios·harmonyos
MavenTalk12 小时前
前端技术选型之uniapp
android·前端·flutter·ios·uni-app·前端开发
豪冷啊14 小时前
iOS 17.4 Not Installed
ios
Topstip14 小时前
iOS 19 重大更新泄露,将带来更“聪明”的 Siri 挑战 ChatGPT
人工智能·ios·ai·chatgpt
软件聚导航2 天前
uniapp 安卓和ios震动方法,支持息屏和后台震动,ios和安卓均通过测试
android·ios·uni-app
键盘敲没电2 天前
【iOS】知乎日报总结
学习·ios·objective-c·xcode
安和昂3 天前
【iOS】UICollectionView的学习
学习·ios·cocoa
二流小码农3 天前
鸿蒙开发:自定义一个任意位置弹出的Dialog
android·ios·harmonyos
小江村儿的文杰4 天前
UE4 iOS Package的过程与XCode工程中没有游戏Content的原因
macos·ios·ue4·xcode
__WanG4 天前
Flutter将应用打包发布到App Store
前端·flutter·ios