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

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函数底层)