ios GCD深入应用

160 阅读4分钟

前言

在多线程和GCD文章中,我们总结了多线程的意义以及实际开发中的案例,其中死锁、线程安全等等都是多线程绕不过的问题,需要我们小心处理。今天我们继续总结下实际开发中还用到了哪些与GCD相关的应用以及需要注意的地方。

1、dispatch_group调度组

几个常用api如下:

  • dispatch_group_create:创建调度组
  • dispatch_group_async:异步提交任务进调度组,需要注意的是使用这种方式进组可以省略enter和leave,下面会举例说明。
  • dispatch_group_enter:手动管理调度组的进出
  • dispatch_group_leave
  • dispatch_group_notify
  • dispatch_group_wait dispatch_group_wait :在调度组完成时调用,或者调度组超时调用。(完成指的是enter和leave次数一样多) ispatch_group_notify:不管超不超时,只要任务组完成,会调用,不完成不会调用。

调度组使用案例

假如有5个任务1、2、3、4、5,任务3必须要2之后执行,任务5必须在前面4个任务完成之后再执行。

分析:任务3必须在任务2之后执行,那么任务2和3应该是串联任务,把2和3作为一个串联任务整体,那么这个任务整体可以和任务1、4并行执行,最后的任务5必须等前面几个任务完成之后才能执行,一个典型的调度组应用。

//串行队列 放2、3
    dispatch_queue_t serialqueue=dispatch_queue_create("aa", DISPATCH_QUEUE_SERIAL);
    //并发队列 放 1、4
    dispatch_queue_t concurrentqueue=dispatch_queue_create("bb", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t mygroup=dispatch_group_create();
    dispatch_group_async(mygroup, concurrentqueue, ^{
        NSLog(@"任务1执行");
    });

    dispatch_group_async(mygroup, concurrentqueue, ^{
        NSLog(@"任务4执行");
    });

    dispatch_group_async(mygroup, serialqueue, ^{
        sleep(1);
        NSLog(@"任务2执行");
    });
    
    //enter和leave组合等价于dispatch_group_async
    dispatch_group_enter(mygroup);
    dispatch_async(serialqueue, ^{
        NSLog(@"任务3执行");
        dispatch_group_leave(mygroup);
    });

    dispatch_group_wait(mygroup, DISPATCH_TIME_FOREVER);
    dispatch_group_notify(mygroup,dispatch_get_main_queue(), ^{
        NSLog(@"任务5执行");
    });

输出:

任务1执行
任务4执行
任务2执行
任务3执行
任务5执行

分析:

  • 任务1、任务4和任务2、3整体上是没有先后顺序的,但是3肯定是在2之后执行。
  • 注意enter和leave组合等价于dispatch_group_async,上面执行任务3的时候就是用的enter和leave手动管理的调度组。

2、信号量

常用api如下:

  • dispatch_semaphore_create:创建一个信号量,初始值必须大于等于0,否则会失败
  • dispatch_semaphore_signal:发送信号,会使信号量值value+1
  • dispatch_semaphore_wait:等待信号量,会使信号量值value-1。当value值为0时,调用该函数就会阻塞所在线程,会持续等待,要么超时要么等待过程中value值大于0继续执行,看到阻塞就要千万注意了别阻塞主线程哦~

通过实例理解一下:

   dispatch_semaphore_t sempt=dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //value=0时信号量-1,会阻塞线程,直到value>0才继续执行
        dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
        sleep(2);
        NSLog(@"任务1执行");
     });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        NSLog(@"任务2执行");
        //信号量+1,此时value=0,dispatch_semaphore_wait接收到信号不会阻塞线程
        dispatch_semaphore_signal(sempt);
     });

输出:

任务2执行
任务1执行

分析:并发队列中,执行dispatch_semaphore_wait时信号量值会减1,由于初始信号量的值为0,此时信号量值等于-1,会阻塞线程执行。执行完任务2,dispatch_semaphore_signal会使信号量值加1,此时信号量值等于0,dispatch_semaphore_wait会收到信号量值变化的广播,大于等于0就继续向下执行任务1。

如果再添加一个任务3呢?

dispatch_semaphore_t sempt=dispatch_semaphore_create(0);
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //value=0时信号量-1,此时信号量值为-1
        dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
        sleep(4);
        NSLog(@"任务1执行");

     });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        NSLog(@"任务2执行");
        //信号量+1,此时value=0,dispatch_semaphore_wait接收到信号不会阻塞线程
        dispatch_semaphore_signal(sempt);
     });

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //此时信号量值为-1
        dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
        NSLog(@"任务3执行");
        dispatch_semaphore_signal(sempt);
     })

输出:

任务2执行
任务1执行

分析:为什么任务3不会执行?初始信号量值为0,所以任务1和任务3都是阻塞状态,当任务2执行完成后信号量+1,任务1的dispatch_semaphore_wait会收到通知继续向下执行,由于任务1没有调用dispatch_semaphore_signal使信号量+1,所以任务3还是阻塞状态不会执行。任务1的dispatch_semaphore_wait等待信号量在任务3的前面,如果在任务1的dispatch_semaphore_wait前面加一句sleep(1),那么会先执行2,再执行3,最后执行1,感兴趣的可以试试。

信号量的使用案例-线程安全

比如有100个线程同时往同一个数组中添加数据,如何保证内存安全呢?

NSMutableArray* arrar=[[NSMutableArray alloc]init];
    dispatch_semaphore_t sempt=dispatch_semaphore_create(1);
    for(int i=0;i<100;i++){
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            dispatch_semaphore_wait(sempt, DISPATCH_TIME_FOREVER);
            NSLog(@"开始添加");
            [arrar addObject:@(1)];
            NSLog(@"结束添加");
            dispatch_semaphore_signal(sempt);
        });
    }

输出:

开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
开始添加
结束添加
.....

分析:通过信号量可以用来控制线程并发访问的最大数量,当我们设置信号量的初时值为1时,就可以实现线程同步

3、信号源

Dispatch Source信号源,区别于dispatch_semaphore,信号源是用来监听事件的对象,这点和NSRunloop很类似,但是Dispatch Source是针对一个队列的,至于队列使用哪个线程执行这个task,则取决于队列的线程池状态,而NSRunloop则是完全与指定线程绑定的。 常用api如下:

  • dispatch_source_create:创建信号源,有4个参数 第一个参数type:信号源的类型,分为有11个类型,常用的是DISPATCH_SOURCE_TYPE_DATA_ADD和DISPATCH_SOURCE_TYPE_TIMER
  1. DISPATCH_SOURCE_TYPE_DATA_ADD:属于自定义事件,可以通过dispatch_source_get_data函数获取事件变量数据,在我们自定义的方法中可以调用dispatch_source_merge_data函数向Dispatch Source设置数据。
  2. DISPATCH_SOURCE_TYPE_DATA_OR:属于自定义事件,用法同上面的类型一样。
  3. DISPATCH_SOURCE_TYPE_MACH_SEND:Mach端口发送事件。
  4. DISPATCH_SOURCE_TYPE_MACH_RECV:Mach端口接收事件。
  5. DISPATCH_SOURCE_TYPE_PROC:与进程相关的事件。
  6. DISPATCH_SOURCE_TYPE_READ:读文件事件。
  7. DISPATCH_SOURCE_TYPE_WRITE:写文件事件。
  8. DISPATCH_SOURCE_TYPE_VNODE:文件属性更改事件。
  9. DISPATCH_SOURCE_TYPE_SIGNAL:接收信号事件。
  10. DISPATCH_SOURCE_TYPE_TIMER:定时器事件。
  11. DISPATCH_SOURCE_TYPE_MEMORYPRESSURE:内存压力事件。
  • dispatch_suspend(queue):挂起信号源(队列也可以调用该函数挂起)
  • dispatch_resume(source) :开启信号源(队列也可以调用该函数开启),信号源刚创建时是挂起状态,需要调用该函数开启。
  • dispatch_source_merge_data:向信号源发送传值事件,不可以传递0值或负值,否则事件不会被触发
  • dispatch_source_set_event_handler:设置响应发送的事件。
  • dispatch_source_get_data:得到信号源发送的值。
  • dispatch_source_cancel:取消信号源,即不再监听,区别于挂起状态。
  • dispatch_source_get_handle:得到dispatch_source_create的第二个参数

先用一个简单的网络请求数据完成后派送信号源更新UI的例子

dispatch_source_t source=dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
dispatch_source_set_event_handler(source, ^{
    //获取信号源中派发的值
    dispatch_source_get_data(source);
    //更新ui操作....
});
//开启监听
dispatch_resume(source);
//网络请求等等完成后通知信号源,并发送数据
 dispatch_source_merge_data(source, 1);

定时器信号源案例

    __block int timeout = 60;
    //创建队列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //创建timer
    dispatch_source_t _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //设置1s触发一次,0s的误差精度
    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源
            dispatch_source_cancel(_timer);
        }
        else{  
            timeout--;
            dispatch_async(dispatch_get_main_queue(), ^{
                //更新主界面的操作
                NSLog(@"%d秒后可重新获取验证码",timeout);
                
            });
        }
    });
    
    //开启信号源监听
    dispatch_resume(_timer);

分析:dispatch_source_set_timer,该函数的作用就是给监听事件类型为DISPATCH_SOURCE_TYPE_TIMER的Dispatch Source设置相关属性,该函数有四个参数:

  1. source:该参数为目标Dispatch Source,类型为dispatch_source_t.
  2. start:该参数为定时器的起始时间,类型为dispatch_time_t
  3. interval:该参数为定时器的间隔时间,类型为UInt64,间隔时间的单位是纳秒。
  4. leeway:该参数为间隔时间的精度,类型为UInt64,时间单位也是纳秒。

进度条信号源案例

@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("ahua.com", NULL);
    //自定义信号源
    self.source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_main_queue());
    //监听信号源派送的事件
    dispatch_source_set_event_handler(self.source, ^{
        //信号源每次发送的值都是1,进度条++1
        NSUInteger value = dispatch_source_get_data(self.source);
        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);
        NSLog(@"已经暂停");
        self.isRunning = NO;
        [sender setTitle:@"暂停中.." forState:UIControlStateNormal];
    }else{
        dispatch_resume(self.source);
        dispatch_resume(self.queue);
        NSLog(@"已经执行了");
        self.isRunning = YES;
        [sender setTitle:@"执行中.." forState:UIControlStateNormal];
    }
}
//点击屏幕向信号源派送事件,模拟触发进度条更新
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    for (int i= 0; i<100; i++) {
        //串行队列每一秒派送一次事件
        dispatch_async(self.queue, ^{
            if (!self.isRunning) {
                NSLog(@"已经暂停");
                return;
            }
            sleep(1);
            dispatch_source_merge_data(self.source, 1);
        });
    }
}