目录展开

线程与进程
线程和进程的定义
线程
- 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
- 进程要想执行任务,必须要有线程,进程至少要有一条线程
- 程序启动时会默认开启一条线程,这条线程被称为主线程或 UI 线程
进程
- 进程是指在系统中正在运行的一个应用程序
- 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
- 通过 “活动监视器” 可以查看 mac 系统内开启的线程 所以,可以简单理解为:进程是线程的容器,而线程用来执行任务。在 iOS 中是单进程开发,一个进程就是一个 app,进程之间是相互独立的,如支付宝、微信、qq 等,这些都是属于不同的进程
进程与线程的关系
进程与线程之间的关系主要涉及两个方面:
-
地址空间 同一个进程的线程共享本进程的地址空间
-
而进程之间则是独立的地址空间
-
资源拥有 同一个进程内线程共享本进程的资源,如内存、I/O、cpu 等
-
但是进程之间资源是独立的 两个之间的关系就相当于工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即 进程相当于一个工厂,线程相当于工厂中的一条流水线 针对进程和线程,还有以下几点说明:
-
1: 多进程要比多线程健壮 一个进程崩溃后,在保护模式下不会对其他进程产生影响
-
而一个线程崩溃整个进 程都死掉
-
2: 使用场景:频繁切换、并发操作 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。
-
同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
-
3: 执行过程 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口
-
但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
4: 线程是处理器调度的基本单位,但是进程不是。
-
5: 线程共享进程的虚拟地址空间,但每条线程有独立的栈、寄存器上下文和调度状态。
多线程
多线程原理
- 对于单核 CPU, 同一时间,CPU 只能处理一条线程,即只有一条线程在工作
- iOS 中的多线程同时执行的本质是 CPU 在多个人物之间直接进行快速的切换 ,由于 CPU 调度线程的时间足够快,就造成了 多线程同时执行的效果。其中切换到时间间隔就是时间片。
多线程意义
优点
- 能适当提高程序的执行效率
- 能适当提高资源利用率,如 CPU、内存
- GCD/Operation 管理的 worker 线程由系统管理并可能复用;不能假定任务结束后线程立即销毁。
缺点
- 开启线程需要占用一定的内存空间,默认情况下,每一个线程占用 512KB
- 如果开启大量线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信,多线程的数据共享
多线程的生命周期
新建 - 就绪 - 运行 - 阻塞 - 死亡
线程与进程
线程和进程的定义
线程
-
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
-
进程要想执行任务,必须要有线程,进程至少要有一条线程
-
程序启动时会默认开启一条线程,这条线程被称为主线程或 UI 线程 进程
-
进程是指在系统中正在运行的一个应用程序
-
每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
-
通过 “活动监视器” 可以查看 mac 系统内开启的线程 所以,可以简单理解为:进程是线程的容器,而线程用来执行任务。在 iOS 中是单进程开发,一个进程就是一个 app,进程之间是相互独立的,如支付宝、微信、qq 等,这些都是属于不同的进程 进程与线程的关系 进程与线程之间的关系主要涉及两个方面:
-
地址空间 同一个进程的线程共享本进程的地址空间
-
而进程之间则是独立的地址空间
-
资源拥有 同一个进程内线程共享本进程的资源,如内存、I/O、cpu 等
-
但是进程之间资源是独立的 两个之间的关系就相当于工厂与流水线的关系,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即 进程相当于一个工厂,线程相当于工厂中的一条流水线 针对进程和线程,还有以下几点说明:
-
1: 多进程要比多线程健壮 一个进程崩溃后,在保护模式下不会对其他进程产生影响
-
而一个线程崩溃整个进 程都死掉
-
2: 使用场景:频繁切换、并发操作 进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。
-
同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程
-
3: 执行过程 每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口
-
但是 线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
4: 线程是处理器调度的基本单位,但是进程不是。
-
5: 线程共享进程的虚拟地址空间,但每条线程有独立的栈、寄存器上下文和调度状态。
-
对于单核 CPU, 同一时间,CPU 只能处理一条线程,即只有一条线程在工作
-
iOS 中的多线程同时执行的本质是 CPU 在多个人物之间直接进行快速的切换 ,由于 CPU 调度线程的时间足够快,就造成了 多线程同时执行的效果。其中切换到时间间隔就是时间片。 多线程意义 优点
-
能适当提高程序的执行效率
-
能适当提高资源利用率,如 CPU、内存
-
GCD/Operation 管理的 worker 线程由系统管理并可能复用;不能假定任务结束后线程立即销毁。缺点
-
开启线程需要占用一定的内存空间,默认情况下,每一个线程占用 512KB
-
如果开启大量线程,会占用大量的内存空间,降低程序的性能
-
线程越多,CPU 在调用线程上的开销就越大
-
程序设计更加复杂,比如线程间的通信,多线程的数据共享
多线程的生命周期
新建 - 就绪 - 运行 - 阻塞 - 死亡

-
新建:主要是实例化线程对象
-
就绪:线程对象调用 start 方法,将线程对象加入可调度线程池,等待 CPU 的调用,即调用 start 方法,并不会立即执行,进入就绪状态,需要等待一段时间,经 CPU 调度后才执行,也就是从就绪状态进入运行状态
-
运行:CPU 负责调度可调度线城市中线程的执行,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由 CPU 负责,开发人员不能干预。
-
阻塞:线程因 sleep、同步锁等等待条件而暂时不可运行。sleep 期间处于等待 / 阻塞状态,时间到后才重新具备调度条件;以下是 NSThread 的休眠方法。
-
sleepForTimeInterval: 在给定的时间间隔内休眠线程,即指定休眠时长
-
同步锁:@synchronized (self):
-
死亡:分为两种情况 正常死亡,即线程执行完毕
-
非正常结束:
exit()终止进程,pthread_exit()结束当前 pthread;NSThread.cancel只是协作式取消请求,不会强制终止正在运行的线程。
简要说明,就是处于运行中的线程拥有一段可以执行的时间(称为时间片),
- 如果时间片用尽,线程就会进入就绪状态队列
- 如果时间片没有用尽,且需要开始等待某事件,就会进入阻塞状态队列
- 等待事件发生后,线程又会重新进入就绪状态队列
- 每当一个线程离开运行,即执行完毕或者强制退出后,会重新从就绪状态队列中选择一个线程继续执行
线程的 exit 与 cancel 说明:
exit()终止当前进程;pthread_exit()结束调用它的 pthread,二者都不是通用的 “杀线程” 接口。NSThread.cancel设置取消标志;运行中的线程需检查isCancelled并自行退出,不会被强制停止。
线程的优先级越高,是不是意味着任务的执行越快?
线程自信的快慢,除了要看优先级,还要查看资源的大小(即任务的复杂程度)、以及 CPU 调度情况。在 NSThread 中,线程优先级 threadPriority 已经被服务质量 qualityOfService 取代。
在早期 NSThread 中,我们就可以通过 threadPriority 设置线程的 “优先级”。
NSThread *thread = [[NSThread alloc] initWithBlock:^{ // 执行任务}];thread.threadPriority = 0.8; // 设置优先级,取值范围 0.0 ~ 1.0[thread start];
0.0是优先级最低,1.0优先级最高,默认值大约为0.5Apple后来引入了 服务质量 概念,用于更智能地管理线程优先级,在NSThread、NSOperation、GCD都支持这种机制,NSThread *thread = [[NSThread alloc] initWithBlock:^{ // 执行任务}];thread.qualityOfService = NSQualityOfServiceUserInitiated; // 设置服务质量[thread start];

线程池

iOS 多线程
iOS 中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation,汇总如图所示

// *********1: pthread*********pthread_t threadId = NULL;//c字符串char *cString = "HelloCode";/** pthread_create 创建线程 参数: 1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾 同时不需要 `*` 2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的) 3. 线程要执行的`函数地址` void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似 (*): 函数名 (void *): 参数类型,void * 4. 传递给第三个参数(函数)的`参数` */int result = pthread_create(&threadId, NULL, pthreadTest, cString);if (result == 0) {NSLog(@"成功");} else {NSLog(@"失败");}//*********2、NSThread*********[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];//*********3、GCD*********dispatch_async(dispatch_get_global_queue(0, 0), ^{[self threadTest];});//*********4、NSOperation*********[[[NSOperationQueue alloc] init] addOperationWithBlock:^{[self threadTest];}];- (void)threadTest{NSLog(@"begin"); NSInteger count = 1000 * 100;for (NSInteger i = 0; i < count; i++) {// 栈区 NSInteger num = i;// 常量区 NSString *name = @"zhang";// 堆区 NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];NSLog(@"%@", myName);}NSLog(@"over");}void *pthreadTest(void *para){// 接 C 语言的字符串// NSLog(@"===> %@ %s", [NSThread currentThread], para);// __bridge 将 C 语言的类型桥接到 OC 的类型 NSString *name = (__bridge NSString *)(para);NSLog(@"===>%@ %@", [NSThread currentThread], name);return NULL;}### 线程安全问题
当多个线程同时访问一块资源时,容易引发数据错乱和数据安全问题,有以下两种解决方案:
- 互斥锁- 自旋锁
#### 互斥锁
- 用于保护临界区,确保同一时间,只有一条线程能够执行- 如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象- 加了互斥锁的代码,当新线程访问时,如果发现其他线程正在执行锁定的代码,新线程就会进入休眠 针对互斥锁,还需要注意以下几点:- 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差- 能够加锁的任意 NSObject 对象- 锁对象一定要保证所有的线程都能够访问
#### 自旋锁
- 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于忙等(即原地打转,称为自旋)阻塞状态- 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用自旋锁,属性修饰符atomic,本身就有一把自旋锁- 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即不停的尝试执行代码,比较消耗性能
**【面试题】:自旋锁 vs 互斥锁**
- 同:在同一时间,保证了只有一条线程执行任务,即保证了相应同步的功能- 不同: 互斥锁:发现其他线程执行,当前线程 休眠(即就绪状态),进入等待执行,即挂起。一直等其他线程打开之后,然后唤醒执行- 自旋锁:发现其他线程执行,当前线程 一直询问(即一直访问),处于忙等状态,耗费的性能比较高
- 场景:根据任务复杂度区分,使用不同的锁,但判断不全时,更多是使用互斥锁去处理 当前的任务状态比较短小精悍时,用自旋锁- 反之的,用互斥锁
#### atomic 原子锁 & nonatomic 非原子锁
atomic 和 nonatomic主要用于属性的修饰,以下是相关的一些说明:
- atomic是原子属性,是为多线程开发准备的,是默认属性! 同一时间 单(线程)写多(线程)读的线程处理技术- Mac开发中常用
- nonatomic 是非原子属性 没有锁!性能高!- 移动端开发常用
atomic的底层在最早是通过轻量级自旋锁 OSSpinLock实现的,后来因为存在优先级反转问题被弃用了,现在内部已经改成使用os_unfair_lock
```objective-cstatic inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy){ if (offset == 0) { object_setClass(self, newValue); return; }
id oldValue; id *slot = (id*) ((char*)self + offset);
if (copy) { newValue = [newValue copyWithZone:nil]; } else if (mutableCopy) { newValue = [newValue mutableCopyWithZone:nil]; } else { if (*slot == newValue) return; newValue = objc_retain(newValue); }
if (!atomic) { oldValue = *slot; *slot = newValue; } else { // 加锁修饰不加锁修饰 spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); oldValue = *slot; *slot = newValue; slotlock.unlock(); }
objc_release(oldValue);}在代码中我们可以看出,
对于 atomic 修饰的属性,进行了 spinlock_t 加锁处理,但是在前文中提到 OSSpinLock 已经废弃了,这里的 spinlock_t 在底层是通过 os_unfair_lock 替代了 OSSpinLock 实现的加锁。同时为了防止哈希冲突,还是用了加盐操作
加盐(Salting) 就是在密码前后加上随机字符串(称为 “盐”),再进行哈希。 密码:123 盐:XyZ@9 哈希前的内容:“123XyZ@9” 哈希结果:MD5 (“123XyZ@9”) = 7b3e25c0b34a7f… 同样的密码,加上不同的盐,哈希值完全不同。
using spinlock_t = mutex_tt<LOCKDEBUG>;class mutex_tt : nocopy_t { os_unfair_lock mLock;...}getter 方法对 atomic 的处理,通 setter 是大致相同的。
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) { if (offset == 0) { return object_getClass(self); }
// Retain release world id *slot = (id*) ((char*)self + offset); if (!atomic) return *slot;
// Atomic retain release world spinlock_t& slotlock = PropertyLocks[slot]; slotlock.lock(); id value = objc_retain(*slot); slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock. return objc_autoreleaseReturnValue(value);}【面试题】atomic 与 nonatomic 的区别
-
nonatomic 非原子属性
-
非线程安全,适合内存小的移动设备
-
atomic 原子属性 (线程安全),针对多线程设计的,默认值
-
atomic 的 getter/setter 使用运行时锁同步单次属性访问;锁实现随 Runtime 版本变化,不能保证读改写或多个属性组成的操作整体线程安全。
-
线程安全,需要消耗大量的资源 iOS 开发的建议
-
所有属性都声明为 nonatomic
-
尽量避免多线程抢夺同一块资源 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力 atomic 修饰的属性绝对安全吗? atomic 只能保证我们的 setter 和 getter 方法的线程安全,并不能保证数据安全。
GCD
GCD 是异步执行任务的技术之一,是 Apple 开发的一个多核变成的较新的解决方式。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。他是在一个线程池的基础上,并行执行任务。
GCD 核心
GCD 的核心主要由 任务 + 队列 + 函数 组成
//********GCD基础写法********//创建任务dispatch_block_t block = ^{NSLog(@"hello GCD");};//创建串行队列dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", NULL);//将任务添加到队列,并指定函数执行dispatch_async(queue, block);#### 任务
任务就是执行操作的意思,换句话说就是我们在线程执行的那段代码,在GCD中是放在block的
- 同步执行 ( sync ) 同步添加任务到指定的队列中,在添加的任务执行结束前,会一直等待,直到里面的任务完成之后再继续执行 只能在当前线程中执行任务,不具备开启新线程的能力- 异步执行(async) 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行后面的任务 可以在新的线程中执行任务,具备开启新线程的能力 进入dispatch_sync函数后程序不会立刻返回,因此会阻塞线程 而进入dispatch_async函数后程序会立刻返回,不会妨碍进行下一步操作
#### 队列

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


- 串行队列:每次只有一个任务被执行,让一个任务接着一个的去执行- 并行队列:可以让多个任务并发执行,可以开启多个线程,并且同时执行任务
并发队列的并发功能只有在异步函数下才可以体现出来
```objective-cdispatch_queue_t queue = dispatch_queue_create("SerialQueue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t queue2 = dispatch_queue_create("ConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
//对于串行队列,GCD提供了一个特殊的队列:主队列 dispatch_queue_t mainQueue = dispatch_get_main_queue();//获取串行队列
//对于并发队列,GCD提供了一个全局并发队列 dispatch_queue_t uque = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

串行并行与同步异步
同步异步函数
同步函数(sync)和异步函数(async),只能决定能否开启新的线程执行任务,不能决定函数是串行执行还是并行执行
同步:
- 在当前线程中执行, 不具备开启新线程的能力
- 队列后面的内容需要等到同步函数返回后才可以继续执行
异步:
- 拥有开创新线程的能力 `, 但是不一定会开启新线程
- 后面的内容不需要等异步函数返回后执行, 后面的内容可以直接执行, 所以需要开启新线程, 因此具备开启新线程的能力 同步函数: 同步函数会阻塞当前函数的返回, 异步函数会立即执行, 执行下面的代码
同步函数后面的内容要等到同步函数返回才可以执行, 异步函数后面的内容不需要等异步函数返回才执行, 可以直接执行, 异步函数里面的内可能需要开启一个新线程来执行
串行并行队列
主要影响任务的执行方式,具体开不开开启新线程取决于上面的函数。
并行:多个任务并发执行
串行:一个任务执行完毕后才去执行下一个任务
- 如果传入的是一个并发队列,那就并发执行任务
- 如果传入一个手动创建的串行队列,那就在子队列串行执行
- 如果传入的是主队列,那就在主线程串行执行
- 如果传入的是一个主队列,那么就不会开启新线程
GCD 的基本使用
同步串行
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) { @autoreleasepool { dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_sync(serialQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"1 %d == %@", i, [NSThread currentThread]); } }); dispatch_sync(serialQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"2 %d == %@", i, [NSThread currentThread]); } }); } return EXIT_SUCCESS;}[图片]
结论 :同步串行队列没有开启新线程也没有异步执行
异步串行
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) { @autoreleasepool { dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"1 %d == %@", i, [NSThread currentThread]); } }); dispatch_async(serialQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"2 %d == %@", i, [NSThread currentThread]); } }); sleep(2); //如果没有sleep,那么程序直接退出而没有输出 } return EXIT_SUCCESS;}
** 结论:** 异步串行队列按提交顺序串行执行任务,调用者不等待;队列不绑定某一条 worker 线程,线程由系统管理和复用。
异步串行
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) { @autoreleasepool { dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_async(serialQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"1 %d == %@", i, [NSThread currentThread]); } }); dispatch_async(serialQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"2 %d == %@", i, [NSThread currentThread]); } }); sleep(2); //如果没有sleep,那么程序直接退出而没有输出 } return EXIT_SUCCESS;}[图片]
异步串行队列会按顺序执行任务,但队列不绑定某一条工作线程;系统决定线程的创建和复用,不能由此断言每个任务都会新建线程或始终在同一线程执行。
同步并行
int main(int argc, const char * argv[]) { @autoreleasepool { dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_sync(concurrentQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"1 %d == %@", i, [NSThread currentThread]); //sleep(1); } }); dispatch_sync(concurrentQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"2 %d == %@", i, [NSThread currentThread]); } }); //sleep(2); } return EXIT_SUCCESS;}[图片]
同步函数没有开辟新线程的能力,也没有体现并发的特点
异步并行
int main(int argc, const char * argv[]) { @autoreleasepool { dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"1 %d == %@", i, [NSThread currentThread]); //sleep(1); } }); dispatch_async(concurrentQueue, ^{ for (int i = 0; i < 5; i++) { NSLog(@"2 %d == %@", i, [NSThread currentThread]); } }); sleep(2); } return EXIT_SUCCESS;}[图片]
异步并发队列允许多个任务并发执行,但不保证每个任务都创建新线程,也不保证任务完成顺序。
面试问题
异步函数 + 并行队列
下面代码的输出顺序是什么
- (void)interview01{//并行队列 dispatch_queue_t queue = dispatch_queue_create("com.CJL.Queue", DISPATCH_QUEUE_CONCURRENT); NSLog(@"1");// 耗时 dispatch_async(queue, ^{ NSLog(@"2"); dispatch_async(queue, ^{ NSLog(@"3"); }); NSLog(@"4"); }); NSLog(@"5");} ----------打印结果-----------一种可能的输出(顺序不保证):1 5 2 4 3[图片]
1 一定先于异步任务提交;但主线程打印 5 与异步 block 开始执行之间没有固定先后关系。
并发队列中的外层 block 会先打印 2,再提交打印 3 的 block,随后打印 4;3 可能在 4 之前或之后执行。队列只保证各任务的提交顺序,不保证并发任务的完成顺序,也不会按任务 “复杂度” 安排先后。
代码修改
**【修改 1】:** 将并行队列改成串行队列后,队列内的打印顺序为 2 4 3;主线程打印 5 与异步 block 的开始时机仍不保证相对顺序。
**【修改 2】:** 在任务 5 之前休眠 2s 时,异步 block 通常会先完成;但这只是调度结果,不是 GCD 保证的固定顺序。
异步函数嵌套同步函数 + 并发队列

任务 1 一定先于异步任务提交;主线程上的任务 5 与异步 block 的启动或完成没有固定先后顺序,不能按任务复杂度推断输出顺序。
在异步 block 中,首先执行任务 2,再执行同步 block。同步函数会阻塞当前调用线程,因此任务 4 要等任务 3 完成;异步 block 内的顺序是任务 2、任务 3、任务 4。
[图片]
异步函数嵌套同步函数 + 串行队列

先执行任务 1;主线程执行任务 5 与串行队列中的异步 block 谁先开始取决于调度,不能用任务复杂度推断先后顺序。
在异步block中,先执行任务2,接下来是同步 block,同步函数会阻塞线程,所以执行任务 4 需要等待任务 3 执行完成,而任务 3 的执行,需要等待异步 block 执行完成,相当于任务 3 等待任务 4 完成
所以就造成了任务4等待任务3,任务3等待任务4,即互相等待的局面,就会造成死锁,这里有个重点是关键的堆栈 slow
修改 :如果删掉任务 4,执行顺序是什么?
还是会死锁,因为 任务 3 等待的是异步 block 执行完毕,而异步 block 等待任务 3
死锁
死锁死因为资源有限以及线程的交错执行导致的。
死锁产生的四个必要条件:
- 互斥访问,在有互斥访问的情况下,线程才会出现等待
- 持有并等待,线程持有一些资源,并等待一些资源
- 资源非抢占,一旦一个资源被持有,除非持有者主动放弃,否则其他竞争者都无法获取这个资源
- 循环等待:循环等待是指存在一系列线程 T0,T1,TnT0 等待 T1,T1 等待 T2,T2 等待 Tn 这样便出现了一个循环等待
同步会阻塞当前函数的返回,异步函数会立刻返回,直接执行下面的代码
因为同步函数会阻塞当前的一个函数,等待这个函数返回后才去执行任务,因此同步函数会等待函数的返回
队列上是放任务,而线程是执行队列上的任务
同步函数 + 主队列

主队列首先有 viewDidLoad 任务,而这个时候给主队列添加了一个新的任务,这个任务会等待我们的 viewDidLoad 执行完毕才会继续执行任务。但主队列绑定主线程,主线程正在执行,他必须空出来才能执行队列的 renew,但是此时主线程被 sync 挂住了,他在等待主队列任务执行完才能继续。
举个例子:
你(主线程)在厨房干活的时候(执行 main 函数)
忽然接到一个命令:
“把一道菜(block)放进你的订单列表(主队列),
然后等它做完再继续干其他事。”
于是:
・厨师(主线程)把任务(block)加进订单列表;
・然后原地等着这道菜做完;
・但厨房里只有他一个厨师(串行执行);
・可他现在在等自己;
・所以他永远不会开始做这道菜。
异步串行队列嵌套同步串行队列

(1)执行 task1
(2)执行 task5
(3)执行 task2
(4)阻塞同步线程,把 task3 加入到队列 myQueue 的队尾
(5)task4 需要等待 task3 执行完成后执行,但是此时 task3 又排在 task4 后面,所以造成了死锁
信号量阻塞主线程
dispatch_semaphore_t sem = dispatch_semaphore_create(0);dispatch_async(dispatch_get_main_queue(), ^{ dispatch_semaphore_signal(sem); NSLog(@"the sem +1");});dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);NSLog(@"the sem -1");主线程中dispatch_semaphore_wait一直等着dispatch_semaphore_signal改变信号量(+1 操作),但是 dispatch_semaphore_wait 却阻塞了主线程导致 dispatch_semaphore_signal 无法执行,从而造成了死锁。
GCD 相关方法
dispatch_set_target_queue
dispatch_queue_create 创建的是队列而不是线程;任务的 QoS 由队列属性、target queue 和系统调度决定。需要配置服务质量时应设置相应 QoS/target queue,不能概括为 “与 global 默认优先级相同的线程”。
dispatch_set_target_queue(serialQueue, globalDispatchQueueBackGround);第一个是需要变更优先级的参数,第二个参数制定了与那个优先级相同的队列

在必须将不可并行执行的处理追加到多个 Serial Dispatch Queue 中时,如果使用 dispatch set target queue 西数将目标指定为某 一个 Serial DispatchQueue,即可防止处理并行执行。
dispatch_semaphore
什么是信号量,这里引用学长的学长的一段话
简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看 门人允许其中三辆直接进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开 车拦,放入外面的一辆进去,如果又离开两辆,则又可以放入两辆,如此往复。在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用
dispatch_semaphore_t currSingal = dispatch_semaphore_create(value);// 创建信号量,如果小于0则会返回NULLdispatch_semaphore_signal(dispatch_semaphore_t _Nonnull dsema); // 发送信号量让信号量加1dispatch_semaphore_wait(dispatch_semaphore_t _Nonnull dsema, dispatch_time_t timeout); // 可以让总信号量减1,信号量小于0的时候就会一直等待,否则就可以正常执行我们要清楚需要哪一个线程等待,哪一个线程继续执行,然后使用信号量
作用 :
- 保持线程同步,将异步任务转化为同步执行任务
- 保障线程的安全,为线程加锁
并发队列里的异步任务顺序执行
dispatch_semaphore_t currentSingal = dispatch_semaphore_create(0); dispatch_queue_t que = dispatch_get_global_queue(0, 0); dispatch_async(que, ^{ NSLog(@"执行任务1, %@", [NSThread currentThread]); dispatch_semaphore_signal(currentSingal); }); dispatch_semaphore_wait(currentSingal, DISPATCH_TIME_FOREVER);
dispatch_async(que, ^{ NSLog(@"执行任务2, %@", [NSThread currentThread]); dispatch_semaphore_signal(currentSingal); }); dispatch_semaphore_wait(currentSingal, DISPATCH_TIME_FOREVER); dispatch_async(que, ^{ NSLog(@"执行任务3, %@", [NSThread currentThread]); dispatch_semaphore_signal(currentSingal); });

/** * 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);}

在这段代码里:
・主线程被 wait 阻塞;
・异步任务完成后 signal;
・主线程恢复运行;
→ 实现了异步任务的同步等待。
设置一个最大开辟的线程数 :
信号量只能限制 wait 与 signal 包围的工作区段同时进入的任务数,不会设置 GCD worker 线程池的最大线程数。阻塞 worker 的任务仍由系统调度,不能假定每个下载都单独创建一条线程。
dispatch_semaphore_t currSingal = dispatch_semaphore_create(3);// 创建信号量,如果小于0则会返回NULL dispatch_queue_t queue = dispatch_get_global_queue(0, 0); for (int i = 0; i < 10; i++) { dispatch_async(queue, ^{ dispatch_semaphore_wait(currSingal, DISPATCH_TIME_FOREVER); NSLog(@"执行任务, %d", i); sleep(1); NSLog(@"完成任务, %d", i); dispatch_semaphore_signal(currSingal); }); }
在诶里可以看出我们这里最开始的三个异步操作, 剩下的需要同步等待, 只有释放出来的操作才可以进行异步操作
这里其实出现了一个优先级反转的问题, 这里我们看一下
, 他会出现什么:
这个报错锁我们的 User-initiated 这个线程在等待交底优先级的线程. 出现了一个优先级反转的内容:
优先级反转
假设现在有 3 个任务 A、B`C’它们的优先 级为 A>B>C 任务 C 在运行时持有一把锁’然后它被高优先级的任务 A 抢占了(任 务 C 的锁没有被释放)。此时任务 A 恰巧也想申请任务 C 持有的锁’但是申请失败, 因 此进入阻塞状态等待任务 C 放锁。此时’任务 B、C 都处于可以运行的状态, 由于任务 B 的优先级高于 C, 因此 B 优先运行 ° 综合观察该情况’就会发现任务 B 好像优先级高 于任务 A’先于任务 A 执行。

这里我们来理解一下上面为什么会出现一个优先级反转的问题:
- 前三个任务线进入工作状态
- 后面 4 - 10 个任务被阻塞
- 系统会在这些阻塞的线程中,挑选一个来唤醒(注意这里并不一定考虑线程的一个优先级)
- 这时候如果某一个优先级比较低的线程被唤醒会开始执行
- 此时另一个高优先级的线程也在等待信号量, 去不许等这个低优先级的线程先完成, 这样那个自高优先级的线程就被低优先级的线程卡住了, 这就是我们这里说的优先级反转的问题
Dispatch semaphore 提供计数许可与等待,不承诺等待者按 FIFO 或特定 QoS 顺序获准;持有许可的低优先级工作也可能造成优先级反转。
所以在 iOS 中为了避免我们出现优先级反转的问题, 尽量减少采用信号量的方式
Dispatch Semaphore 线程安全和线程同步(为线程加锁)
在程序执行时多个任务可能会对同—份数据产生 竞争’因此任务会使用锁来保护共享数据 °
线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。
线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。
这里就涉及到之前了解过的售票问题,下面,我们模拟火车票售卖的方式,实现 NSThread 线程安全和解决线程同步问题。
/** * 售卖火车票(线程安全) */- (void)saleTicketSafe { while (1) { // 相当于加锁 static dispatch_semaphore_t semaphoreLock; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ semaphoreLock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
if (self.ticketSurplusCount > 0) { // 如果还有票,继续售卖 self.ticketSurplusCount--; NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", (long)self.ticketSurplusCount, [NSThread currentThread]]); [NSThread sleepForTimeInterval:0.2]; } else { // 如果已卖完,关闭售票窗口 NSLog(@"所有火车票均已售完");
// 相当于解锁 dispatch_semaphore_signal(semaphoreLock); break; }
// 相当于解锁 dispatch_semaphore_signal(semaphoreLock); }}信号量只限制被保护区段的并发数;任务仍提交到并发队列,调用者仍可异步返回,阻塞等待还可能占用 worker。
dispatch_after
表示在某队列中的 block 延迟执行
应用:在主队列延迟执行一项任务,如 viewDidLoad 后延迟 1s,提示一个 alertView(是延迟加入到队列,而不是延迟执行)
dispatch_once- (void)cjl_testOnce{ /* dispatch_once保证在App运行期间,block中的代码只执行一次 应用场景:单例、method-Swizzling */ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //创建单例、method swizzled或其他任务 NSLog(@"创建单例"); });}#### dispatch_apply
按照指定的次数将指定的任务追加到指定的队列中,快速迭代一组任务,并等待全部队列执行结束。主要用于需要快速并行处理大量相同类型任务的场景,如图像处理、数据处理或任何形式的批处理任务。
在串行队列中无法体现出它的作用,因为队列中的任务始终是一次执行一个。
在并发队列中如果开启了多个线程的话就能在多个线程中同时展开执行,效率特别高。
```objective-c- (void)cjl_testApply{ /* dispatch_apply将指定的Block重复提交到指定队列,并等待全部迭代结束;并发迭代访问共享可变数据时仍需自行同步。
应用场景:用来拉取网络数据后提前算出各个控件的大小,防止绘制时计算,提高表单滑动流畅性 - 添加到串行队列中——按序执行 - 添加到主队列中——死锁 - 添加到并发队列中——乱序执行 - 添加到全局队列中——乱序执行 */
dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_SERIAL); NSLog(@"dispatch_apply前"); /** param1:重复次数 param2:追加的队列 param3:执行任务 */ dispatch_apply(10, queue, ^(size_t index) { NSLog(@"dispatch_apply 的线程 %zu - %@", index, [NSThread currentThread]); }); NSLog(@"dispatch_apply后");}#### dispatch_group_t
notify的作用就是:**当 group 中所有任务完成(enter/leave 配对完或者 dispatch_group_async 内 block 执行完)时,执行 block。**
##### dispatch_group_async + dispatch_group_notify
```objective-c- (void)cjl_testGroup1{ /* dispatch_group_t:调度组将任务分组执行,能监听任务组完成,并设置等待时间
应用场景:多个接口请求之后刷新页面 */
dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{ NSLog(@"请求一完成"); });
dispatch_group_async(group, queue, ^{ NSLog(@"请求二完成"); });
// 当group内所有任务完成时,这个block会在主线程执行。很适合UI刷新,不阻塞主线程
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"刷新页面"); });}##### dispatch_group_enter + dispatch_group_leave + dispatch_group_notify
```objective-c- (void)cjl_testGroup2{ /* dispatch_group_enter和dispatch_group_leave成对出现,使进出组的逻辑更加清晰 */ dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@"请求一完成"); dispatch_group_leave(group); });
dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@"请求二完成"); dispatch_group_leave(group); });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"刷新界面"); });}- dispatch_group_enter (group):标记 “组里有一个任务开始了”。
- dispatch_group_leave (group):标记 “组里的一个任务完成了”。
- **enter/leave 必须配对出现,** 否则组永远不会完成。
可以理解为给 group 维护了一个计数器:每次 enter +1,每次 leave -1,当计数器归零时,group 完成。
dispatch_wait

wait 会阻塞线程,notify 不会阻塞 是异步等待
- (void)cjl_testGroup3{ /* long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout)
group:需要等待的调度组 timeout:等待的超时时间(即等多久) - 设置为DISPATCH_TIME_NOW意味着不等待直接判定调度组是否执行完毕 - 设置为DISPATCH_TIME_FOREVER则会阻塞当前调用线程,直到调度组执行完毕
返回值:为long类型 - 返回值为0——在指定时间内调度组完成了任务 - 返回值不为0——在指定时间内调度组没有按时完成任务
*/ dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@"请求一完成"); dispatch_group_leave(group); });
dispatch_group_enter(group); dispatch_async(queue, ^{ NSLog(@"请求二完成"); dispatch_group_leave(group); });
// long timeout = dispatch_group_wait(group, DISPATCH_TIME_NOW);// long timeout = dispatch_group_wait(group, DISPATCH_TIME_FOREVER); long timeout = dispatch_group_wait(group, dispatch_time(DISPATCH_TIME_NOW, 1 *NSEC_PER_SEC)); NSLog(@"timeout = %ld", timeout); if (timeout == 0) { NSLog(@"按时完成任务"); }else{ NSLog(@"超时"); }
dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"刷新界面"); });}#### dispatch_barrier_sync & dispatch_barrier_async
dispatch_barrier_async 是 GCD(Grand Central Dispatch)中的一个方法,它用于在并发队列中插入一个“栅栏”(barrier)。栅栏的作用是在其前面的任务执行完毕后,再执行栅栏任务,然后再执行栅栏之后的任务。这可以用于确保在多线程环境中某些操作的顺序性和互斥性。
栅栏函数用于自己创建的并发队列;串行队列本身已保证串行执行,使用 barrier 不会提供额外的栅栏效果。
```objective-c- (void)cjl_testBarrier{ /* dispatch_barrier_sync & dispatch_barrier_async
应用场景:同步锁
等栅栏前追加到队列中的任务执行完毕后,再将栅栏后的任务追加到队列中。 简而言之,就是先执行栅栏前任务,再执行栅栏任务,最后执行栅栏后任务
- dispatch_barrier_async:前面的任务执行完毕才会来到这里 - dispatch_barrier_sync:作用相同,但是这个会堵塞线程,影响后面的任务执行
- dispatch_barrier_async可以控制队列中任务的执行顺序, - 而dispatch_barrier_sync不仅阻塞了队列的执行,也阻塞了线程的执行(尽量少用) */
[self cjl_testBarrier1]; [self cjl_testBarrier2];}- (void)cjl_testBarrier1{ //串行队列使用栅栏函数
dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_SERIAL);
NSLog(@"开始 - %@", [NSThread currentThread]); dispatch_async(queue, ^{ sleep(2); NSLog(@"延迟2s的任务1 - %@", [NSThread currentThread]); }); NSLog(@"第一次结束 - %@", [NSThread currentThread]);
//栅栏函数的作用是将队列中的任务进行分组,所以我们只要关注任务1、任务2 dispatch_barrier_async(queue, ^{ NSLog(@"------------栅栏任务------------%@", [NSThread currentThread]); }); NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
dispatch_async(queue, ^{ sleep(2); NSLog(@"延迟2s的任务2 - %@", [NSThread currentThread]); }); NSLog(@"第二次结束 - %@", [NSThread currentThread]);}- (void)cjl_testBarrier2{ //并发队列使用栅栏函数
dispatch_queue_t queue = dispatch_queue_create("CJL", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"开始 - %@", [NSThread currentThread]); dispatch_async(queue, ^{ sleep(2); NSLog(@"延迟2s的任务1 - %@", [NSThread currentThread]); }); NSLog(@"第一次结束 - %@", [NSThread currentThread]);
//由于并发队列异步执行任务是乱序执行完毕的,所以使用栅栏函数可以很好的控制队列内任务执行的顺序 dispatch_barrier_async(queue, ^{ NSLog(@"------------栅栏任务------------%@", [NSThread currentThread]); }); NSLog(@"栅栏结束 - %@", [NSThread currentThread]);
dispatch_async(queue, ^{ sleep(2); NSLog(@"延迟2s的任务2 - %@", [NSThread currentThread]); }); NSLog(@"第二次结束 - %@", [NSThread currentThread]);}栅栏函数最典型的用途就是实现读写安全:
dispatch_queue_t rwQueue = dispatch_queue_create("com.example.rwQueue", DISPATCH_QUEUE_CONCURRENT);NSMutableArray *array = [NSMutableArray array];
// 写操作:用 barrier 独占执行dispatch_barrier_async(rwQueue, ^{ [array addObject:@"数据"]; NSLog(@"写入完成");});
// 读操作:可以并发执行dispatch_async(rwQueue, ^{ NSLog(@"读取数据1:%@", array);});dispatch_async(rwQueue, ^{ NSLog(@"读取数据2:%@", array);});读操作可以并发,提高性能。写操作被 barrier 独占,保证数据安全
dispatch_source_t
暂时无法在飞书文档外展示此内容
GCD 中最底层、最强大的系统事件处理机制之一,被称为 “事件源 (Dispatch Source)”。
它可以用来监听 系统底层事件(如文件变化、计时器、进程、信号、网络、I/O 等)。
Dispatch Source Timer 不依赖 RunLoop,但不是实时定时器;系统负载和 leeway 等因素可能导致回调晚于设定时间。
- (void)cjl_testSource{ /* dispatch_source
应用场景:GCDTimer 在iOS开发中一般使用NSTimer来处理定时逻辑,但NSTimer依赖RunLoop,在运行模式变化或RunLoop被阻塞时,回调可能推迟。GCD定时器不依赖RunLoop,但仍受系统调度和leeway影响,不能保证准时触发。
dispatch_source是一种基本的数据类型,可以用来监听一些底层的系统事件 - Timer Dispatch Source:定时器事件源,用来生成周期性的通知或回调 - Signal Dispatch Source:监听信号事件源,当有UNIX信号发生时会通知 - Descriptor Dispatch Source:监听文件或socket事件源,当文件或socket数据发生变化时会通知 - Process Dispatch Source:监听进程事件源,与进程相关的事件通知 - Mach port Dispatch Source:监听Mach端口事件源 - Custom Dispatch Source:监听自定义事件源
主要使用的API: - dispatch_source_create: 创建事件源 - dispatch_source_set_event_handler: 设置数据源回调 - dispatch_source_merge_data: 设置事件源数据 - dispatch_source_get_data: 获取事件源数据 - dispatch_resume: 继续 - dispatch_suspend: 挂起 - dispatch_cancle: 取消 */
//1.创建队列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); //2.创建timer dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); //3.设置timer首次执行时间,间隔,精确度 dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0*NSEC_PER_SEC, 0.1*NSEC_PER_SEC); //4.设置timer事件回调 dispatch_source_set_event_handler(timer, ^{ NSLog(@"GCDTimer"); }); //5.默认是挂起状态,需要手动激活 dispatch_resume(timer);
}#### disoatch_suspend & dispatch_resume
`dispatch_suspend` / `dispatch_resume` 用于暂停和恢复应用拥有的队列或 source;挂起与恢复必须配对。不要对系统提供的 global queue 使用它们。
```objective-cdispatch_queue_t queue = dispatch_queue_create("com.example.suspendable", DISPATCH_QUEUE_SERIAL);dispatch_suspend(queue);dispatch_resume(queue);GCD 任务能取消吗?
GCD 提供 dispatch_block_cancel,可取消尚未开始执行的 dispatch block;已经开始或正在执行的任务需要在 block 内协作检查取消状态。
对于需要在任务中途停止的工作,可以使用共享的取消标志,并在适当位置检查;取消一个 Dispatch Source 只会取消该 source,不会取消任意 GCD block。
如何使用 GCD 实现一个常驻线程?
- 使用无限循环 在一个线程中使用无限循环(如 while 循环)来持续执行任务或监听事件。为了防止线程占用过多的 CPU 资源,可以在循环中加入适当的延时。
- 使用 dispatch_source_t dispatch_source_t 是 GCD 中一种可以用于监听事件的对象,比如定时器、文件描述符等。可以创建一个 dispatch_source_t 定时器,让它定期触发并执行任务。
- 使用 NSThread 使用 NSThread 创建线程并在其中执行无限循环或监听事件。
怎样利用 GCD 实现多读单写呢(怎么实现一个多读单写的模型)?
通过栅栏异步调用的方式,分配写操作到并发队列当中。
原文发布于 CSDN:【iOS】多线程与 GCD