美文网首页
GCD探究(一) -- 任务与队列

GCD探究(一) -- 任务与队列

作者: iOSer_jia | 来源:发表于2020-11-27 18:00 被阅读0次

    GCD全程Grand Central Dispath,是苹果提供的一套多核并行运算的解决方案,GCD使用纯C语言的API,提供了非常强大的API,它会自动利用更多的CPU内核(比如双核、四核),自动管理线程的生命周期(创建线程、调度线程、销毁线程),我们只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码。

    函数、队列与任务

    GCD常见的用法

    dispatch_sync(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"GCD");
    });
    

    可以看到GCD的使用分为3个部分,函数dispatch_sync、队列dispatch_get_global_queue(0, 0),以及任务block。

    函数

    在GCD中可以将执行函数分为同步和异步两种

    • 同步函数:dispatch_sync,必须等待当前任务block执行完毕后才能执行下一语句,同步函数不会开启线程,会在当前线程中执行任务
    • 异步函数:dispatch_async,不用等待当前任务block执行完毕就可以执行下一语句,可以开启线程执行线程

    队列

    GCD队列是任务的等待队列,遵循先进先出(FIFO)原则,没调度一个任务就从队列中释放一个任务,队列有两种:串行队列和并发队列,两者的主要区别为执行顺序不同,开启线程数不同

    串行队列:每次只能有一个任务执行,任务一个接着一个执行,一个任务执行完毕后,在执行下一个任务

    dispatch_queue_t queue = dispatch_queue_create("net.gcd.queue", DISPATCH_QUEUE_SERIAL);
    

    并发队列:可以让多个任务并发执行,可以开启多想线程

    dispatch_queue_t queue = dispatch_queue_create("net.gcd.queue", DISPATCH_QUEUE_CONCURRENT);
    

    并发队列的并发功能只有在异步函数下才有效
    #define DISPATCH_QUEUE_SERIAL NULL,所以dispatch_queue_create("", NULL)得到的是同步队列

    主队列

    GCD提供了一种特殊的串行队列--主队列,所有放在队列中的任务都会放到主队列中执行

    全局并发队列

    GCD默认提供了全局并发队列,他需要提供两个参数,第一个是队列优先级,通常用DISPATCH_QUEUE_PRIORITY_DEFAULT,在iOS9之后,已经被服务质量代替,如QOS_CLASS_DEFAULT,第二个参数是苹果的保留字段,一般写0.

    dispatch_queue_t queue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
    

    在使用多线程开发时,如果对队列没有特殊需求,可以直接使用全局并发队列

    函数与队列

    根据函数与队列的两两组合,会出现出现以下四种结果

    同步函数并发队列

    • 不会开启线程,在当前线程执行任务
    • 任务串行执行,任务一个接着一个
    • 可能会产生堵塞

    同步函数并发队列

    • 不会开启线程,在当前线程执行任务
    • 任务一个接着一个

    异步函数串行队列

    • 开启一条新线程
    • 任务一个接着一个

    异步函数并发队列

    • 开启多个线程
    • 异步执行任务,没有顺序,与CPU调度有关

    GCD的其他方法

    除了开启线程的能力,GCD还提供了许多api,我们可以利用这些api实现单例、多读单写、计时器等功能

    栅栏函数 dispatch_barrier_async

    如果我们需要异步执行两组操作,而且第一组操作执行完成之后才能执行第二组操作,我们就需要一个栅栏一样的东西将这两组操作分割开来,而GCD的dispatch_barrier_async便可以实现着这一功能。

    dispatch_barrier_async会等待前边追加到并发队列的任务全部执行完毕后,再将指定的任务追加到该异步队列中,然后在dispatch_barrier_async追加的任务执行完毕之后,异步队列才恢复为正常执行。

    例如:

    dispatch_queue_t queue = dispatch_queue_create("barrier_test", DISPATCH_QUEUE_CONCURRENT);
        
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"1");
        }
    });
        
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"2");
        }
    });
        
    dispatch_barrier_async(queue, ^{
       for (int i = 0; i < 2; i++) {
           NSLog(@"barrier");
       }
    });
        
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"3");
        }
    });
        
    dispatch_async(queue, ^{
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2];
            NSLog(@"4");
        }
    });
    

    执行结果为

    2020-11-25 16:05:29.664252+0800 GCDTest[91450:4599536] 1
    2020-11-25 16:05:29.664294+0800 GCDTest[91450:4599535] 2
    2020-11-25 16:05:31.668346+0800 GCDTest[91450:4599535] 2
    2020-11-25 16:05:31.668442+0800 GCDTest[91450:4599536] 1
    2020-11-25 16:05:31.668615+0800 GCDTest[91450:4599536] barrier
    2020-11-25 16:05:31.668735+0800 GCDTest[91450:4599536] barrier
    2020-11-25 16:05:33.670656+0800 GCDTest[91450:4599536] 3
    2020-11-25 16:05:33.670617+0800 GCDTest[91450:4599535] 4
    2020-11-25 16:05:35.675116+0800 GCDTest[91450:4599535] 4
    2020-11-25 16:05:35.675144+0800 GCDTest[91450:4599536] 3
    

    利用这一特性,我们可以使用栅栏函数实现多读单写。

    - (void)setName:(NSString *)name {
        dispatch_barrier_async(self.queue, ^{
            self->_name = [name copy];
        });
    }
    
    - (NSString *)name {
        __block NSString *tempName;
        dispatch_sync(self.queue, ^{
            tempName = self->_name;
        });
        return tempName;
    }
    
    - (dispatch_queue_t)queue {
        if (!_queue) {
            _queue = dispatch_queue_create("barrier_test", DISPATCH_QUEUE_CONCURRENT);
        }
        return _queue;
    }
    

    队列组 dispatch_group

    有时候我们会有这样的需求,分别执行两个异步耗时任务,然后当两个耗时任务都执行完毕后再回到主线程执行任务,这时候可以用到GCD的队列组。

    GCD的队列组有几个关键的api

    • dispatch_group_create 创建队列组
    • dispatch_group_enter 加入队列组
    • dispatch_group_leave 离开队列组
    • dispatch_group_async 将任务加入队列组
    • dispatch_group_notify 回到指定队列执行任务
    • dispatch_group_wait 同上,但会阻塞当前线程
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"1");
    });
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"2");
    });
    dispatch_group_notify(group, queue, ^{
        NSLog(@"3");
    });
    

    执行结果:

    2020-11-26 17:33:44.591118+0800 GCDTest[6468:5517558] 2
    2020-11-26 17:33:44.591118+0800 GCDTest[6468:5517552] 1
    2020-11-26 17:33:44.591338+0800 GCDTest[6468:5517552] 3
    

    GCD一次性代码 dispatch_once

    我们在创建单例、或者有整个运行过程中只执行一次的代码时,我们可以使用GCD的dispatch_once实现,即使在多线程的环境,dispatch_once也可以保证线程安全。

    - (void)once {
        static dispatch_once_t onceToken;
         dispatch_once(&onceToken, ^{
            // 只执行一次的代码
        });
    }
    

    GCD计时器 dispatch_source

    dispatch source 是一种处理事件的数据类型,这些被处理的事件为操作系统中的底层级别,而计时器类型是其支持的其中一种类型。

    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.0 * NSEC_PER_SEC);
    dispatch_source_set_event_handler(_timer, ^{
        NSLog(@"source");
    });
    dispatch_resume(_timer);
    
    • dispatch_source_create :创建source,计时器类型使用DISPATCH_SOURCE_TYPE_TIMER,而且可以指定队列
    • dispatch_source_set_timer:设置定时器的相关参数,第一个参数为定时器,第二个为开始时间,第三个为回调时间间隔,第四个允许误差范围
    • dispatch_source_set_event_handler:设置事件处理句柄,一个句柄可以是一个block或者是一个函数,dispatch source会把句柄投放到队列中执行。
    • dispatch_resume:timer默认是挂起的,需要手动开启

    NSTimer相比,GCD的timer不需要依赖runloop,不会因为runloop的繁忙而导致及时不准。

    GCD快速迭代方法 dispatch_apply

    和for循环类似,dispatch_apply可以快速循环遍历。而相比于普通的for循环,dispatch_apply按照指定次数将任务追加到指定的队列中,并等待全部队列执行结束。

    如果在串行队列中使用dispatch_apply,那么就和for循环一样按照顺序同步执行,没有快速迭代的意义,我们可以利用并发队列队形进行异步执行,让dispatch_apply的任务多个线程中不是异步执行,并且无论在串行队列还是异步队列,dispatch_apply都会等到全部任务执行完成,有点类似dispatch_group_wait方法。

    可以用下面的例子清楚了解:

    NSLog(@"start");
    dispatch_apply(10, queue, ^(size_t i) {
        NSLog(@"%zd-----%@", i, [NSThread currentThread]);
    });
    NSLog(@"end");
    

    执行结果为:

    2020-11-27 15:35:23.341807+0800 GCDTest[44897:5974416] start
    2020-11-27 15:35:23.341930+0800 GCDTest[44897:5974416] 0-----<NSThread: 0x600001700040>{number = 1, name = main}
    2020-11-27 15:35:23.341998+0800 GCDTest[44897:5974416] 2-----<NSThread: 0x600001700040>{number = 1, name = main}
    2020-11-27 15:35:23.342015+0800 GCDTest[44897:5974466] 1-----<NSThread: 0x60000173a500>{number = 2, name = (null)}
    2020-11-27 15:35:23.342048+0800 GCDTest[44897:5974416] 3-----<NSThread: 0x600001700040>{number = 1, name = main}
    2020-11-27 15:35:23.342096+0800 GCDTest[44897:5974416] 5-----<NSThread: 0x600001700040>{number = 1, name = main}
    2020-11-27 15:35:23.342093+0800 GCDTest[44897:5974466] 4-----<NSThread: 0x60000173a500>{number = 2, name = (null)}
    2020-11-27 15:35:23.342180+0800 GCDTest[44897:5974466] 7-----<NSThread: 0x60000173a500>{number = 2, name = (null)}
    2020-11-27 15:35:23.342188+0800 GCDTest[44897:5974416] 6-----<NSThread: 0x600001700040>{number = 1, name = main}
    2020-11-27 15:35:23.342316+0800 GCDTest[44897:5974466] 8-----<NSThread: 0x60000173a500>{number = 2, name = (null)}
    2020-11-27 15:35:23.342346+0800 GCDTest[44897:5974464] 9-----<NSThread: 0x60000174e880>{number = 3, name = (null)}
    2020-11-27 15:35:23.342849+0800 GCDTest[44897:5974416] end
    

    GCD信号量 dispatch_semaphore

    GCD中的信号量指的是dispatch_semaphore,是持有计数的信号,类似高速路收费站的栏杆,可以通过时,打开栏杆,不可以通过时,关闭栏杆。在dispatch_semaphore中,使用计数来完成这个功能,计数为0时等待,不可通过,计数为1或大于1时,计数减1且不等待,可通过。

    dispatch_semaphore提供了三个函数。

    • dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
    • dispatch_semaphore_signal:发送一个信号,让信号总量加1
    • dispatch_semaphore_wait:可以使总信号量减1,当信号量为0时就会一直等待(阻塞所有线程),否则可以正常执行

    基于dispatch_semaphore的这一性质,它常常被用来作为锁来实现线程同步,如下

    NSLog(@"current thread: %@", [NSThread currentThread]);
        
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
        
    __block int num = 0;
        
    dispatch_async(queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"current thread: %@", [NSThread currentThread]);
        num = 100;
        dispatch_semaphore_signal(semaphore);
    });
        
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"num = %d", num);
    

    如果没有加入dispatch_semaphore,那么NSLog(@"num = %d", num);便不会等待num = 100;执行完成,而是直接打印当前数值0,而加入dispatch_semaphore_t后,开始semaphore为0,执行到
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);时,因为semaphore为0,所以下面的代码不会执行,而子线程中的任务执行num = 100;完成后,会调用一遍dispatch_semaphore_signal(semaphore);semaphore加一变为1,此时会通知到dispatch_semaphore,从而继续往下执行NSLog(@"num = %d", num);

    相关文章

      网友评论

          本文标题:GCD探究(一) -- 任务与队列

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