10 - OC多线程之GCD常用API

OC底层原理探索文档汇总

dispatch_after延迟执行

使用很简单,只是需要知道一点,等待指定的时间后将任务块异步的添加到指定的队列中,并不是延迟执行,而是延迟入队

代码:

- (void)cjl_testAfter{
    /*
     dispatch_after表示在某队列中的block延迟执行
     应用场景:在主队列上延迟执行一项任务,如viewDidload之后延迟1s,提示一个alertview(是延迟加入到队列,而不是延迟执行)
     */
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"2s后输出");
    });
   
}

dispatch_once单例

dispatch_once的任务只执行一次

代码:

- (void)cjl_testOnce{
    /*
     dispatch_once保证在App运行期间,block中的代码只执行一次
     应用场景:单例、method-Swizzling
     */
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //创建单例、method swizzled或其他任务
        NSLog(@"创建单例");
    });
}

注意:

  • onceToken是静态变量,具有唯一性
  • dispatch_once的底层会进行加锁来保证block执行的唯一性
  • 如果任务没有执行过,会将任务进行加锁,如果在当前任务执行期间,有其他任务进来,会进入无限次等待,原因是当前任务已经获取了锁,进行了加锁,其他任务是无法获取锁的。

dispatch_apply重复执行

可以让任务重复添加到队列中重复执行

代码:

- (void)cjl_testApply{
    /*
     dispatch_apply将指定的Block追加到指定的队列中重复执行,并等到全部的处理执行结束——相当于线程安全的for循环

     应用场景:用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性
     - 添加到串行队列中——按序执行
     - 添加到主队列中——死锁
     - 添加到并发队列中——乱序执行
     - 添加到全局队列中——乱序执行
     */
    
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_SERIAL);
    NSLog(@"dispatch_apply前");
    /**
         param1:重复次数
         param2:追加的队列
         param3:执行任务
         */
    dispatch_apply(10, queue, ^(size_t index) {
        NSLog(@"dispatch_apply 的线程 %zu - %@", index, [NSThread currentThread]);
    });
    NSLog(@"dispatch_apply后");
}

注意:

  • dispatch_apply是同步函数,所以不能将其添加到主队列中,会造成死锁
  • 相当于是线程安全的for循环

dispatch_group_t调度组

通过调度组将任务分组实现,调度组的最直接作用是控制任务执行顺序

API:

dispatch_group_create 创建组 
dispatch_group_async 进组任务 
dispatch_group_notify 进组任务执行完毕通知 
dispatch_group_wait 进组任务执行等待时间

//进组和出组一般是成对使用的
dispatch_group_enter 进组 
dispatch_group_leave 出组

【方式一】使用dispatch_group_async + dispatch_group_notify

设定dispatch_group_async的任务执行完才可以执行dispatch_group_notify中的任务

代码:

- (void)cjl_testGroup1{
    /*
     dispatch_group_t:调度组将任务分组执行,能监听任务组完成,并设置等待时间

     应用场景:多个接口请求之后刷新页面
     */
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"请求一完成");
    });
    
    dispatch_group_async(group, queue, ^{
        NSLog(@"请求二完成");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新页面");
    });
}

【方式二】使用dispatch_group_enter + dispatch_group_leave + dispatch_group_notify

代码:

- (void)cjl_testGroup2{
    /*
     dispatch_group_enter和dispatch_group_leave成对出现,使进出组的逻辑更加清晰
     */
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求一完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求二完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"刷新界面");
    });
}

执行结果:

2021-11-03 20:47:03.680798+0800 多线程使用[73188:1335127] 请求一完成
2021-11-03 20:47:03.680800+0800 多线程使用[73188:1335125] 请求二完成
2021-11-03 20:47:03.694075+0800 多线程使用[73188:1334877] 刷新界面
  • 在我们想要提前执行的任务执行后加上dispatch_group_leave,就可以让dispatch_group_notify中的任务执行
  • 只要有一对enter-leave执行完成就可以,不需要多对都执行完成
  • 如果enter比leave多,会一直等待
  • 如果enter比leave少,会崩溃

在方式二的基础上增加超时dispatch_group_wait
代码:

- (void)cjl_testGroup3{
    /*
     long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)

     group:需要等待的调度组
     timeout:等待的超时时间(即等多久)
        - 设置为DISPATCH_TIME_NOW意味着不等待直接判定调度组是否执行完毕
        - 设置为DISPATCH_TIME_FOREVER则会阻塞当前调度组,直到调度组执行完毕


     返回值:为long类型
        - 返回值为0——在指定时间内调度组完成了任务
        - 返回值不为0——在指定时间内调度组没有按时完成任务

     */
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求一完成");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_async(queue, ^{
        NSLog(@"请求二完成");
        dispatch_group_leave(group);
    });
    
//    long timeout = dispatch_group_wait(group, DISPATCH_TIME_NOW);
//    long timeout = dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    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(@"刷新界面");
    });
}

总结:

  • enter-leave只要成对就可以,不管远近,一旦有一对执行完成,就可以执行dispatch_group_notify中的任务
  • dispatch_group_async 等同于enter - leave,其底层的实现就是enter-leave
  • enter-leave必须成对出现

栅栏函数

栅栏函数有同步栅栏函数和异步栅栏函数,分别通过dispatch_barrier_sync & dispatch_barrier_async实现

栅栏函数的作用就是在自定义的并发队列中让某一部分任务按顺序执行,具有同步的效果。

同步栅栏函数dispatch_barrier_sync

//同步栅栏函数dispatch_barrier_sync
- (void)cjl_testBarrier1{
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"开始 - %@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s的任务1 - %@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束 - %@", [NSThread currentThread]);
    
    //栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1、任务2
    dispatch_barrier_sync(queue, ^{
        sleep(1);
        NSLog(@"------------延迟1s的栅栏任务------------%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"不延迟的任务2 - %@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束 - %@", [NSThread currentThread]);
}

运行结果:

2021-11-03 21:40:32.581611+0800 多线程使用[75238:1382574] 开始 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:32.581733+0800 多线程使用[75238:1382574] 第一次结束 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:34.582062+0800 多线程使用[75238:1382843] 延迟2s的任务1 - {number = 6, name = (null)}
2021-11-03 21:40:35.582597+0800 多线程使用[75238:1382574] ------------延迟1s的栅栏任务------------<_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:35.582839+0800 多线程使用[75238:1382574] 栅栏结束 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:35.583055+0800 多线程使用[75238:1382574] 第二次结束 - <_NSMainThread: 0x600001a7c140>{number = 1, name = main}
2021-11-03 21:40:35.583136+0800 多线程使用[75238:1382843] 不延迟的任务2 - {number = 6, name = (null)}

说明:

  • 同步栅栏函数提交任务以供当前线程执行,运行结果中可以看到是主线程执行的同步栅栏函数,这个很好理解,前面我们分析过了,同步函数不会开辟新线程。
  • 需要等待该任务的完成才能返回,所以会阻塞当前线程的执行,运行结果中可以看到执行完栅栏函数才会执行”栅栏结束“代码
  • 当栅栏任务到达自定义并发队列的顶端时,并不会立即执行,必须等待当前队列中正在执行的block完成执行才可以执行栅栏任务,所以运行结果中可以看出即时任务1需要等待2s,也必须等任务1执行完成才可以执行栅栏函数
  • 在栅栏任务完成之前,不会执行栅栏函数之后提交的任何任务,所以运行结果中可以看到栅栏函数需要等待1s,也必须等待栅栏函数执行完才可以执行任务2.
  • 也就是说栅栏函数作为栅栏,可以将它之前的任务、栅栏任务、之后的任务按顺序执行,使得在并发队列中没有串行的效果

异步栅栏函数dispatch_barrier_async:
代码:

//异步栅栏函数dispatch_barrier_async
- (void)cjl_testBarrier2{
    //并发队列使用栅栏函数
    
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_CONCURRENT);
    
    NSLog(@"开始 - %@", [NSThread currentThread]);
    dispatch_async(queue, ^{
        sleep(2);
        NSLog(@"延迟2s的任务1 - %@", [NSThread currentThread]);
    });
    NSLog(@"第一次结束 - %@", [NSThread currentThread]);
    
    //由于并发队列异步执行任务是乱序执行完毕的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序
    dispatch_barrier_async(queue, ^{
        sleep(1);
        NSLog(@"------------延迟1s的栅栏任务------------%@", [NSThread currentThread]);
    });
    NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
    
    dispatch_async(queue, ^{
        NSLog(@"不延迟的任务2 - %@", [NSThread currentThread]);
    });
    NSLog(@"第二次结束 - %@", [NSThread currentThread]);
}

运行结果:

2021-11-03 21:44:03.482963+0800 多线程使用[75383:1386090] 开始 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:03.483082+0800 多线程使用[75383:1386090] 第一次结束 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:03.483157+0800 多线程使用[75383:1386090] 栅栏结束 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:03.483248+0800 多线程使用[75383:1386090] 第二次结束 - <_NSMainThread: 0x600001874140>{number = 1, name = main}
2021-11-03 21:44:05.485233+0800 多线程使用[75383:1386439] 延迟2s的任务1 - {number = 7, name = (null)}
2021-11-03 21:44:06.487452+0800 多线程使用[75383:1386439] ------------延迟1s的栅栏任务------------{number = 7, name = (null)}
2021-11-03 21:44:06.487667+0800 多线程使用[75383:1386439] 不延迟的任务2 - {number = 7, name = (null)}

说明:

  • 异步栅栏函数提交异步执行的代码块并立即返回,不需要等待block执行完成就可以返回,所以不会阻塞当前线程,在运行结果中可以看到主线程的任务全部执行完才会执行异步函数,并不会阻塞主线程。
  • 异步函数会开辟新线程,所以运行结果中执行栅栏任务的线程不是主线程
  • 这里也可以看到并发队列的执行顺序是任务1、栅栏任务、任务2,其原理与同步栅栏函数一样,不再赘述

总结:

  • 栅栏函数的作用是在自定义的并发队列中让某些任务按顺序执行,具有串行的效果
  • 栅栏函数只能作用于自定义的并发队列,如果作用于串行队列以及全局并发队列,它就相当于一个普通的同步函数或异步函数,无法起到栅栏的作用
  • 同步栅栏函数和异步栅栏函数的作用都是一样的,区别在于同步栅栏函数是当前线程执行的,而且必须等待栅栏任务完成才可以返回,有阻塞当前线程的效果。异步函数是新开辟线程执行的,而且无需等待任务块的完成就可以返回,没有阻塞当前线程的效果
  • 当我们想要在并发队列中控制某些任务的执行顺序时就可以使用栅栏函数

信号量

信号量主要用作同步锁,实现线程安全,还可以用于控制GCD最大并发数。

代码:

- (void)cjl_testSemaphore{
    /*
     应用场景:同步当锁, 控制GCD最大并发数

     - dispatch_semaphore_create():创建信号量
     - dispatch_semaphore_wait():等待(减少)信号量,信号量减1。当信号量< 0时会阻塞当前线程,根据传入的等待时间决定接下来的操作——如果永久等待将等到有信号(信号量>=0)才可以执行下去
     - dispatch_semaphore_signal():递增信号量,信号量加1。当信号量>= 0 会执行dispatch_semaphore_wait中等待的任务

     */
    dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"当前 - %d, 线程 - %@", i, [NSThread currentThread]);
        });
    }
    
    //利用信号量来改写
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    
    for (int i = 0; i < 10; i++) {
        dispatch_async(queue, ^{
            NSLog(@"当前 - %d, 线程 - %@", i, [NSThread currentThread]);
            
            dispatch_semaphore_signal(sem);
        });
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    }
}

说明:

  • dispatch_semaphore_wait()会将信号量-1,当减小到<0时,该函数将无法返回,执行该任务的线程也将被阻塞,wait之后的代码将无法执行
  • dispatch_semaphore_signal会将信号量+1,当加到>=0时,可以执行wait中的任务。
  • 我们看到执行一次signal,就可以执行一次wait之后的代码

同步锁的使用:

  • wait之后的代码作为线程安全的代码
  • wait作为锁,当执行了wait后信号<0,此时就被上锁了,想要执行,需要通过signal解锁。

最大并发数的使用:

  • 最大并发数也就是线程最多可以同时执行多少任务
  • 将wait之后的代码作为任务
  • 我们调用n次signal,最大并发数就是n
  • 这是因为当执行了n次wait之后的任务后,信号<0,就无法再次执行了

dispatch_source_t计时操作

dispatch_source_t主要用于计时操作,其原因是因为它创建的timer不依赖于RunLoop,且计时精准度比NSTimer高

代码:

- (void)cjl_testSource{
    /*
     dispatch_source
     
     应用场景:GCDTimer
     在iOS开发中一般使用NSTimer来处理定时逻辑,但NSTimer是依赖Runloop的,而Runloop可以运行在不同的模式下。如果NSTimer添加在一种模式下,当Runloop运行在其他模式下的时候,定时器就挂机了;又如果Runloop在阻塞状态,NSTimer触发时间就会推迟到下一个Runloop周期。因此NSTimer在计时上会有误差,并不是特别精确,而GCD定时器不依赖Runloop,计时精度要高很多
     
     dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件
        - Timer Dispatch Source:定时器事件源,用来生成周期性的通知或回调
        - Signal Dispatch Source:监听信号事件源,当有UNIX信号发生时会通知
        - Descriptor Dispatch Source:监听文件或socket事件源,当文件或socket数据发生变化时会通知
        - Process Dispatch Source:监听进程事件源,与进程相关的事件通知
        - Mach port Dispatch Source:监听Mach端口事件源
        - Custom Dispatch Source:监听自定义事件源

     主要使用的API:
        - dispatch_source_create: 创建事件源
        - dispatch_source_set_event_handler: 设置数据源回调
        - dispatch_source_merge_data: 设置事件源数据
        - dispatch_source_get_data: 获取事件源数据
        - dispatch_resume: 继续
        - dispatch_suspend: 挂起
        - dispatch_cancle: 取消
     */
    
    //1.创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    //2.创建timer
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //3.设置timer首次执行时间,间隔,精确度
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0*NSEC_PER_SEC, 0.1*NSEC_PER_SEC);
    //4.设置timer事件回调
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"GCDTimer");
    });
    //5.默认是挂起状态,需要手动激活
    dispatch_resume(timer);
    
}

你可能感兴趣的:(10 - OC多线程之GCD常用API)