[iOS] GCD是神马-任务相关操作

2019-08-16  本文已影响0人  木小易Ying

关于GCD的基本概念可以参看上一篇吼:https://www.jianshu.com/p/0328c9a23cb4

我们使用GCD主要是用来执行任务、启动timer等,GCD提供了超多方法,分类如下,可以看到有io、timer、object、block等等等……

dispatch的各种功能

这次就先看一下任务相关的一些方法啦,日常都经常会用到,通过这些方法可以让任务执行在不同线程,以及做一些组合任务等,常见的一些方法如下:(async和sync太常用就不举例啦)

// 异步/同步抛任务:
dispatch_async (传入block)
dispatch_sync
dispatch_async_f (传入函数的形式,每种方法都有就不赘述啦)
dispatch_sync_f

// 循环执行:
dispatch_apply

// 延迟执行:
dispatch_after

// 栅栏任务:
dispatch_barrier_async
dispatch_barrier_sync

// 单次执行
dispatch_once

// 组合执行
dispatch_group_create
dispatch_group_async
dispatch_group_wait
dispatch_group_notify
dispatch_group_enter
dispatch_group_leave

// 信号量控制线程安全等
dispatch_semaphore_create
dispatch_semaphore_wait
dispatch_semaphore_signal

① dispatch_apply

这个方法主要是用于多线程迭代,可以比for循环(只能运行在一个线程)更快的执行,并且它之后的代码会等待迭代执行完再继续执行。

dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
  NSLog(@"index:%zu, thread:%@", index, [NSThread currentThread]);
});
    
NSLog(@"finished");

输出:
index:3, thread:<NSThread: 0x600003f6f200>{number = 4, name = (null)}
index:2, thread:<NSThread: 0x600003f0ed80>{number = 3, name = (null)}
index:4, thread:<NSThread: 0x600003f6f200>{number = 4, name = (null)}
index:0, thread:<NSThread: 0x600003f0a940>{number = 1, name = main}
index:1, thread:<NSThread: 0x600003f7b140>{number = 5, name = (null)}
index:5, thread:<NSThread: 0x600003f0ed80>{number = 3, name = (null)}
index:7, thread:<NSThread: 0x600003f7b140>{number = 5, name = (null)}
index:6, thread:<NSThread: 0x600003f6f200>{number = 4, name = (null)}
index:9, thread:<NSThread: 0x600003f0ed80>{number = 3, name = (null)}
index:8, thread:<NSThread: 0x600003f0a940>{number = 1, name = main}
finished

注意其实apply会等待,所以和sync类似,不能在串行队列dispatch_apply给自己串行队列哦,这样还是会死锁crash的。

因为迭代如果放入并发队列是会交给不同的线程,所以执行顺序也不是固定的,但是它之后的代码一定会等所有迭代执行结束后再执行。为了防止主线程被堵塞,最好在其他线程执行dispatch_apply。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
  dispatch_apply(10, dispatch_get_global_queue(0, 0), ^(size_t index) {
    NSLog(@"DOING JOB: %zu", index);
  });
        
  dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"finished");
  });
});

② dispatch_barrier_async & dispatch_barrier_sync

栅栏任务,会等它前面的任务都执行结束后再开始执行,并且当栅栏任务执行时,它后面被抛入的任务只能等待栅栏任务执行结束再开始执行。

栅栏任务
dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    NSLog(@"task 1");
    sleep(10);
    NSLog(@"task 1 end");
});

dispatch_async(concurrentQueue, ^{
    NSLog(@"task 2");
    sleep(10);
    NSLog(@"task 2 end");
});

// 栅栏任务
dispatch_barrier_async(concurrentQueue, ^{
    NSLog(@"task 3 barrier");
    sleep(10);
    NSLog(@"task 3 barrier end");
});

dispatch_async(concurrentQueue, ^{
    NSLog(@"task 4");
    sleep(10);
    NSLog(@"task 4 end");
});

dispatch_async(concurrentQueue, ^{
    NSLog(@"task 5");
    sleep(10);
    NSLog(@"task 5 end");
});

输出:
task 2
task 1
task 2 end
task 1 end
task 3 barrier
task 3 barrier end
task 5
task 4
task 5 end
task 4 end

※dispatch_barrier_async和dispatch_barrier_sync的区别是啥呢?
dispatch_barrier_async不会等待栅栏任务执行完就会接着执行后面的,但是dispatch_barrier_sync会等待栅栏执行完再继续后面的代码执行。

dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{
    NSLog(@"task 1");
    sleep(10);
    NSLog(@"task 1 end");
});

// 栅栏任务
dispatch_barrier_sync(concurrentQueue, ^{
    NSLog(@"task 3 barrier");
    sleep(10);
    NSLog(@"task 3 barrier end");
});

NSLog(@"continue");

输出:
task 1
task 1 end
task 3 barrier
task 3 barrier end
continue

栅栏任务的对于串行队列其实没什么意义,毕竟串行队列本来就已经是依次执行的了,但也不是所有并行队列都支持栅栏任务。

栅栏任务队列限制.png

栅栏任务只对自己创建的并行队列生效哦

如果用global queue,达到的效果就和dispatch_async/dispatch_sync是一致的啦,并不能限制运行顺序了。

dispatch_queue_t gq = dispatch_get_global_queue(0, 0);
dispatch_async(gq, ^{
    NSLog(@"task 1");
    sleep(10);
    NSLog(@"task 1 end");
});

dispatch_async(gq, ^{
    NSLog(@"task 2");
    sleep(10);
    NSLog(@"task 2 end");
});

dispatch_barrier_async(gq, ^{
    NSLog(@"task 3 barrier");
    sleep(10);
    NSLog(@"task 3 barrier end");
});

输出:
task 1
task 2
task 3 barrier
task 3 barrier end
task 2 end
task 1 end

③ dispatch_after

延迟任务,在指定时间将任务抛到队列,官方的定义(A queue to which the given block will be submitted at the specified time),不一定任务执行会在指定的time哦,只是抛入队列的时间哦,如果队列在那个时候出现了问题可能不会按点执行或者可能不执行QAQ。

它会启动一个倒计时,在到时以后将任务抛入~

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"delay task");
});

它的使用非常简单,只要传入时间点以及执行的任务block就可以啦,时间点支持dispatch_time或者dispatch_walltime。

dispatch_time和dispatch_walltime的区别是神马呢?
简要的说下就是dispatch_time的倒计时在手机休眠的时候就会停止,手机启动再继续倒计时;dispatch_walltime则是在我们定义的时候就会生成触发时间的绝对值,无论手机是否休眠过,到点就会触发。


④ dispatch_once

如果使用过单例一定见过这个函数,dispatch_once是确保任务在App声明周期内只执行一次的方法。

static dispatch_once_t onceToken;
    
for (NSInteger i = 0; i < 5; i++) {
    dispatch_once(&onceToken, ^{
        NSLog(@"once task");
    });
}

dispatch_once(&onceToken, ^{
  NSLog(@"twice task");
});

输出只有一次once task,所以其实单次执行是和onceToken绑定的,与执行的是什么block并没有关系,下面我们在任务执行前中后打印一下onceToken的数值。

static dispatch_once_t onceToken;
NSLog(@"once onceToken:%ld", onceToken);

dispatch_once(&onceToken, ^{
    NSLog(@"once onceToken:%ld", onceToken);
});

NSLog(@"once onceToken:%ld", onceToken);

输出:
once onceToken:0
once onceToken:768
once onceToken:-1

可以看出当任务未执行时,onceToken是0,然后任务执行开始时会置为一个神奇的数字,在结束以后会改为-1。

P.S. static的局部变量的生命周期是整个App的生命周期哦,虽然作用域没有变,但只要App不销毁就只会运行一次dispatch_once哦

static修饰会默认将其初始化为0,当值为0时才会执行block。onceToken为除0和-1的其他值时,线程被阻塞,等待onceToken值改变。当block执行完成,底层会将onceToken设置为-1,这也就是为什么要传onceToken的地址(static修饰的变量可以通过地址修改onceToken的值),同时底层会加锁来保证这个方法是线程安全的。

dispatch_once参考图

dispatch_once的线程安全以及死锁问题可以参考:https://www.jianshu.com/p/f4e38d06fa04

它的原理是在开始执行block以后改变信号量,当有其他调用者在block未结束时调用dispatch_once的话,它内部会维护一个调用者链表,当block结束时改变信号量,通知到各个调用者。
由于dispatch_once的等待特性,如果class A的init方法内需要init class B,然后class B的init方法内需要init class A,而两者都是dispatch_once,那么循环调用将会导致互相等待的死锁。


⑤ dispatch_group_xxxx

group主要用于管理互相关联的任务,即一组任务。之前的栅栏任务barrier可以确保在同一个queue内的某几个任务完成后再继续执行后面的任务,但是group可以确保在不同queue内的任务在确保完成后再继续执行。

当我们需要异步执行多个任务,并且在这几个任务都完成以后再回主线程执行一些操作的时候,就可以用任务组。

(1) 创建任务组 dispatch_group_create

创建直接调用即可非常简单label都不需要~

dispatch_group_t group1 = dispatch_group_create();
(2) 抛任务 dispatch_group_async

dispatch_group_async和dispatch_async很类似,区别只是这个任务会算在group里面,需要给dispatch_group_async一个group & queue & block,任务就会被抛到指定queue啦。

是木有同步方法dispatch_group_sync的哦,毕竟如果是同步的,其实group也没有太大意义。

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
  NSLog(@"group1 task");
});
(3) 等待任务执行完毕 dispatch_group_wait
dispatch_group_wait(group, time out时间点);

dispatch_group_wait是用于让当前线程阻塞并等待group内所有任务执行完毕以后继续当前线程的运行,具体需要传给它一个group以及什么时候time out,到了time out的时间即使group仍旧没有执行完毕,或者如果group里面没有task,线程会接着执行。

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"group1 task1 start");
    sleep(5);
    
    NSLog(@"group1 task1 end");
});

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
    dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"group1 task2 start");
        sleep(5);
        
        NSLog(@"group1 task2 end");
    });
});

dispatch_group_wait(group1, DISPATCH_TIME_FOREVER);
NSLog(@"group1 continue");

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"group1 task3 start");
    sleep(5);
    
    NSLog(@"group1 task3 end");
});

输出:
group1 task1 start
group1 task1 end
group1 task2 start
group1 task2 end
group1 continue
group1 task3 start
group1 task3 end

这里即使task 2是在wait之后抛入group的,但是仍旧会等待task 2完成之后才会继续wait以后的代码,所以其实wait需要等待group内的task全部完成,无论是在wait前后抛入group的。

※wait听起来就会很容易在队列产生死锁,我们来试一下(现在只要有等待这个事儿,我就下意识觉得串行队列很容易崩-。-)

dispatch_group_async(group1, dispatch_get_main_queue(), ^{
    NSLog(@"group1 task1 start");
    sleep(5);
    
    NSLog(@"group1 task1 end");
});

dispatch_group_wait(group1, DISPATCH_TIME_FOREVER);
NSLog(@"group1 continue");

当给主队列抛一个异步block,并且dispatch_group_wait这个block的执行,如果time out的时间点设为DISPATCH_TIME_FOREVER,那么就会一直等待,上面的代码不会有任何输出,因为主队列在dispatch_group_wait这行阻塞着,不会执行抛给它的task1,又会造成无法解开阻塞,于是就一直停留在wait那一行,不会产生crash。

官方表示,如果从多个线程同时wait的结果是undefined,也就是不一定是神马样子,所以如果多个线程都会wait一个任务组的执行结果的时候注意一下避免出现同时执行wait啊,不过一般都很少会真的能做到同时。

The result of calling this function from multiple threads simultaneously with the same dispatch group is undefined.

(4) 异步等待任务执行完毕 dispatch_group_notify

wait任务组执行完毕会阻塞线程,还有一种方式可以不用阻塞当前线程,即

dispatch_group_notify(group, queue, block);

当group内所有任务执行完毕后,会在第二个参数的queue内执行第三个block,下面举个栗子~

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"group1 task1 start");
    sleep(5);
    
    NSLog(@"group1 task1 end");
});

dispatch_group_notify(group1, dispatch_get_main_queue(), ^{
    NSLog(@"group1 tasks end");
});

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"group1 task2 start");
    sleep(10);
    
    NSLog(@"group1 task2 end");
});

输出:
group1 task1 start
group1 task2 start
group1 task1 end
group1 task2 end
group1 tasks end
(5) 手动管理任务执行开始与结束 dispatch_group_enter& dispatch_group_leave

dispatch_group_enter(group)即标记group中未完成任务数+1
dispatch_group_leave(group)即标记group中未完成任务数-1

当group的未完成任务数=0时,才会调用dispatch_group_wait后的以及notify内的代码,如果dispatch_group_enter次数大于dispatch_group_leave那么永远也不会执行notify和wait;如果dispatch_group_leave次数大于dispatch_group_enter,会crash哦。

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"group1 task1 start");
    sleep(5);

    NSLog(@"group1 task1 end");
});

dispatch_group_notify(group1, dispatch_get_main_queue(), ^{
    NSLog(@"group1 tasks end");
});

dispatch_group_enter(group1);

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"group1 leave");
    dispatch_group_leave(group1);
});

输出:
group1 task1 start
group1 task1 end
group1 leave
group1 tasks end

即使task 1在5s的时候已经结束了,但是notify里面的代码并不会执行,因为dispatch_group_enter增加了未完成任务数,然后当10s的时候dispatch_group_leave以后,notify内的代码才执行。

用dispatch_group_async不是自然就会在任务结束后调用wait和notify么,为什么还要用enter和leave手动管理嘞?
假设你的任务是异步被抛入group的,也就是执行到wait和notify代码的时候,可能任务还没有被抛入group,那么其实就没有达到等待的目的,所以手动enter和leave可以管理一些不是立刻被抛入group的任务。



P.S.题外话其实用for循环+group就可以实现dispatch_apply哈

for (NSInteger i = 0; i < 10; i++) {
    dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
        NSLog(@"group1 task:%ld thread:%@", (long)i, [NSThread currentThread]);
    });
}

dispatch_wait(group1, DISPATCH_TIME_FOREVER);
NSLog(@"group1 tasks end");

输出:
group1 task:1 thread:<NSThread: 0x6000017a02c0>{number = 5, name = (null)}
group1 task:0 thread:<NSThread: 0x6000017a4680>{number = 4, name = (null)}
group1 task:2 thread:<NSThread: 0x6000017a02c0>{number = 5, name = (null)}
group1 task:3 thread:<NSThread: 0x6000017a4680>{number = 4, name = (null)}
group1 task:4 thread:<NSThread: 0x6000017a02c0>{number = 5, name = (null)}
group1 task:5 thread:<NSThread: 0x6000017a0340>{number = 6, name = (null)}
group1 task:6 thread:<NSThread: 0x6000017a4780>{number = 7, name = (null)}
group1 task:7 thread:<NSThread: 0x6000017a03c0>{number = 8, name = (null)}
group1 task:8 thread:<NSThread: 0x6000017a0440>{number = 9, name = (null)}
group1 task:9 thread:<NSThread: 0x6000017a46c0>{number = 10, name = (null)}
group1 tasks end

⑥ dispatch_semaphore_xxx 信号量

信号量其实也是用来确保一些任务已经做完了以后才会执行另外一些任务的工具,以及可以用于确保线程安全。

信号量其实就是一个数字,当它大于等于0的时候就可以继续执行,如果它小于0就会等待。比如当我们需要下载完图片才能进行图片处理的时候,也可以用信号量保证图片处理在图片下载结束之后进行。

dispatch_semaphore_create:创建一个信号量并指定起始信号量的值
·传入value < 0, 返回nil,使用wait或者signal会crash
·传入value = 0,多线程在等待某个特定线程的结束
·传入value > 0,有一定数量的资源可供多线程使用

dispatch_semaphore_signal:发送一个信号,信号总量+1
·返回值非0即有被唤醒的等待,0则代表没有唤醒任何等待

dispatch_semaphore_wait:信号总量-1,同时等待信号量>=0时继续执行后面的代码(阻塞所在线程)。
·可以设定timeout时间,返回值如果是0就是被信号量不小于0唤醒了,如果是非0就是timeout唤醒的

举个栗子,如果创建一个dispatch_semaphore_create(0)即初始总量为0的信号量,然后立刻执行dispatch_semaphore_wait,wait使信号总量变为-1,所以所在线程阻塞等待,直到有dispatch_semaphore_signal执行将信号总量+1变为0以后,dispatch_semaphore_wait才会停止等待,继续执行后面的代码。

这里用信号量来实现一下多个线程串行做任务~

dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"task1 start");
    sleep(5);
    
    NSLog(@"task1 end thread:%@", [NSThread currentThread]);
    dispatch_semaphore_signal(semaphore);
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"task2 start");
    sleep(5);
    
    NSLog(@"task2 end thread:%@", [NSThread currentThread]);
    dispatch_semaphore_signal(semaphore);
});

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    
    NSLog(@"task3 start");
    sleep(5);
    
    NSLog(@"task3 end thread:%@", [NSThread currentThread]);
    dispatch_semaphore_signal(semaphore);
});

输出:
task1 start
task1 end thread:<NSThread: 0x6000025c0140>{number = 4, name = (null)}
task3 start
task3 end thread:<NSThread: 0x6000007d3f40>{number = 8, name = (null)}
task2 start
task2 end thread:<NSThread: 0x600002504240>{number = 9, name = (null)}

每次只会唤醒一个wait,唤醒哪一个其实是不一定的哦。


⑦ 多个耗时操作示例

dispatch_group_t group1 = dispatch_group_create();
dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    //模拟网络连接下载耗时操作
    NSLog(@"downloading 1");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"downloading 1 finished");
    });
});

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"downloading 2");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"downloading 2 finished");
    });
});

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    NSLog(@"downloading 3");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"downloading 3 finished");
    });
});

dispatch_group_notify(group1, dispatch_get_main_queue(), ^{
    NSLog(@"update UI");
});

如果在异步任务线程抛任务,单纯用group会使实际耗时任务还没完成就走到了notify,这样会导致UI刷新的时候实际没有数据,用户看到的还是空白,用信号量可以避免UI先更新的问题。

dispatch_group_t group1 = dispatch_group_create();
dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    //模拟网络连接下载耗时操作
    NSLog(@"downloading 1");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"downloading 1 finished");
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    NSLog(@"downloading 2");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"downloading 2 finished");
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_async(group1, dispatch_get_global_queue(0, 0), ^{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    
    NSLog(@"downloading 3");
    
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_global_queue(0, 0), ^{
        NSLog(@"downloading 3 finished");
        dispatch_semaphore_signal(semaphore);
    });
    
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
});

dispatch_group_notify(group1, dispatch_get_main_queue(), ^{
    NSLog(@"update UI");
});

用信号量可以保证group里面的任务只有在耗时任务结束以后再返回,从而达到当group所有任务都结束的时候,说明耗时任务也都结束了。如果你还希望异步任务可以顺序执行的话,其实可以用串行队列或者NSOperation的依赖关系做。(P.S. 这里用手动group leave和enter也可以做)

参考文档:

  1. GCD: https://www.jianshu.com/p/2d57c72016c6
  2. once原理: https://www.jianshu.com/p/3b823d5e1d9f
    https://xiaozhuanlan.com/topic/7916538240
  3. 信号量应用:https://www.jianshu.com/p/56c4838aa3e0
上一篇下一篇

猜你喜欢

热点阅读