线程相关

线程相关

异步任务&同步任务

两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

  • 同步执行(sync)
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async)
    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力。
1
2
3
4
GCD同步
dispatch_sync(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
GCD异步
dispatch_async(<#dispatch_queue_t _Nonnull queue#>, <#^(void)block#>)
队列(Dispatch Queue)

队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。

  • 串行队列(Serial Dispatch Queue)
    • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
    • DISPATCH_QUEUE_SERIAL 表示串行队列
    • dispatch_get_main_queue() 属于串行队列
  • 并发队列(Concurrent Dispatch Queue)
    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
    • DISPATCH_QUEUE_CONCURRENT 表示并发队列。
    • dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);属于并发队列。第一个参数表示队列优先级,一般用 DISPATCH_QUEUE_PRIORITY_DEFAULT。第二个参数暂时没用,用 0 即可
1
2
3
4
5
6
7
8
第一个参数表示队列的唯一标识符,用于 DEBUG,可为空。队列的名称推荐使用应用程序 ID 这种逆序全程域名。
第二个参数用来识别是串行队列还是并发队列。DISPATCH_QUEUE_SERIAL 表示串行队列,DISPATCH_QUEUE_CONCURRENT 表示并发队列
dispatch_queue_create(<#const char * _Nullable label#>, <#dispatch_queue_attr_t _Nullable attr#>)
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("com.testQueue", DISPATCH_QUEUE_CONCURRENT);
区别 并发队列 串行队列 主队列
同步(sync) 没有开启新线程,串行执行任务 没有开启新线程,串行执行任务 死锁卡住不执行
异步(async) 有开启新线程,并发执行任务 有开启新线程(1条),串行执行任务 没有开启新线程,串行执行任务
区别 『异步执行+并发队列』嵌套『同一个并发队列』 『同步执行+并发队列』嵌套『同一个并发队列』 『异步执行+串行队列』嵌套『同一个串行队列』 『同步执行+串行队列』嵌套『同一个串行队列』
同步(sync) 没有开启新的线程,串行执行任务 没有开启新线程,串行执行任务 死锁卡住不执行 死锁卡住不执行
异步(async) 有开启新线程,并发执行任务 有开启新线程,并发执行任务 有开启新线程(1 条),串行执行任务 有开启新线程(1 条),串行执行任务

开辟线程的个数和异步+并发队列有关。

队列是依托于线程而存在的,只有一个线程串行队列并行队列并没有什么不同。所以可以先分析线程的个数再分析队列从而得到执行的步骤。

GCD 延时执行方法:dispatch_after

我们经常会遇到这样的需求:在指定时间(例如 3 秒)之后执行某个任务。可以用 GCD 的dispatch_after方法来实现。
需要注意的是:dispatch_after方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after方法是很有效的

1
2
3
4
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2.0 秒后异步追加任务代码到主队列,并开始执行
NSLog(@"after---%@",[NSThread currentThread]); // 打印当前线程
});
GCD 栅栏方法:dispatch_barrier_async

dispatch_barrier_async 方法会等待前边追加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到该异步队列中。然后在 dispatch_barrier_async 方法追加的任务执行完毕之后,异步队列才恢复为一般动作,接着追加任务到该异步队列并开始执行。

我们要注意的是不能使用全局并发队列( dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);)否则会散失栅栏函数的意义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dispatch_queue_t queue = dispatch_queue_create("ccc", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_async(queue, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});

实现同步锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_queue_t queue = dispatch_queue_create("ccc", DISPATCH_QUEUE_CONCURRENT);
- (void)setTestStr:(NSString *)testStr {
dispatch_barrier_async(quene, ^{
_testStr = testStr;
});
}
- (NSString *)testStr {
__block NSString *tempStr;
dispatch_sync(quene, ^{
tempStr = _testStr;
});
return tempStr;
}
GCD 一次性代码(只执行一次):dispatch_once

整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 方法。使用 dispatch_once 方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once 也可以保证线程安全。

1
2
3
4
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码(这里面默认是线程安全的)
});
GCD 快速迭代方法:dispatch_apply

dispatch_apply 按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。

如果是在串行队列中使用 dispatch_apply,那么就和 for 循环一样,按顺序同步执行。但是这样就体现不出快速迭代的意义了。

我们可以利用并发队列进行异步执行。比如说遍历 0~5 这 6 个数字,for 循环的做法是每次取出一个元素,逐个遍历。dispatch_apply可以 在多个线程中同时(异步)遍历多个数字。

还有一点,无论是在串行队列,还是并发队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。

1
2
3
4
5
6
7
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");
GCD队列组:dispatch_group

调用队列组的 dispatch_group_async先把任务放到队列中,然后将队列放入队列组中异步执行。如不调用队列组则是顺序执行。或者使用队列组的 dispatch_group_enterdispatch_group_leave组合来实现 dispatch_group_async

调用队列组的 dispatch_group_notify回到指定线程执行任务。或者使用 dispatch_group_wait回到当前线程继续向下执行(会阻塞当前线程)

dispatch_group_enter标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数 +1

dispatch_group_leave标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数 -1。

当 group 中未执行完毕任务数为0的时候,才会使 dispatch_group_wait解除阻塞,以及执行追加到 dispatch_group_notify中的任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---begin");
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步任务 1、任务 2 都执行完毕后,回到主线程执行下边任务
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"group---end");
});
//或
// 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程)
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"group---end");
GCD 信号量:dispatch_semaphore

信号量是基于计数器的一种多线程同步机制,用来管理对资源的并发访问。(类似于NSOperationQueue的设置最大并发量)

Dispatch Semaphore 中,使用计数来完成这个功能,计数小于 0 时等待,阻塞线程。计数为 0 或大于 0 时,计数减 1 ,继续运行。

dispatch_semaphore_create:创建一个 Semaphore 并初始化信号的总量

dispatch_semaphore_signal:发送一个信号,让信号总量加 1

dispatch_semaphore_wait:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。

实现线程同步,将异步执行任务转换为同步执行任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* semaphore 线程同步
*/
- (void)semaphoreSync {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block int number = 0;
dispatch_async(queue, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
}

执行顺如下:

  1. semaphore 初始创建时计数为 0。
  2. 异步执行任务 1追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait方法,semaphore 减 1,此时 semaphore == -1,当前线程进入等待状态。
  3. 然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal之后,总信号量加 1,此时 semaphore == 0,正在被阻塞的线程(主线程)恢复继续执行。
  4. 最后打印 semaphore---end,number = 100

这样就实现了线程同步,将异步执行任务转换为同步执行任务。