美文网首页iOS开发APP & program大前端开发
iOS 多线程原理 - GCD函数底层

iOS 多线程原理 - GCD函数底层

作者: 顶级蜗牛 | 来源:发表于2022-05-28 17:25 被阅读0次

    libdispatch-1271.120.2 下载
    苹果官方资源opensource

    多线程相关文献:
    iOS 多线程原理 - 线程与队列底层
    iOS 多线程原理 - GCD函数底层
    iOS 线程底层 - 锁

    本章节探究:
    1.单例 dispatch_once
    2.栅栏函数 barrier
    3.调度组 group
    4.信号量 semaphore
    5.dispatch_source

    前言

    在了解了线程与队列的底层原理之后,本章节来看看GCD函数的底层原理,研究这些API是怎么调用的,并附上使用案例。

    一、单例

    + (SingleExample *)shareInstance {
        static SingleExample *single = nil;
        static dispatch_once_t onceToken ;
        dispatch_once(&onceToken, ^{
            single = [[SingleExample alloc] init];
        }) ;
        return single;
    }
    

    来看看dispatch_once这个函数原理。
    打开libdispatch源码

    dispatch_once的源码声明:

    dispatch_once

    dispatch_once_t *val它里面有一个状态的记录,来保证block只被调用一次。

    dispatch_once_f的源码声明:

    dispatch_once_f

    需要看看执行func的要出于什么条件下才会被执行 (_dispatch_once_gate_tryenter

    _dispatch_once_gate_tryenter的源码声明:

    _dispatch_once_gate_tryenter

    而等待是怎么等的呢?(_dispatch_once_wait
    _dispatch_once_wait的源码声明:

    在死循环里不断地查询单例状态,一旦任务执行完毕才跳出循环。

    单例总结:
    1.线程安全
    2.任务只会被执行一次
    3.通过一个状态值来保证任务是否被执行过

    二、栅栏函数

    同步栅栏函数dispatch_barrier_sync案例:

    - (void)test_barrier {
        dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(t, ^{
            NSLog(@"1");
        });
        dispatch_async(t, ^{
            NSLog(@"2");
        });
        // 栅栏函数
        dispatch_barrier_sync(t, ^{
            sleep(2);
            NSLog(@"%@", [NSThread currentThread]); // main
            NSLog(@"3");
        });
        
        NSLog(@"4");
        
        dispatch_async(t, ^{
            NSLog(@"5");
        });
    }
    // 12顺序不一定;3一定在12后面;45在3后面;45顺序不一定
    // 同步栅栏dispatch_barrier_sync 和 普通的同步dispatch_sync效果是一样的
    

    异步栅栏函数dispatch_barrier_async案例:

    - (void)test_barrier {
        dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(t, ^{
            NSLog(@"1");
        });
        dispatch_async(t, ^{
            NSLog(@"2");
        });
        // 异步栅栏函数
        dispatch_barrier_async(t, ^{
            sleep(2);
            NSLog(@"3");
        });
        
        NSLog(@"4");
        
        dispatch_async(t, ^{
            NSLog(@"5");
        });
    }
    // 124顺序不一定,3一定在12后面,5一定在3后面
    // 异步栅栏函数只能栅得住非全局队列的任务
    

    全局队列案例

    - (void)test_barrier_global {
        dispatch_queue_t t = dispatch_get_global_queue(0, 0);
        dispatch_async(t, ^{
            NSLog(@"1");
        });
        dispatch_async(t, ^{
            NSLog(@"2");
        });
        // 栅栏函数
        dispatch_barrier_async(t, ^{
            sleep(2);
            NSLog(@"3");
        });
        
        NSLog(@"4");
        
        dispatch_async(t, ^{
            NSLog(@"5");
        });
    }
    
    // 1245没有顺序 3在最后
    // 异步栅栏函数栅不住全局队列里的任务
    

    栅栏函数分为同步栅栏异步栅栏
    dispatch_barrier_async在自定义的并发队列里,全局和串行达不到我们要的效果。
    苹果文档中指出,如果使用的是全局队列或者创建的不是并发队列,则dispatch_barrier_async实际上就相当于dispatch_async

    1.同步栅栏dispatch_barrier_sync

    其实同步栅栏与普通同步实现的效果是差不多的,在源码上只有一点点小差异。

    dispatch_barrier_sync _dispatch_barrier_sync_f _dispatch_barrier_sync_f_inline

    _dispatch_barrier_sync_f_inline里会判断不同的队列条件来去选择分支继续往下走,这里我以并发队列为例,它会走_dispatch_sync_f_slow代码分支:

    _dispatch_sync_f_slow

    以并发队列为例,它会走_dispatch_sync_invoke_and_complete_recurse代码分支:

    _dispatch_sync_invoke_and_complete_recurse

    _dispatch_sync_function_invoke_inline和dispatch_sync底层一样的,是去调用func
    栅栏函数完成任务后会执行_dispatch_sync_complete_recurse唤醒队列里后续的任务。

    _dispatch_sync_complete_recurse

    _dispatch_sync_complete_recurse里通过do...while去唤醒队列里的任务dx_wakeup
    dx_wakeup是一个dq_wakeup的宏定义:

    #define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
    
    dq_wakeup

    (栅栏函数栅不住全局队列的原因就在这里,因为它指定的wakeup函数不一样。)

    唤醒以并发队列为例,它会走_dispatch_lane_wakeup

    _dispatch_lane_wakeup

    为barrier形式,调用_dispatch_lane_barrier_complete

    _dispatch_lane_barrier_complete
    • 如果是串行队列,则会进行等待,等待其他的任务执行完成,再按顺序执行;
    • 如果是并发队列,则会调用_dispatch_lane_drain_non_barriers方法将栅栏之前的任务执行完成;
    • 最后会调用_dispatch_lane_class_barrier_complete方法,也就是把栅栏拔掉了,不拦了,从而执行栅栏之后的任务。

    唤醒以全局并发队列为例,它会走_dispatch_root_queue_wakeup
    它里面就没有拦截有关栅栏函数相关的东西。

    _dispatch_root_queue_wakeup

    同步栅栏函数dispatch_barrier_sync和普通同步函数dispatch_sync效果是一样的:
    阻塞当前线程,不开辟线程,立即执行,同步栅栏函数还需要等栅栏任务完成后唤醒非全局队列后续的任务

    为什么苹果设计栅栏函数栅不住全局并发队列?
    因为我们系统也会使用全局并发队列,避免造成系统任务被阻塞。

    2.异步栅栏dispatch_barrier_async
    dispatch_barrier_async
    • _dispatch_continuation_init保存任务
    _dispatch_continuation_init

    _dispatch_continuation_init保存了任务,在需要执行的时候拿出来执行

    _dispatch_continuation_init
    • _dispatch_continuation_async
    _dispatch_continuation_async

    dx_push是宏定义:

    #define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
    

    找到dq_push的声明:

    dq_push

    根据不同的队列赋值给dq_push不一样的函数
    以并发队列为例:

    _dispatch_lane_concurrent_push的源码声明:

    _dispatch_lane_concurrent_push就是栅栏异步与普通异步函数的分支:

    _dispatch_lane_concurrent_push _dispatch_lane_push

    走到dx_wakeup函数,这里在同步栅栏部分已经介绍过了。

    栅栏函数总结:
    1.栅栏函数只针对非全局队列;
    2.栅栏函数不能栅住全局队列,因为系统也在用它,防止阻塞住系统任务;
    3.栅栏函数需要等待当前队列前面的任务执行完,再去执行栅栏任务,最后唤醒执行栅栏任务后面的任务

    三、调度组

    - (void)test_group {
        dispatch_group_t group = dispatch_group_create();
        dispatch_queue_t que1 = dispatch_queue_create("An", DISPATCH_QUEUE_CONCURRENT);
        dispatch_queue_t que2 = dispatch_queue_create("Lin", DISPATCH_QUEUE_CONCURRENT);
        
        dispatch_group_enter(group);
        dispatch_async(que1, ^{
            sleep(4);
            NSLog(@"1");
            dispatch_group_leave(group);
        });
    
        dispatch_group_enter(group);
        dispatch_async(que2, ^{
            sleep(3);
            NSLog(@"2");
            dispatch_group_leave(group);
        });
        
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_global_queue(0, 0 ), ^{
            sleep(2);
            NSLog(@"3");
            dispatch_group_leave(group);
        });
        
        dispatch_group_enter(group);
        dispatch_async(dispatch_get_main_queue(), ^{
            sleep(1);
            NSLog(@"4");
            dispatch_group_leave(group);
        });
    
        dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
            NSLog(@"6");
        });
    
        dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
            NSLog(@"5");
        });
    }
    // 5在4321任务之后
    

    当然也可以使用dispatch_group_async来代替dispatch_group_enterdispatch_group_leave;效果是一样的。

    研究的对象有三个:dispatch_group_enterdispatch_group_leavedispatch_group_notify

    1. dispatch_group_enter的源码分析
    dispatch_group_enter

    ps: 这里 DISPATCH_GROUP_VALUE_INTERVAL = 0x0000000000000004ULL
    注释里说的 0->-1 跃迁的进位其实是位运算。实际上是+4

    苹果官方文档dispatch_group_enter的解释:
    调用此函数将增加组中当前未完成任务的计数。如果应用程序通过dispatch_group_async函数以外的方式显式地从组中添加和删除任务,那么使用这个函数(与dispatch_group_leave一起使用)允许您的应用程序正确地管理任务引用计数。对这个函数的调用必须与对dispatch_group_leave的调用相平衡。您可以使用此函数同时将一个块与多个组关联。

    2. dispatch_group_leave的源码分析
    dispatch_group_leave

    苹果官方文档dispatch_group_leave的解释:
    调用此函数将减少组中当前未完成任务的计数。如果应用程序通过dispatch_group_async函数以外的方式显式地从组中添加和删除任务,那么使用这个函数(与dispatch_group_enter一起使用)允许您的应用程序正确地管理任务引用计数。
    对该函数的调用必须平衡对dispatch_group_enter的调用。调用它的次数超过dispatch_group_enter是无效的,这会导致负的计数。

    3.dispatch_group_notify的源码分析
    dispatch_group_notify dispatch_group_notify

    dispatch_group_enter通过改变调度组状态值+4;dispatch_group_leave通过调度组状态值-4;dispatch_group_notify在调度组内不断地获取调度组状态值,如果状态值达到平衡(等于0),则说明前面的任务做完了,需要执行notify里的任务。

    调度组总结:
    1.dispatch_group_enterdispatch_group_leave必须成对使用;
    2.dispatch_group_leave次数多于dispatch_group_enter会导致崩溃;
    3.调度组底层是通过修改调度组的状态值的增(enter)减(leave),不断地监听这个状态值是否达到平衡(等于0),一旦平衡则去执行dispatch_group_notify里的任务。

    四、信号量

    - (void)test_semaphore {
        // 设置0,任务1不需要等;设置1,任务1和2不需要等...
        dispatch_semaphore_t sem = dispatch_semaphore_create(0);
        
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"1");
            dispatch_semaphore_signal(sem); 
        });
        
        // 等待任务1的signal
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            sleep(2);
            NSLog(@"2");
            dispatch_semaphore_signal(sem);
        });
        
         // 等待任务2的signal
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"3");
            dispatch_semaphore_signal(sem);
        });
        
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"4");
            dispatch_semaphore_signal(sem);
        });
        
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"5");
            dispatch_semaphore_signal(sem);
        });
    }
    // 12345
    
    • dispatch_semaphore_create 创建信号量,指定信号量数值
    • dispatch_semaphore_signal 发送信号量,将信号量数值+1
    • dispatch_semaphore_wait 等待信号量;当信号量数值为0时,阻塞当前线程一直等待;当信号量数值大于等于1时,将信号量数值-1并执行当前线程的任务
    1.dispatch_semaphore_create的源码声明:
    dispatch_semaphore_signal声明 dispatch_semaphore_signal实现
    • 当两个线程需要协调特定事件的完成时,为该值传递0很有用;
    • 传递大于0的值对于管理有限的资源池很有用,其中池大小等于该值;
    • 信号量的起始值传递小于信号量的起始值。 传递小于零的值将导致返回 NULL,也就是小于0就不会正常执行

    总的来说:信号量初始值可以控制线程池中的最多并发数量

    2.dispatch_semaphore_signal的源码声明:
    dispatch_semaphore_signal

    os_atomic_inc2o原子操作自增加1,然后会判断,如果value > 0,就会返回0;
    加一次后依然小于0就报异常 Unbalanced call to dispatch_semaphore_signal(),然后会调用_dispatch_semaphore_signal_slow做容错的处理。

    _dispatch_semaphore_signal_slow _dispatch_sema4_signal
    3. dispatch_semaphore_wait的源码声明:
    dispatch_semaphore_wait
    • os_atomic_dec2o进行原子自减1操作,也就是对value值进行减操作,控制可并发数。
    • 如果可并发数为2,则调用该方法后,变为1,表示现在并发数为 1,剩下还可同时执行1个任务,不会执行_dispatch_semaphore_wait_slow去等待。
    • 如果初始值是0,减操作之后为负数,则会调用_dispatch_semaphore_wait_slow方法。

    看看_dispatch_semaphore_wait_slow的等待逻辑

    _dispatch_semaphore_wait_slow _dispatch_sema4_wait

    一个do-while循环,当不满足条件时,会一直循环下去,从而导致流程的阻塞。
    上面举例里面就相当于,下图中的情况:

    举例

    信号量总结:
    1.dispatch_semaphore_wait 信号量等待,内部是对并发数做自减1操作,如果小于0,会执行_dispatch_semaphore_wait_slow然后调用_dispatch_sema4_wait是一个do-while,直到满足条件结束循环。
    2.dispatch_semaphore_signal 信号量释放 ,内部是对并发数做自加1操作,直到大于0时,为可操作。
    3.保持线程同步,将异步执行任务转换为同步执行任务。
    4.保证线程安全,为线程加锁,相当于自旋锁。

    五、dispatch_source

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

    定时器监听
    倒计时案例:

    - (void)iTimer {
        __block int timeout = 60;
        
        dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
        dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_source_set_timer(_timer,dispatch_walltime(NULL, 0),1.0*NSEC_PER_SEC, 0);
        dispatch_source_set_event_handler(_timer, ^{
            if(timeout <= 0) {
                dispatch_source_cancel(_timer);
            } else {
                timeout--;
                NSLog(@"倒计时:%d", timeout);
            }
        });
        dispatch_resume(_timer);
    }
    

    自定义事件,变量增加
    变量增加案例:

    #import "ViewController.h"
    
    @interface ViewController ()
    
    @property (weak, nonatomic) IBOutlet UIButton *iBt;
    @property (weak, nonatomic) IBOutlet UIProgressView *iProgress;
    @property (nonatomic, strong) dispatch_source_t source;
    @property (nonatomic, strong) dispatch_queue_t queue;
    @property (nonatomic, assign) NSUInteger totalComplete;
    @property (nonatomic ,assign) int iNum;
    
    @end
    
    @implementation ViewController
    
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.totalComplete = 0;
        self.queue = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
        self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
        dispatch_source_set_event_handler(self.source, ^{
            NSUInteger value = dispatch_source_get_data(self.source); // 每次去获取iNum的值
            self.totalComplete += value;
            NSLog(@"进度: %.2f",self.totalComplete/100.0);
            self.iProgress.progress = self.totalComplete/100.0;
        });
        
    //    [self iTimer];
    }
    
    - (IBAction)btClick:(id)sender {
        if ([self.iBt.titleLabel.text isEqualToString:@"开始"]) {
            dispatch_resume(self.source);
            NSLog(@"开始了");
            self.iNum = 1;
            [sender setTitle:@"暂停" forState:UIControlStateNormal];
            
            for (int i= 0; i<1000; i++) {
                dispatch_async(self.queue, ^{
                    sleep(1);
                    dispatch_source_merge_data(self.source, self.iNum); // 传递iNum触发hander
                });
            }
        } else {
            dispatch_suspend(self.source);
            NSLog(@"暂停了");
            self.iNum = 0;
            [sender setTitle:@"开始" forState:UIControlStateNormal];
        }
    }
    @end
    

    附上dispatch_source的Demo

    相关文章

      网友评论

        本文标题:iOS 多线程原理 - GCD函数底层

        本文链接:https://www.haomeiwen.com/subject/pkzmprtx.html