关于GCD和队列的简单介绍请看:【iOS】GCD学习
本篇主要介绍GCD中的方法。
我们有时候需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作,当然操作组里也可以包含一个或者多个任务
这就需要用到dispatch_barrier_async
方法在两个操作组间形成栅栏
dispatch_barrier_async
方法会等待前边追加到并发队列中的任务全部执行完毕后,再将制定的任务追加到该异步队列中。然后在dispatch_barrier_async
方法追加的任务执行完毕之后,接着追加任务到该异步队列并开始执行,大佬博客中的示意图非常的形象,具体图示如下:
栅栏方法的代码使用样例如下:
- (void) barrier {
dispatch_queue_t queue = dispatch_queue_create("net.testQueue", 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]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
});
}
在执行完栅栏前面的操作之后执行栅栏操作,最后再执行栅栏后面的操作。
我们之前考虑异步栅栏+单一队列的时候栅栏只作用于同一队列
那么对于身处不同队列的任务又有什么样的拦截作用呢?
对于重要的栅栏方法部分,我们将各种情况都实验一下:
(由于异步执行+串行队列本身就是在创建的唯一一个新线程里按任务添加顺序排队执行,所以其实在这种情况下添加栅栏是没有意义的)
- (void) asyncBarrierAndOneSerial {
dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_SERIAL);
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]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
});
}
(该种情况上方已经讲述过了)
- (void)syncBarrierAndOneSerial {
dispatch_queue_t queue = dispatch_queue_create("net.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
// 追加任务1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1--%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2--%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_sync(queue, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2--%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3--%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4--%@", [NSThread currentThread]); // 打印当前线程
});
}
我们可以看到在串行队列中无论是同步执行还是异步执行,都是排好队一个一个按顺序来执行的。
- (void)syncBarrierAndOneConcurrent {
dispatch_queue_t queue = dispatch_queue_create("net.testQuquq", 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_sync(queue, ^{
// 追加barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier--%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3--%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue, ^{
// 追加任务4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4--%@", [NSThread currentThread]); // 打印当前线程
});
}
运行结果:
实际的运行结果是栅栏前的任务组(也就是任务1和任务2),在程序开始执行两秒之后同时打印了结果,接着两秒的时间单独执行了栅栏中的方法,最后两秒时间同时执行了栅栏后的任务组(也就是任务3和任务4),而且由于栅栏前后的任务组中的任务都是在并行队列中异步执行,所以执行结束的顺序是不确定的。
- (void)asyncBarrierAndSerials {
dispatch_queue_t queue1 = dispatch_queue_create("net.testQueue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("net.testQueue2", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue3 = dispatch_queue_create("net.testQueue3", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue4 = dispatch_queue_create("net.testQueue4", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue5 = dispatch_queue_create("net.testQueue5", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{
// 追加任务1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue2, ^{
// 追加任务2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_async(queue3, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue4, ^{
// 追加任务4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@", [NSThread currentThread]); // 打印当前线程
});
dispatch_async(queue5, ^{
// 追加任务5
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"5---%@", [NSThread currentThread]); // 打印当前线程
});
}
异步栅栏+多个串行队列的情况下每个任务都是几乎同时执行的,五个任务执行的结束时间都是完全随机的,此时的栅栏也就失去了该有的意义。
异步栅栏+多个串行队列情况的各任务执行结束时间都是完全随机的,所以异步栅栏+多个并行队列更是可想而知,肯定也是完全随机的。
- (void) asyncBarrierAndConcurrents {
dispatch_queue_t queueFirst = dispatch_queue_create("net.testQueueFirst", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueSecond = dispatch_queue_create("net.testQueueSecond", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueThird = dispatch_queue_create("net.testQueueThird", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueFourth = dispatch_queue_create("net.testQueueFourth", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueFifth = dispatch_queue_create("net.testQueueFifth", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queueFirst, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queueSecond, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_async(queueThird, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queueFourth, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queueFifth, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
});
}
- (void) syncBarrierAndSerials {
dispatch_queue_t queueFirst = dispatch_queue_create("net.testQueueFirst", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueSecond = dispatch_queue_create("net.testQueueSecond", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueThird = dispatch_queue_create("net.testQueueThird", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueFourth = dispatch_queue_create("net.testQueueFourth", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queueFifth = dispatch_queue_create("net.testQueueFifth", DISPATCH_QUEUE_SERIAL);
dispatch_async(queueFirst, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queueSecond, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_sync(queueThird, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queueFourth, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queueFifth, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
});
}
这种情况下的栅栏和任务1还有任务2是几乎同时执行同时先出结果的(而且每次栅栏都是第一个出结果),但是由于同步的栅栏占用了主线程,就导致栅栏后的任务3和任务4只能等到栅栏中的任务执行完成之后再开始去执行。
- (void) syncBarrierAndConcurrents {
dispatch_queue_t queueFirst = dispatch_queue_create("net.testQueueFirst", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueSecond = dispatch_queue_create("net.testQueueSecond", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueThird = dispatch_queue_create("net.testQueueThird", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueFourth = dispatch_queue_create("net.testQueueFourth", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queueFifth = dispatch_queue_create("net.testQueueFifth", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queueFirst, ^{
// 追加任务 1
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"1---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queueSecond, ^{
// 追加任务 2
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"2---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_barrier_sync(queueThird, ^{
// 追加任务 barrier
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"barrier---%@",[NSThread currentThread]);// 打印当前线程
});
dispatch_async(queueFourth, ^{
// 追加任务 3
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"3---%@",[NSThread currentThread]); // 打印当前线程
});
dispatch_async(queueFifth, ^{
// 追加任务 4
[NSThread sleepForTimeInterval:2]; // 模拟耗时操作
NSLog(@"4---%@",[NSThread currentThread]); // 打印当前线程
});
}
实际运行过程中,任务1、任务2和栅栏都是同时先开始去执行的,而且三者执行结束的时间是不确定的,然而由于栅栏占用了主线程的原因,任务3和任务4只有等到栅栏执行完成之后才开始执行。
我们经常会遇到这样的需求:在指定时间(例如 3 秒)之后执行某个任务。这种情况就可以用 GCD
的dispatch_after
方法来实现。
需要注意的是:dispatch_after
方法并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,dispatch_after
方法是很有效的。
- (void)after {
NSLog(@"currentThread---%@", [NSThread currentThread]); // 打印当前线程
NSLog(@"asyncMain---begin");
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// NSEC_PER_SEC是一个宏定义,通常用于表示一秒钟所包含的纳秒数。
// 2.0 秒后异步追加任务代码到主队列,并开始执行
NSLog(@"after---%@", [NSThread currentThread]); // 打印当前线程
NSLog(@"asyncMain---willEnd");
});
}
具体的运行情况是:先打印了asyncMain---begin
,接着过了两秒后紧接着按顺序打印了after---<_NSMainThread: 0x60000110c900>{number = 1, name = main}
和asyncMain---willEnd
。
我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once
方法。使用 dispatch_once
方法能保证某段代码在程序运行过程中只被执行 1 次,并且即使在多线程的环境下,dispatch_once
也可以保证线程安全。
/**
* 一次性代码(只执行一次)dispatch_once
*/
- (void)once {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行 1 次的代码(这里面默认是线程安全的)
});
}
通常我们会用 for
循环遍历,但是 GCD
给我们提供了快速迭代的方法dispatch_apply
。dispatch_apply
按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
如果是在串行队列中使用 dispatch_apply
,那么就和 for
循环一样,按顺序同步执行。但是这样就体现不出快速迭代的意义了。
我们可以利用并发队列进行异步执行。比如说遍历 0~5
这 6
个数字,for
循环的做法是每次取出一个元素,逐个遍历。dispatch_apply
可以在多个线程中同时(异步)遍历多个数字。
还有一点,无论是在串行队列,还是并发队列中,dispatch_apply
都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait
方法
- (void)apply {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
NSLog(@"apply---begin");
dispatch_apply(6, queue, ^(size_t iteration) {
NSLog(@"%zd---%@", iteration, [NSThread currentThread]);
});
NSLog(@"apply---end");
}
因为是在并发队列中异步执行任务,所以各个任务的执行时间长短不定,最后结束顺序也不定。但是 apply—end 一定在最后执行。这是因为 dispatch_apply 方法会等待全部任务执行完毕。
有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。
调用队列组的 dispatch_group_async
先把任务放到队列中,然后将队列放入队列组中。或者使用队列组的 dispatch_group_enter
、dispatch_group_leave
组合来实现 dispatch_group_async
调用队列组的 dispatch_group_notify
回到指定线程执行任务。或者使用 dispatch_group_wait
回到当前线程继续向下执行(会阻塞当前线程)。
监听 group
中任务的完成状态,当所有的任务都执行完成后,追加任务到 group
中,并执行任务:
- (void)group {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
//dispatch_group_notify会等到group中的处理全部结束时再开始执行
//在group中的处理全部结束时,将第三个参数(block)追加到第二个参数所对应的queue中
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"done");
});
}
由于添加到group中的队列在多线程并发时的执行结果时间是不确定的,所以打印的顺序都是随机的(理论如此,不过任务的执行顺序可能受到提交的先后顺序的影响,尤其是当多个任务都被提交到同一个队列时。)。
另外我们也可以使用dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
第二个参数为dispatch_time_t
类型,可以自定义来等待group
中的处理全部结束
dispatch_group_wait
用于暂停当前线程(阻塞当前线程),等待指定的 group
中的任务执行完成后,才会往下继续执行。
如果我们不去添加dispatch_group_wai
t来进行等待,那么由于group
中的处理本身也是异步的,所以就会在group
中的处理还没有执行完时就去执行其他的任务,例子如下:
- (void)groupWait {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
NSLog(@"YES!!");
// dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
}
结果:
可以看到打印YES!!操作在group中的处理还没有执行完时就已经执行了。
而像下面这样:
- (void)groupWait {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{
NSLog(@"blk0");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk1");
});
dispatch_group_async(group, queue, ^{
NSLog(@"blk2");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"YES!!");
}
可以看到就是在group
中全部的处理执行完之后再执行的打印YES!!
操作
从 dispatch_group_wait
相关代码运行输出结果可以看出: 当所有任务执行完成之后,才执行 dispatch_group_wait
之后的操作。但是,使用dispatch_group_wait
会阻塞当前线程!
dispatch_group_enter
标志着一个任务追加到 group
,执行一次,相当于 group
中未执行完毕任务数 +1
dispatch_group_leave
标志着一个任务离开了 group
,执行一次,相当于 group
中未执行完毕任务数 -1
当 group
中未执行完毕任务数为0 的时候,才会使 dispatch_group_wait
解除阻塞,以及执行追加到 dispatch_group_notify
中的任务
接着我们来看一下通过 dispatch_group_enter
和 dispatch_group_leave
配和来实现向group
添加操作:
- (void)groupWithEnterAndLeave {
// 首先 需要创建一个线程组
dispatch_group_t group = dispatch_group_create();
// 任务1
dispatch_group_enter(group);
void (^blockFirst)(int) = ^(int a){
NSLog(@"任务%d完成!", a);
dispatch_group_leave(group);
};
blockFirst(1);
// 任务2
dispatch_group_enter(group);
void (^blockSecond)(int) = ^(int a){
NSLog(@"任务%d完成!", a);
dispatch_group_leave(group);
};
blockSecond(2);
// 全部完成
dispatch_group_notify(group, dispatch_get_main_queue(), ^(){
NSLog(@"全部完成");
});
}
我们可以看到:任务1和任务2执行完成之后,才会执行全部完成中的任务。
从 dispatch_group_enter
、dispatch_group_leave
相关代码运行结果中可以看出:当所有任务执行完成之后,才执行 dispatch_group_notify
中的任务。这里的dispatch_group_enter、dispatch_group_leave
组合,其实等同于dispatch_group_async
。
不过使用dispatch_group_enter
和 dispatch_group_leave
需要成对出现。
如果 dispatch_group_leave
的调用次数多于 dispatch_group_enter
的调用次数,程序会 crash
。
GCD
中的信号量是指 Dispatch Semaphore
,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore
中,使用计数来完成这个功能,计数小于 0 时需要等待,不可通过。计数为 0 或大于 0 时,不用等待可通过。计数大于 0 且计数减 1 时不用等待,可通过。 Dispatch Semaphore
提供了三个方法:
dispatch_semaphore_create
:创建一个 Semaphore
并初始化信号的总量。dispatch_semaphore_signal
:发送一个信号,让信号总量加 1
。dispatch_semaphore_wait
:可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。注意: 信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。
Dispatch Semaphore
在实际开发中主要用于:
Dispatch Semaphore
线程同步
我们在开发中,会遇到这样的需求:异步执行耗时任务,并使用异步执行的结果进行一些额外的操作。换句话说,相当于,将将异步执行任务转换为同步执行任务。
下面我们就来利用Dispatch 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 = %d", number);
}
可以看到semaphore---end
是在执行完 number = 100;
之后才打印的。而且输出结果 number
为 100。整个的执行顺序如下:
semaphore
初始创建时计数为 0
异步执行 将 任务 1 追加到队列之后,不做等待,接着执行 dispatch_semaphore_wait
方法,semaphore
减 1,此时 semaphore == -1
,当前线程进入等待状态(后面的内容不执行,只执行我们所添加的任务1,等到dispatch_semaphore_signal
操作使信号量计数``>=0时线程才会恢复正常运作)
然后,异步任务 1 开始执行。任务 1 执行到 dispatch_semaphore_signal
之后,总信号量加 1,此时 semaphore == 0
,正在被阻塞的线程(主线程)恢复继续执行
最后打印 semaphore---end,number = 100
这样就实现了线程同步,将异步执行任务转换为同步执行任务。
线程安全: 如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
线程同步: 可理解为线程 A 和 线程 B 一块配合,线程 A 执行到一定程度时要依靠线程B 的某个结果,于是停下来,示意 线程B 运行;线程B 依言执行,再将结果给 线程A;线程A 再继续操作。
举个简单例子就是:两个人在一起聊天。两个人不能同时说话,避免听不清(操作冲突)。等一个人说完(一个线程结束操作),另一个再说(另一个线程再开始操作)。
下面,我们模拟火车票售卖的方式,实现 NSThread 线程安全和解决线程同步问题(例子借鉴自:大佬博客)。
场景: 总共有 50 张火车票,有两个售卖火车票的窗口,一个是北京火车票售卖窗口,另一个是上海火车票售卖窗口。两个窗口同时售卖火车票,卖完为止。
先来看看不考虑线程安全的代码:
@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount;
@end
/**
* 非线程安全:不使用 semaphore
* 初始化火车票数量、卖票窗口(非线程安全)、并开始卖票
*/
- (void)initTicketStatusNotSafe {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketNotSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketNotSafe];
});
}
/**
* 售卖火车票(非线程安全)
*/
- (void)saleTicketNotSafe {
while (1) {
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
break;
}
}
}
结果:
可以看到在不考虑线程安全,不使用 semaphore
的情况下,得到票数是错乱的,而且同一张票可能会发生卖两遍的情况,这样显然不符合我们的需求,所以我们需要考虑线程安全问题。
考虑线程安全的代码:
@interface ViewController ()
@property (nonatomic, assign) NSInteger ticketSurplusCount;
@end
//创建一个全局信号量
dispatch_semaphore_t semaphoreLock;
/**
* 线程安全:使用 semaphore 加锁
* 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
*/
- (void)initTicketStatusSafe {
NSLog(@"currentThread---%@",[NSThread currentThread]); // 打印当前线程
NSLog(@"semaphore---begin");
semaphoreLock = dispatch_semaphore_create(1);
self.ticketSurplusCount = 50;
// queue1 代表北京火车票售卖窗口
dispatch_queue_t queue1 = dispatch_queue_create("net.bujige.testQueue1", DISPATCH_QUEUE_SERIAL);
// queue2 代表上海火车票售卖窗口
dispatch_queue_t queue2 = dispatch_queue_create("net.bujige.testQueue2", DISPATCH_QUEUE_SERIAL);
__weak typeof(self) weakSelf = self;
dispatch_async(queue1, ^{
[weakSelf saleTicketSafe];
});
dispatch_async(queue2, ^{
[weakSelf saleTicketSafe];
});
}
/**
* 售卖火车票(线程安全)
*/
- (void)saleTicketSafe {
while (1) {
// 相当于加锁
dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖
self.ticketSurplusCount--;
NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
[NSThread sleepForTimeInterval:0.2];
} else { // 如果已卖完,关闭售票窗口
NSLog(@"所有火车票均已售完");
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
break;
}
// 相当于解锁
dispatch_semaphore_signal(semaphoreLock);
}
}
结果:
思路: 此处我们采用了dispatch_semaphore
机制,买票的操作是每次异步执行的,但是如果第一张票还没卖出去第二张票已经开始卖了的话就会由于dispatch_semaphore_wait
操作使得信号量计数=-1,线程就会进入等待状态,等待第一张票卖完之后的dispatch_semaphore_signal
操作,这个操作会让信号量的计数=1,使得线程重写开始正常运行,开始正常执行卖第二张票的处理,以此类推,通过保护每一次的卖票从而实现整个售票流程的正确性。
可以看出,在考虑了线程安全的情况下,使用 dispatch_semaphore
机制之后,得到的票数是正确的,没有出现混乱的情况。我们也就解决了多个线程同步的问题。