iOS当中关于并发操作的实现方式有三种:
- NSThread
- GCD
- Operation
NSThread
NSThread 是 Objective-C 对pthread的封装。它可以直接操作线程对象,最大限度的掌控线程的生命周期。它足够轻量级,但是需要手动管理线程的生命周期,包括创建、启动、暂停、取消等。
另外使用NSThread操作多线程的数据同步时,为了保证数据安全需要手动加锁,这不仅会占用更多的系统资源,在较为复杂的环境中对开发人员也是一项不小的挑战。
NSThread的使用场景比较少,但是它提供了几个我们会经常用到的方法,比如:
- 获取当前线程
NSThread *currentThread = [NSThread currentThread];
- 使当前线程休眠(暂停)
[NSThread sleepForTimeInterval:200];
NSThread提供了多个创建线程的方式,比如:- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument
、+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument
;
值得注意的是,NSThread有一种隐式创建线程的方式performSelectorInBackground:withObject:
,它会隐式地创建一个线程,如:
[self performSelectorInBackground:@selector(testThread) withObject:nil];
这条线程会调用testThread
方法
- (void)testThread {
NSLog(@"%@",[NSThread currentThread]);
}
我们在testThread
打印当前执行的线程可以看到控制台输出如下信息:
<NSThread: 0x6000000ab900>{number = 4, name = (null)
主线程 number = 1, 此处线程 number = 4,可见不是主线程,而是另外创建了新的线程。
GCD
GCD (Grand Central Dispatch)是基于队列的并发编程API,只支持FIFO(先入先出)队列;GCD队列内部使用的就是线程。它可以充分的利用当下CPU的多核特性,并且自动对线程进行管理,开发者只需要定义每个队列需要执行的工作即可。GCD可以高效处理大量并发数据,执行操作的block都能以极快的速度运行。
GCD是基于iOS和macOS的XNU内核实现的,它底层用来处理事务的Dispatch Source对资源的占用很少。GCD是纯C语言的API,不属于Cocoa框架,API都在libdispatch库中。GCD的底层除了用来处理事件(比如线程间通信)的Dispatch Source
外,还有一个用来管理block操作的Dispatch Queue
,Dispatch Queue
通过结构体和链表,被实现为FIFO队列。FIFO队列管理则是通过dispatch_async
函数所追加的block。 相对于Dispatch Source
来说,我们平时的使用中对Dispatch Queue
的操作更多。
GCD中的常用方法
-
dispatch_async
: 线程的异步操作。异步操作可以让我们在不阻塞线程的情况下充分利用不同线程和队列来处理任务。 -
dispatch_after
: 用于线程的延时操作 -
dispatch_once
: 确保单例的线程安全。表示所修饰的区域只会被访问一次,这样在多线程情况下,类也只会被初始化一次,确保了Objective-C中单例的原子化。 -
dispatch_group
: 一般用于多个任务同步。一般用法是当多个任务关联到同一个group后,所有的任务在执行完成后在执行一个统一的后续工作。注意:dispatch_group_wait
是一个同步操作,会阻塞线程。
GCD 中全局(global
)队列中的优先级
-
background
:用来处理特别耗时的后台操作,例如同步、备份数据 -
utility
:用来处理需要一些时间而又不需要立刻返回结果的操作,特别适用于异步操作,例如下载、导入数据等 -
default
:默认优先级。一般来说开发者应该指定优先级。default
属于特殊情况。 -
user-Initiated
:用来处理用户出发的需要立刻返回结果的操作,比如打开用户点击的文件 -
user-Interactive
:用来处理与用户交互的操作。一般在主线程,如果不及时响应,则有可能阻塞主线程的操作 -
unspecified
:未确定优先级,有系统根据环境判断。比如使用过时的 API 不支持优先级时,就可以设定为未确定优先级。unspecified
属于特殊情况。
NSOperation
NSOperation
是 iOS 中的又一种实现并发编程的方式。它将单个任务算做一个NSOperation
,然后放在 NSOperationQueue
队列中进行管理和运行。它是GCD的封装,执行效率上要低于GCD,不过它也有很多值得重视的优势:
- GCD只支持FIFO队列,NSOperationQueue 可以很方便地调整执行顺序,设置最大并发数量
- NSOperationQueue 可以轻松地在 operation 间设置依赖关系
- 可以很方便的使用KVO监测 operation 的执行状态(正在执行/执行完成/执行取消)
NSOperation的使用
NSOperation
指代一系列工作或者任务。它本身是一个抽象类,一般使用它的子类NSBlockOperation和NSOperationQueue来完成自定义的工作(当然也可以通过继承它来自定义operation)。每个单独的 NSOperation
有四种状态:准备就绪(Ready
)、执行中(Executing
)、暂停(Cancelled
)、完成(Finished
)。
NSBlockOperation
系统定义的一个 Operation
的子类。它本身在默认权限的全局队列上运行,负责并执行多个任务。其中任务之间互不依赖,同时 BlockOperation
也可以像 dispatch_group
一样同步管理多个任务。
NSOperationQueue
负责安排和运行多个 Operation
的队列。但是它并不局限于先进先出的队列操作。它提供了多个接口可以实现暂停、继续、终止、优先顺序、依赖等复杂操作。同时,还可以通过设置 maxConcurrentOperationCount
属性来区分其是串行的还是并行的。
线程安全问题
并发编程当中,由于涉及到多线程的读写操作,当具有多个线程的时候很容易就会出现问题
竞态条件
竞态条件是指两个或两个以上线程对共享数据进行读写操作时,最终的数据结果不确定的情况,就好像同一时间多个程序员修改同一个项目当中的同一个文件,那么很容易就会引发代码冲突一样。
对于多线程操作的数据读写问题的处理一般有三种方式:
- 使用串行队列,无论读操作还是写操作,同时只能有一个进行,这样就保证了队列的安全。但是缺点也很明显:速度慢。尤其是在有大量读/写操作的时候,每次只能进行单个读操作或写操作,效率很低。
- 使用并行队列配合异步操作完成。异步操作由于会直接返回结果,所以必须配合逃逸闭包来保证后续操作的合法性。
- 使用并行队列,在进行读操作时,用
sync
直接返回结果;在进行写操作时,用barrier flag
来保证此时并行队列只进行当前的写操作(类似于将并行队列暂时转换为串行队列),而无视其他操作。
优先倒置
指低优先级的任务由于各种原因先于高优先级的任务执行。
死锁问题
指两个或两个以上线程,它们之间互相等待彼此停止执行,以获得某个资源,但是没有一方会提前退出的情况。
iOS开发当中的死锁案例:
- 两个Operation相互依赖
NSOperation *operationA = [[NSOperation alloc] init];
NSOperation *operationB = [[NSOperation alloc] init];
[operationA addDependency:operationB];
[operationB addDependency:operationA];
- 在同一个串行队列中进行异步、同步嵌套
dispatch_async(queue, ^{
dispatch_sync(queue, ^{
});
});
参考资源
《iOS面试之道》
《Objective-C 高级编程iOS 与OS X多线程和内存管理》
网友评论