iOS 多线程(四):GCD源码分析下(栅栏、信号量、调度组、dispatch_source)

622 阅读9分钟

准备

一、栅栏函数的应用

作用

  • 控制任务执行顺序,同步。

函数

  • dispatch_barrier_async:前面的任务执行完毕才会来到这里,这里执行完毕才会执行后边的任务。
  • dispatch_barrier_sync:作用相同,但是这个会堵塞线程,影响后面的任务执行。
  • 非常重要的一点:栅栏函数只能控制同一并发队列。

dispatch_barrier_async 示例

- (void)myDemo {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT);
    
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"任务1");
    });
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"任务2");
    });
    // 栅栏函数
    dispatch_barrier_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"------barrier------");
    });
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务3");
    });
    
    NSLog(@"--------干干干--------");
}

执行结果:
--------干干干--------
任务1
任务2
-------barrier-------
任务3
  • 任务1任务2barrier中都是有延迟,但执行结果是先执行了任务1任务2,然后执行barrier,最后执行下面的任务3,确实如上面所说前面的任务执行完才会执行barrier和后面的任务。

dispatch_barrier_sync 示例

- (void)myDemo {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT);
    
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"任务1");
    });
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"任务2");
    });
    // 栅栏函数
    dispatch_barrier_sync(concurrentQueue, ^{
        sleep(1);
        NSLog(@"------barrier------");
    });
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务3");
    });
    
    NSLog(@"--------干干干--------");
}

执行结果:
任务2
任务1
------barrier------
--------干干干--------
任务3
  • 可以看到sync函数不仅有async函数的作用,还有堵塞线程的作用,主线程中的干干干也是在barrier后面才执行。

dispatch_get_global_queue 示例

将普通队列换成全局并发队列,查看执行结果:

- (void)myDemo {
    dispatch_queue_t concurrentQueue = dispatch_get_global_queue(0, 0);
    
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"任务1");
    });
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"任务2");
    });
    // 栅栏函数
    dispatch_barrier_async(concurrentQueue, ^{
        sleep(1);
        NSLog(@"------barrier------");
    });
    // 异步函数
    dispatch_async(concurrentQueue, ^{
        NSLog(@"任务3");
    });
    
    NSLog(@"--------干干干--------");
}

执行结果:
--------干干干--------
任务3
------barrier------
任务2
任务1
  • 可以看到在全局并发队列中,栅栏函数的作用失效了。

流程控制案例

看下面这个案例:

- (void)myDemo {
    dispatch_queue_t concurrentQueue = dispatch_queue_create("ssl", DISPATCH_QUEUE_CONCURRENT);
    // 多线程 操作marray
    for (int i = 0; i < 1000; i++) {
        dispatch_async(concurrentQueue, ^{
            NSString *imageName = [NSString stringWithFormat:@"%d.jpg", (i % 10)];
            NSURL *url = [[NSBundle mainBundle] URLForResource:imageName withExtension:nil];
            NSData *data = [NSData dataWithContentsOfURL:url];
            UIImage *image = [UIImage imageWithData:data];
            
            [self.mArray addObject:image];
        });
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"数组的个数:%zd",self.mArray.count);
}

执行结果:
数组的个数:996
  • 数组的个数不一定是固定的996,但是是小于1000的。
  • 因为这样操作线程是不安全的,当两个任务同时进行addObject操作时,假设这时mArray的数量是800,那么两个任务都会向801的位置进行赋值操作,其中的一个就会被覆盖,从而导致最终数组的个数小于1000

[self.mArray addObject:image]放到栅栏函数中,来保证线程的安全:

- (void)myDemo {
    ...
    dispatch_barrier_async(concurrentQueue , ^{
        [self.mArray addObject:image];
    });
    ...
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"数组的个数:%zd",self.mArray.count);
}

执行结果:
数组的个数:1000
  • 栅栏函数会保证前面的任务执行完毕,再执行后面的任务,任务会一个一个的执行,保证了线程的安全,所以最终数组的个数是1000

二、栅栏函数的底层原理

我们以同步的栅栏函数进行底层原理的分析。

栅栏函数之前的任务执行

进入dispatch_barrier_async

image.png

进入_dispatch_barrier_sync_f -> _dispatch_barrier_sync_f_inline -> _dispatch_sync_f_slow

image.png

符号断点跟流程:

image.png

  • 可以看到栅栏之前的任务是在__DISPATCH_WAIT_FOR_QUEUE__中执行的,这个函数在 上一篇 也提到过,死锁就是在这个函数中发生的。

栅栏函数之后的任务执行

接下来进入_dispatch_sync_invoke_and_complete_recurse -> _dispatch_sync_complete_recurse

static void
_dispatch_sync_complete_recurse(dispatch_queue_t dq, dispatch_queue_t stop_dq,
		uintptr_t dc_flags)
{
    bool barrier = (dc_flags & DC_FLAG_BARRIER);
    // 循环操作
    do {
        if (dq == stop_dq) return;
        // 是否存在栅栏
        if (barrier) {
            // 执行栅栏函数
            // #define dx_wakeup(x, y, z) dx_vtable(x)->dq_wakeup(x, y, z)
            dx_wakeup(dq, 0, DISPATCH_WAKEUP_BARRIER_COMPLETE);
        } else {
            // 再次进来时,没有栅栏函数,一些状态的修改
            _dispatch_lane_non_barrier_complete(upcast(dq)._dl, 0);
        }
        dq = dq->do_targetq;
        barrier = (dq->dq_width == 1);
    } while (unlikely(dq->do_targetq));
}

断点跟流程验证:

image.png

接下来会继续调用到_dispatch_sync_f_slow函数,完成所有任务的执行:

image.png

总结

  • 首先通过__DISPATCH_WAIT_FOR_QUEUE__函数将先加入队列的任务执行完。
  • 然后do while循环,判断如果有barrier,先执行barrier任务,最后再循环执行剩下的任务。

三、信号量的使用

作用

  • 同步->当锁,控制GCD最大并发数。

相关函数

  • dispatch_semaphore_create :创建信号量。
  • dispatch_semaphore_wait:信号量等待。
  • dispatch_semaphore_signal:信号量释放。

示例

- (void)myDemo {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_semaphore_t sem = dispatch_semaphore_create(1);

    //任务1
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
        sleep(2);
        NSLog(@"执行任务1");
        NSLog(@"任务1完成");
        dispatch_semaphore_signal(sem); // 发信号
    });
    //任务2
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
        sleep(1);
        NSLog(@"执行任务2");
        NSLog(@"任务2完成");
        dispatch_semaphore_signal(sem); // 发信号
    });
    //任务3
    dispatch_async(queue, ^{
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); // 等待
        NSLog(@"执行任务3");
        NSLog(@"任务3完成");
        dispatch_semaphore_signal(sem);
    });
    NSLog(@"任务4");
}

执行结果:

任务4
执行任务1
任务1完成
执行任务2
任务2完成
执行任务3
任务3完成
  • 任务1任务2都有延迟操作,它们本应该后执行的,因为加了信号量,任务却是从上到下执行的,任务1任务2反而先执行了。

四、信号量的底层原理

dispatch_semaphore_create

image.png

  • 可以看到信号量的创建主要就是给dsema_value赋值,赋值为我们传进来的值,接下来继续分析。

dispatch_semaphore_wait

intptr_t
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
{
    long value = os_atomic_dec2o(dsema, dsema_value, acquire);
    if (likely(value >= 0)) {
        return 0;
    }
    return _dispatch_semaphore_wait_slow(dsema, timeout);
}
  • dsema_value进行--操作,如果>= 0,函数返回0,任务可以执行。
  • 否则,返回_dispatch_semaphore_signal_slow函数。

进入_dispatch_semaphore_wait_slow

image.png

  • timeoutDISPATCH_TIME_FOREVER,将会进行等待。

进入_dispatch_sema4_wait函数:

image.png

  • 可以看到这里是一个do while循环,所以dispatch_semaphore_wait下面的代码不能执行。
  • dsema_value >= 0do while循环结束,任务又可以正常执行,这就涉及到了dsema_value++操作,下面来看dispatch_semaphore_signal函数。

dispatch_semaphore_signal

dispatch_semaphore_signal(dispatch_semaphore_t dsema)
{
    long value = os_atomic_inc2o(dsema, dsema_value, release);
    if (likely(value > 0)) {
        return 0;
    }
    if (unlikely(value == LONG_MIN)) {
        DISPATCH_CLIENT_CRASH(value,
                "Unbalanced call to dispatch_semaphore_signal()");
    }
    return _dispatch_semaphore_signal_slow(dsema);
}
  • dsema_value进行++操作,如果> 0,函数返回0
  • 如果value == LONG_MIN,程序崩溃。
  • 上面的条件都不成立,返回_dispatch_semaphore_signal_slow函数。

进入_dispatch_semaphore_signal_slow函数,可以看到是一些异常的处理:

image.png

总结

  • 信号量操作主要就是dsema_value的相关处理。
  • dispatch_semaphore_create初始化dsema_value值。
  • dispatch_semaphore_wait进行dsema_value--操作,如果>= 0任务执行,否则进行do while循环阻塞任务执行,等待dsema_value>= 0任务可以继续执行。
  • dispatch_semaphore_signal进行dsema_value++操作。

五、调度组的应用

作用

  • 控制任务执行顺序。

函数

  • dispatch_group_create:创建组。
  • dispatch_group_async:进组任务。
  • dispatch_group_notify:进组任务执行完毕通知。
  • dispatch_group_wait:进组任务执行等待时间。
  • dispatch_group_enter:进组。 
  • dispatch_group_leave:出组。

示例

- (void)myGroupDemo
{
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    //创建调度组
    dispatch_group_async(group, queue, ^{
        sleep(1);
        [self.mArray addObject:@"图片1"];
    });
    
    // 进组和出租 成对 先进后出
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        sleep(1);
        [self.mArray addObject:@"图片2"];
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSMutableString *muStr = [NSMutableString new];
        for (NSString *str in self.mArray) {
            [muStr appendString:str];
        }
        [muStr appendString:@"-生成水印"];
        
        NSLog(@"%@",muStr);
    });
}

执行结果:
图片1图片2-生成水印
  • 当两个异步任务都完成以后,才会调用到group任务,下面来分析一下底层原理。

六、调度组的原理

调度组早先版本是用信号量来实现的,现在自己写了一套,也是仿照信号量的原理,都是关于value的一些操作。

dispatch_group_create

dispatch_group_t
dispatch_group_create(void)
{
    return _dispatch_group_create_with_count(0);
}

进入_dispatch_group_create_with_count

static inline dispatch_group_t
_dispatch_group_create_with_count(uint32_t n)
{
    dispatch_group_t dg = _dispatch_object_alloc(DISPATCH_VTABLE(group),
                    sizeof(struct dispatch_group_s));
    dg->do_next = DISPATCH_OBJECT_LISTLESS;
    dg->do_targetq = _dispatch_get_default_queue(false);
    if (n) {
        os_atomic_store2o(dg, dg_bits,
                        (uint32_t)-n * DISPATCH_GROUP_VALUE_INTERVAL, relaxed);
        os_atomic_store2o(dg, do_ref_cnt, 1, relaxed); // <rdar://22318411>
    }
    return dg;
}
  • 这里是进行了一些初始化的操作。

dispatch_group_enter

void dispatch_group_enter(dispatch_group_t dg)
{
    // The value is decremented on a 32bits wide atomic so that the carry
    // for the 0 -> -1 transition is not propagated to the upper 32bits.
    uint32_t old_bits = os_atomic_sub_orig2o(dg, dg_bits,
                DISPATCH_GROUP_VALUE_INTERVAL, acquire);
    uint32_t old_value = old_bits & DISPATCH_GROUP_VALUE_MASK;
    if (unlikely(old_value == 0)) {
        _dispatch_retain(dg); // <rdar://problem/22318411>
    }
    if (unlikely(old_value == DISPATCH_GROUP_VALUE_MAX)) {
        DISPATCH_CLIENT_CRASH(old_bits,
                        "Too many nested calls to dispatch_group_enter()");
    }
}
  • value进行--操作。
  • 如果old_value等于0,调用_dispatch_retain函数
  • 如果old_value等于DISPATCH_GROUP_VALUE_MAX,程序崩溃,DISPATCH_GROUP_VALUE_MAX的值为4
    #define DISPATCH_GROUP_VALUE_INTERVAL   0x0000000000000004ULL
    #define DISPATCH_GROUP_VALUE_MAX        DISPATCH_GROUP_VALUE_INTERVAL
    

dispatch_group_leave

image.png

  • 这里会执行value++操作,如果old_value等于-1,调用_dispatch_group_wake执行唤醒操作,唤醒操作会调用dispatch_group_notify中的block任务。

dispatch_group_notify

image.png

  • old_state == 0也就是value == 0时,执行_dispatch_group_wake,调用dispatch_group_notify中的block任务。

dispatch_group_async

我们猜测dispatch_group_async函数中应该是有这dispatch_group_enterdispatch_group_leave的调用,接下来进行探索。

进入dispatch_group_async

image.png

进入_dispatch_continuation_group_async

image.png

  • 在这里找到了dispatch_group_enter函数的调用,继续看_dispatch_continuation_async

我们在 iOS 多线程(二):GCD基础&源码分析上 中分析过,通过_dispatch_continuation_async最终会调用到_dispatch_continuation_invoke_inline函数:

image.png

  • 我们之前分析的正常情况会调用_dispatch_client_callout函数,但如果标记是DC_FLAG_GROUP_ASYNC的时候,调用的是_dispatch_continuation_with_group_invoke函数。

进入_dispatch_continuation_with_group_invoke

image.png

  • 如上图,当type == DISPATCH_GROUP_TYPE时,先通过_dispatch_client_callout完成block任务的调用,然后就会调用dispatch_group_leave函数,完美!!。

总结

  • dispatch_group_create:初始化value = 0;
  • dispatch_group_entervalue--;
  • dispatch_group_leavevalue++,如果old_value == -1,调用dispatch_group_notify中的block任务。
  • dispatch_group_notify:如果value == 0,调用dispatch_group_notify中的block任务。
  • dispatch_group_async:内部会调用成对的dispatch_group_enterdispatch_group_leave

七、dispatch_source

作用

  • CPU负荷非常小,尽量不占用资源。
  • 联结的优势。

概念

在任一线程上调用它的一个函数dispatch_source_merge_data后,会执行Dispatch Source实现定义好的句柄(可以把句柄简单理解为一个block)这个过程叫Custom event,用户事件,是dispatch source支持处理的一种事件。

句柄是一种指向指针的指针,它指向的就是一个类或者结构,它和系统有很密切的关系

HINSTANCE(实例句柄),HBITMAP(位图句柄),HDC(设备表述句柄),HICON(图标句柄)等。这当中还有一个通用的句柄,就是HANDLE

函数

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

基本使用

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIProgressView *progressView;
@property (nonatomic, strong) dispatch_source_t source;
@property (nonatomic, strong) dispatch_queue_t queue;

@property (nonatomic, assign) NSUInteger totalComplete;
@property (nonatomic) BOOL isRunning;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.totalComplete = 0;
    
    self.queue = dispatch_queue_create("com.ssl.dd", 0);
    
     第一个参数:dispatch_source_type_t type为设置GCD源方法的类型,前面已经列举过了。
     第二个参数:uintptr_t handle Apple的API介绍说,暂时没有使用,传0即可。
     第三个参数:unsigned long mask Apple的API介绍说,使用DISPATCH_TIMER_STRICT,会引起电量消耗加剧,毕竟要求精确时间,所以一般传0即可,视业务情况而定。
     第四个参数:dispatch_queue_t _Nullable queue 队列,将定时器事件处理的Block提交到哪个队列之上。可以传Null,默认为全局队列。注意:当提交到全局队列的时候,时间处理的回调内,需要异步获取UI线程,更新UI...不过这好像是常识,又啰嗦了...
     */
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    
    // 保存代码块 ---> 异步 dispatch_source_set_event_handler()
    // 设置取消回调 dispatch_source_set_cancel_handler(dispatch_source_t source,dispatch_block_t _Nullable handler)
    // 封装我们需要回调的触发函数 -- 响应
    dispatch_source_set_event_handler(self.source, ^{
        
        NSUInteger value = dispatch_source_get_data(self.source); // 取回来值 1 响应式
        self.totalComplete += value;
        NSLog(@"进度:%.2f", self.totalComplete/100.0);
        self.progressView.progress = self.totalComplete/100.0;
    });
    
    self.isRunning     = YES;
    dispatch_resume(self.source);
}
    
- (IBAction)didClickStartOrPauseAction:(id)sender {

    if (self.isRunning) {// 正在跑就暂停
        dispatch_suspend(self.source);
        dispatch_suspend(self.queue);// mainqueue 挂起
        self.isRunning = NO;
        [sender setTitle:@"暂停中..." forState:UIControlStateNormal];
    }else{
        dispatch_resume(self.source);
        dispatch_resume(self.queue);
        self.isRunning = YES;
        [sender setTitle:@"加载中..." forState:UIControlStateNormal];
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    NSLog(@"点击开始加载");
    for (NSUInteger index = 0; index < 100; index++) {
        dispatch_async(self.queue, ^{
            if (!self.isRunning) {
                NSLog(@"暂停下载");
                return ;
            }
            sleep(2);

            dispatch_source_merge_data(self.source, 1); // source 值响应
        });
    }
}

打印结果:

image.png