- 理解同步,异步,并发/并行,串行概念
- 介绍 iOS 多线程中 GCD 的相关知识以及使用方法
1. 快速理解同步 ,异步,并发/并行,串行
首先,串行并行针对的是队列,同步异步针对的是任务;如果觉得队列和任务不太好理解咱们可以打个比方,假设一个应用程序是一个工厂,那队列就是里面的流水线以及线上的工人,而流水线上的工人所要处理的产品就是所谓的任务。
同步
- 多个任务情况下,一个任务A执行结束,才可以执行另一个任务B。同步执行的任务是存在同一线程中的。不具备开线程能力。
- 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
异步
- 多个任务情况下,一个任务A正在执行,同时可以执行另一个任务B。任务B不用等待任务A结束才执行。具备创建线程能力,存在多条线程。
- 异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。
- 把一个任务添加到某queue后就马上离开,不会做任何等待,不管任务在那个queue里的执行状态,可以继续执行任务。
并发和并行
- 并发和并行其实是异步线程实现的两种形式。
- 并行其实是真正的异步,多核CUP可以同时开启多条线程供多个任务同时执行,互补干扰。
- 但是并发就不一样了,是一个伪异步。在单核CUP中只能有一条线程,但是又想执行多个任务。这个时候,只能在一条线程上不停的切换任务,比如任务A执行了20%,任务A停下里,线程让给任务B,任务执行了30%停下,再让任务A执行。这样我们用的时候,由于CUP处理速度快,你看起来好像是同时执行,其实不是的,同一时间只会执行单个任务。
- 并发不一定并行,但并行一定并发。
- 并行队列的并行功能只有在异步(dispatch_async)函数下才有效,虽然可开启多线程,但不具备创建多线程能力,这跟任同步异步类型有关。
串行
- 它是同步线程的实现方式,就是任务A执行结束才能开始执行B,单个线程只能执行一个任务,就如单行道只能行驶一辆车。
2. GCD
2.1 GCD的基本使用
- 创建一个队列(串行队列或并行队列)
- 将任务追加到任务的等待队列中,然后系统会根据任务类型执行任务(同步执行或异步执行)
创建方法
- 自定义队列
dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)第一个参数表示队列的唯一标识符,用于DEBUG,可为空;第二个参数用来识别是串行队列还是并行队列。DISPATCH_QUEUE_SERIAL表示串行队列,DISPATCH_QUEUE_CONCURRENT表示并发行列。 - // 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL); - // 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT; - 主队列:
dispatch_get_main_queue()一种特殊的串行队列 - 全局队列:
dispatch_get_global_queue(<#long identifier#>, <#unsigned long flags#>)第一个参数队列优先级标志,第二个参数暂时没用,可写0; - 任务(同步和异步)
dispatch_sync(queue, ^{ // 这里放同步执行任务代码 });dispatch_async(queue, ^{ // 这里放异步执行任务代码 });
应用组合
| 区别 | 并行队列 | 串行队列 | 主队列 |
|---|---|---|---|
| 同步(sync) | 没有开启新线程,串行执行任务. 只有当前这个线程,不存在并发. 同步需要等待队列的任务执行结束 |
没有开启新线程,串行执行任务. 串行队列每次只有一个任务被执行. 同步执行不具备开启新线程的能力 |
主线程中调用死锁,互相等待. 其他线程中调任务是在主线程(非当前线程)中执行的不会开启新线程,按顺序执行 |
| 异步(async) | 有开启新线程,并发执行任务. 异步同时执行多个任务. 没有明显先后 |
开启新线程(1条)线程中串行执行任务. 异步有开新线程能力,串行队列只开启一个线程 |
只在主线程中执行任务,按顺序执行。 |
线程间通信
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);dispatch_async(dispatch_get_main_queue(), ^{}
2.2 GCD其他功能用法
GCD延时方法 dispatch_after
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });需要注意的是:dispatch_after函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到指定队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after函数是很有效的。- 延时还有
performSelector:<#(nonnull SEL)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#>,使用performSelector你有没有发现在子线程中调用这个方法,有时候延时执行的代码并没有走,performSelector fterDelay相当于 告诉当前线程 用当前线程的定时器去调,但是我们知道,在子线程中,默认是没有定时器的,所以将没有被调用的机会。
GCD一次性方法 dispatch_once
- 我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的
dispatch_once函数。使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。 dispatch_once有两个参数,dispatch_once_t的特殊参数,还有一个块参数。对于onceToken标记,该函数保证相关的块必定会执行,且执行一次。此操作完全是线程安全的。注意:对于只执行一次的块来说,对于传入函数的标记参数必须完全相同,因此,开发时需要将标记变量声明在static或global作用于中。
+ (instancetype)sharedInstance {
static sortMethods * sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc]init];//只执行一次,并且是线程安全的
});
return sharedInstance;
}
GCD栅栏方法 dispatch_barrier_async``dispatch_barrier_sync
barrier的直译是障碍,栅栏和分界线的意思。例如,要执行三部分任务,第二部分要在第一部分之后才执行,第三部分要在第二部分之后才执行,所以可以把第二部分视作一个栅栏barrier。barrier函数会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在barrier函数追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。
- 我们先看
dispatch_barrier_sync
/** 栅栏方法*/
- (void)barrier{
dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"1------%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"2------%@",[NSThread currentThread]);
});
dispatch_barrier_sync(concurrentQueue, ^{//异步栅栏
sleep(2);
NSLog(@"barrier------%@",[NSThread currentThread]);
});
NSLog(@"main---------");
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"3------%@",[NSThread currentThread]);
});
NSLog(@"main----------");
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"4------%@",[NSThread currentThread]);
});
}
//输出
1------<NSThread: 0x6000004795c0>{number = 3, name = (null)}
2------<NSThread: 0x60400027a700>{number = 4, name = (null)}
barrier------<NSThread: 0x60400006b600>{number = 1, name = main}
main---------
main----------
4------<NSThread: 0x6000004795c0>{number = 3, name = (null)}
3------<NSThread: 0x60400027a700>{number = 4, name = (null)}
请注意我在队列任务里面都设置有延时的,但是还是先执行前面两个任务,然后执行barrier,然后再执行后面的两个任务,请注意main的位置, 可以说明后面的main和后面两个个异步线程是按照正常的多线程处理的。
- 再看
dispatch_barrier_async
/** 栅栏方法*/
- (void)barrier{
dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"1------%@",[NSThread currentThread]);
});
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"2------%@",[NSThread currentThread]);
});
dispatch_barrier_async(concurrentQueue, ^{//异步栅栏
sleep(2);
NSLog(@"barrier------%@",[NSThread currentThread]);
});
NSLog(@"main---------");
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"3------%@",[NSThread currentThread]);
});
NSLog(@"main----------");
dispatch_async(concurrentQueue, ^{//异步并行
sleep(2);
NSLog(@"4------%@",[NSThread currentThread]);
});
}
//输出
main---------
main----------
1------<NSThread: 0x60000026bb40>{number = 4, name = (null)}
2------<NSThread: 0x604000465cc0>{number = 3, name = (null)}
barrier------<NSThread: 0x60000026bb40>{number = 4, name = (null)}
3------<NSThread: 0x60000026bb40>{number = 4, name = (null)}
4------<NSThread: 0x604000465cc0>{number = 3, name = (null)}
这个时候可以发现main的位置变了,但是执行任务的顺序还是先执行前面两个,再执行障碍任务,最后执行最后两个个任务 这说明了,异步障碍任务只会将队列中的任务设置障碍而不会阻碍后面的主线程的代码。
GCD快速迭代方法 dispatch_apply
- 通常我们会用 for 循环遍历,但是GCD给我们提供了快速迭代的函数
dispatch_apply。dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。我们可以利用异步队列同时遍历。比如说遍历 0~5 这6个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以同时遍历多个数字。
/** 快速迭代方法 */
- (void)dispatch_apply{
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t index) {
NSLog(@"%zd---%@",index, [NSThread currentThread]);
});
NSLog(@"apply---end");
}
//输出
apply---begin
1---<NSThread: 0x604000473780>{number = 3, name = (null)}
3---<NSThread: 0x600000465900>{number = 5, name = (null)}
2---<NSThread: 0x600000465540>{number = 4, name = (null)}
0---<NSThread: 0x60400006d780>{number = 1, name = main}
4---<NSThread: 0x604000473780>{number = 3, name = (null)}
5---<NSThread: 0x600000465900>{number = 5, name = (null)}
apply---end
- 0~5 打印顺序不定,最后打印了apply---end。因为是在并发队列中异步队执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是apply---end一定在最后执行。这是因为dispatch_apply函数会等待全部任务执行完毕。
GCD的队列组 dispatch_group
在追加到Dispatch Queue中的多个任务处理完毕之后想执行结束处理,这种需求会经常出现。在使用Concurrent Queue 时,可能会同时使用多个Dispatch Queue时,源代码就会变得很复杂。
- 这时候我们需要GCD队列组。
调用队列组的
dispatch_group_async先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的dispatch_group_enter、dispatch_group_leave组合来实现dispatch_group_async。 调用队列组的dispatch_group_notify回到指定线程执行任务。或者使用dispatch_group_wait回到当前线程继续向下执行(会阻塞当前线程)。
dispatch_group_async和dispatch_group_notify
监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。
/** 队列组*/
- (void)dispatch_groupNotify{
NSLog(@"currentThread --- %@",[NSThread currentThread]);
NSLog(@"begin--------------------");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"1---------%@",[NSThread currentThread]);
});
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"2---------%@",[NSThread currentThread]);
});
dispatch_group_notify(group, concurrentQueue, ^{
NSLog(@"end-------------------");
});
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"3---------%@",[NSThread currentThread]);
});
}
//输出
currentThread --- <NSThread: 0x60400007d740>{number = 1, name = main}
begin--------------------
3---------<NSThread: 0x600000469b40>{number = 5, name = (null)}
1---------<NSThread: 0x60400026e980>{number = 4, name = (null)}
2---------<NSThread: 0x60400047f340>{number = 3, name = (null)}
end-------------------
从dispatch_group_notify相关代码运行输出结果可以看出: 当所有任务都执行完成之后,才执行dispatch_group_notify block 中的任务。
dispatch_group_async和dispatch_group_wait
/** 队列组*/
- (void)dispatch_groupWait{
NSLog(@"currentThread --- %@",[NSThread currentThread]);
NSLog(@"begin--------------------");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"1---------%@",[NSThread currentThread]);
});
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"2---------%@",[NSThread currentThread]);
});
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"3---------%@",[NSThread currentThread]);
});
// 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"end-------------------");
}
//输出
currentThread --- <NSThread: 0x600000079e80>{number = 1, name = main}
begin--------------------
2---------<NSThread: 0x60000066a380>{number = 4, name = (null)}
3---------<NSThread: 0x604000465640>{number = 3, name = (null)}
1---------<NSThread: 0x604000465700>{number = 5, name = (null)}
end-------------------
当所有任务执行完成之后,才执行 dispatch_group_wait 之后的操作。但是,使用dispatch_group_wait 会阻塞当前线程。
dispatch_group_enter、dispatch_group_leave
dispatch_group_enter标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1;dispatch_group_leave标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1;当group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。
/** 队列组*/
- (void)dispatch_groupEnterLeave{
NSLog(@"currentThread --- %@",[NSThread currentThread]);
NSLog(@"begin--------------------");
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t concurrentQueue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
NSLog(@"1---------%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
NSLog(@"2---------%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_enter(group);
dispatch_async(concurrentQueue, ^{
NSLog(@"3---------%@",[NSThread currentThread]);
dispatch_group_leave(group);
});
dispatch_group_notify(group, concurrentQueue, ^{
NSLog(@"end-------------------");
});
}
//输出
currentThread --- <NSThread: 0x60400006f0c0>{number = 1, name = main}
begin--------------------
1---------<NSThread: 0x60000027f240>{number = 3, name = (null)}
2---------<NSThread: 0x60000027edc0>{number = 4, name = (null)}
3---------<NSThread: 0x60000027f500>{number = 5, name = (null)}
end-------------------
GCD的信号量 dispatch_semaphore
主要作用:保持线程同步,为线程加锁。是持有计数的信号,使用信号量计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。只有三个方法。
dispatch_semaphore_t dispatch_semaphore_create(long value):会根据传入的long型参数创建对应数目的信号量,long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout): 若信号量大于0,则会使信号量减1并返回,程序继续住下执行;如果信号量是0,则会阻塞当前线程,直到信号量大于0或者根据传入的等待时间来等待。long dispatch_semaphore_signal(dispatch_semaphore_t dsema):使增加一个信号量。
线程同步
- (void)dispatch_semaphore{
NSLog(@"currentThread --- %@",[NSThread currentThread]);
NSLog(@"begin--------------------");
dispatch_queue_t queue =dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semphore = dispatch_semaphore_create(0);
__block int j = 0;
dispatch_async(queue, ^{
NSLog(@"1----------%@",[NSThread currentThread]);
j = 100;
dispatch_semaphore_signal(semphore);
});
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
NSLog(@"end------------ j=%d",j);
}
//输出
currentThread --- <NSThread: 0x6040000710c0>{number = 1, name = main}
begin--------------------
1----------<NSThread: 0x600000473640>{number = 3, name = (null)}
end------------ j=100
//如果注释掉`dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER)`;
//输出
currentThread --- <NSThread: 0x60400007fdc0>{number = 1, name = main}
begin--------------------
end------------ j=0
1----------<NSThread: 0x60400027cfc0>{number = 3, name = (null)}
由于是将block异步添加到一个并行队列里面,所以程序在主线程跃过block直接到
dispatch_semaphore_wait这一行,因为dispatch_semaphore_create(0)创建semaphore信号量计数为0,wait的时间值又为DISPATCH_TIME_FOREVER,所以当前线程会一直阻塞,直到block在子线程执行到dispatch_semaphore_signal,使信号量+1,此时semaphore信号量为1了,所以程序继续往下执行。这就保证了线程间同步了。
线程加锁(保证线程安全)
直接上代码
- (void)dispatch_semaphoreSafe{
NSLog(@"currentThread --- %@",[NSThread currentThread]);
NSLog(@"begin--------------------");
dispatch_queue_t queue =dispatch_get_global_queue(0, 0);
dispatch_semaphore_t semphore = dispatch_semaphore_create(1);
for (int i = 0; i < 5 ; i++) {
dispatch_async(queue, ^{
//相当于枷锁
dispatch_semaphore_wait(semphore, DISPATCH_TIME_FOREVER);
NSLog(@"%d----------%@",i,[NSThread currentThread]);
//相当于解锁
dispatch_semaphore_signal(semphore);
});
}
NSLog(@"main end------------ ");
}
dispatch_semaphore_create(1)信号量计数为1。当线程1执行到
dispatch_semaphore_wait这一行时,semaphore的信号量为1,所以使信号量-1变为0,并且线程1继续往下执行;如果当在线程1NSLog这一行代码还没执行完的时候,又有线程2来访问,执行dispatch_semaphore_wait时由于此时信号量为0,且时间为DISPATCH_TIME_FOREVER,所以会一直阻塞线程2(此时线程2处于等待状态),直到线程1执行完NSLog并执行完dispatch_semaphore_signal使信号量为1后,线程2才能解除阻塞继续住下执行。以上可以保证同时只有一个线程执行NSLog这一行代码。
@end 参考网上资料学习并整理,欢迎指正。