1. 进程、线程、任务
进程(process),指的是一个正在运行中的可执行文件。每一个进程都拥有独立的虚拟内存空间和系统资源,包括端口权限等,且至少包含一个主线程和任意数量的辅助线程。另外,当一个进程的主线程退出时,这个进程就结束了;
线程(thread),指的是一个独立的代码执行路径,也就是说线程是代码执行路径的最小分支。在 iOS 中,线程的底层实现是基于 POSIX threads API 的,也就是我们常说的 pthreads ;
任务(task),指的是我们需要执行的工作,是一个抽象的概念,用通俗的话说,就是一段代码。
串行 vs. 并发
从本质上来说,串行和并发的主要区别在于允许同时执行的任务数量。串行,指的是一次只能执行一个任务,必须等一个任务执行完成后才能执行下一个任务;并发,则指的是允许多个任务同时执行。
同步 vs. 异步
同样的,同步和异步操作的主要区别在于是否等待操作执行完成,亦即是否阻塞当前任务。
同步操作会阻塞当前任务,等待操作执行完成后再继续执行接下来的代码,而异步操作则恰好相反,它会在调用后立即返回,不会等待操作的执行结果。
2. Operation Queues vs. Grand Central Dispatch (GCD)
简单来说,GCD 是苹果基于 C 语言开发的,一个用于多核编程的解决方案,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。而 Operation Queues 则是一个建立在 GCD 的基础之上的,面向对象的解决方案。它使用起来比 GCD 更加灵活,功能也更加强大。
Operation Queues :相对 GCD 来说,使用 Operation 和 Operation Queues 会增加一点点额外的开销,但是我们却换来了非常强大的灵活性和功能,我们可以给 operation 之间添加依赖关系、取消一个正在执行的 operation 、暂停和恢复 operation queue 、设置Operation优先级等;GCD也可以进行suspend/resume,但不能取消。NSOperation作为OC对象,还支持KVO,这也是GCD所没有的。
GCD :则是一种更轻量级的,以 FIFO 的顺序执行并发任务的方式,使用 GCD 时我们并不关心任务的调度情况,而让系统帮我们自动处理。但是 GCD 的短板也是非常明显的,比如我们想要给任务之间添加依赖关系、取消或者暂停一个正在执行的任务时就会变得非常棘手。
NSOperation,被添加到队列中后-(void)start
方法会被调用,方法内会检查和设置op的状态,之后调用-(void)main
方法。如果一个op不打算放入队列,也可以手动调用开始方法,但是对于已经在队列中的op,再手动调用开始方法是错误的。
GCD使用注意事项
2.1 dispatch_once_t必须是全局或static变量,非全局或非static的dispatch_once_t变量在使用时可能会导致非常不好排查的bug。
2.2 创建队列dispatch_queue_t dispatch_queue_create ( const char *label, dispatch_queue_attr_t attr );
第二个参数dispatch_queue_attr_t在网上教程中常用NULL,实际提供了更清晰、严谨的参数DISPATCH_QUEUE_SERIAL、DISPATCH_QUEUE_CONCURRENT
2.3 dispatch_after是延迟提交,而非延迟运行。需要延迟运行可以使用定时器。
2.4 dispatch_suspend并非立即停止队列的运行,而是在当前block任务执行完成后,暂停后续的block执行。
2.5 dispatch_apply会“等待”其所有的循环运行完毕才往下执行,也就是会阻塞外部的线程。嵌套使用也会造成死锁。
dispatch_queue_t queue = dispatch_queue_create("com.my.testQueue", DISPATCH_QUEUE_SERIAL);
dispatch_apply(5, queue, ^(size_t i) {
NSLog(@"outter loop: %zu", i);
// 此处造成死锁
dispatch_apply(3, queue, ^(size_t j) {
NSLog(@"inner loop: %zu", j);
});
});
2.6 dispatchbarrier\(a)sync
作用就是向某个队列插入一个block,当目前正在执行的block运行完成后,阻塞这个block后面添加的block,只运行这个block直到完成,然后再继续后续的任务。这个效果只在自己创建的并发队列上有效,使用其他队列效果则与dispatch_(a)sync
一样。
2.7 dispatch_set_context
可以为队列添加上下文数据,但是因为GCD是C语言接口形式的,所以其context参数类型是“void *”。如果参数用Objective-C的对象,但是要用__bridge等关键字转为Core Foundation对象,同时注意在dispatch_set_finalizer_f
对应的函数中释放,避免内存泄露。
/*
__bridge: 只做了类型转换,不修改内存管理权;
__bridge_retained(即CFBridgingRetain)转换类型,同时将内存管理权从ARC中移除,后面需要使用CFRelease来释放对象;
__bridge_transfer(即CFBridgingRelease)将Core Foundation的对象转换为Objective-C的对象,同时将内存管理权交给ARC。
*/
void cleanStaff(void *context) {
//这里用__bridge转换,不改变内存管理权
Data *data = (__bridge Data *)(context);
NSLog(@"In clean, context number: %d", data.number);
//释放context的内存!
CFRelease(context);
}
- (void)testBody {
//创建队列
dispatch_queue_t queue = dispatch_queue_create("me.tutuge.test.gcd", DISPATCH_QUEUE_SERIAL);
//创建自定义Data类型context数据并初始化
Data *myData = [Data new];
myData.number = 10;
//绑定context
//这里用__bridge_retained转换,将context的内存管理权从ARC移除,交由我们自己手动释放!
dispatch_set_context(queue, (__bridge_retained void *)(myData));
//设置finalizer函数,用于在队列执行完成后释放对应context内存
dispatch_set_finalizer_f(queue, cleanStaff);
dispatch_async(queue, ^{
//获取队列的context数据
//这里用__bridge转换,不改变内存管理权
Data *data = (__bridge Data *)(dispatch_get_context(queue));
//打印
NSLog(@"1: context number: %d", data.number);
//修改context保存的数据
data.number = 20;
});
}
2.8 在当前队列中使用sync提交任务到当前队列,造成死锁。需要注意的是,队列和线程并非同一个概念,每个队列放的Block任务会在线程中执行,可能是主线程或子线程。
// 代码在ViewDidLoad方法,即主线程中
/*
Calling 'dispatch_sync' function and targeting the current queue results in deadlock.
1. dispatch_sync将block提交到main queue
2. dispatch_sync在阻塞当前队列任务的执行,直到Block执行完成
3. dispatch_sync死锁,因为要执行Block的队列被阻塞,Block无法完成
*/
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"main");
});
// 不会死锁
/* As an optimization, this function invokes the block on the current thread when possible.
1. dispatch_sync将block提交到自定义队列queue
2. dispatch_sync阻塞当前队列任务的执行,直到Block执行完成
3. queue在当前线程(主线程)执行Block,log输出主线程信息
*/
dispatch_queue_t queue = dispatch_queue_create("com.myQueue", NULL);
dispatch_sync(queue, ^{
NSLog(@"queue:%@",[NSThread currentThread]); // log main thread info
});
上面自定义队列的例子,输出了主线程的信息,因此网上其他博客中常说到的dispatch_sync
会阻塞当前线程的说法是错误的,毕竟queue中的任务在主线程执行了,因此应该是“阻塞了队列或者队列当前任务”更加准确。
2.9 GCD只能suspend和resume队列,并不能cancel。
3. 多线程安全
多线程的安全,一方面是防止一个进程在写时,另一个进程也在写入或者读取;另一方面是保证一个代码片段内对内存的连续多次访问结果一致。
线程安全是有粒度大小的,可能是一个model,可能是model中的一个数组,可能是一段代码或一个方法。
Apple的多线程编码文档: Threading Programming Guide。
3.1 @synchronized
@synchronized
和其他互斥锁一样,它防止不同的线程在同一时间获取相同的锁,而且不需要使用代码直接创建互斥锁对象,而是直接使用OC对象作为一个lock token。@synchronized
隐式添加了异常处理代码,如果代码块中抛出异常会自动释放互斥锁。
@synchronized(obj) {
// do work
}
// 上面代码实际上为,objc_sync_enter会创建一个与obj关联的互斥锁
@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}
如果传入的是nil对象,则会是一个空操作,失去了线程安全的功能,应该避免这种情况发生。
在SDWebImage中也有使用@synchronized
实现线程安全的情况:
// SDWebImageDownloaderOperation
@synchronized(self) {...}
// SDWebImageManager.m
@synchronized(self.failedURLs) {...}
@synchronized(self.runningOpeations) {...}
3.2 atomic关键字
atomic的作用只是给getter和setter加了个锁,atomic只能保证代码进入getter或者setter函数内部时是安全的,一旦出了getter和setter,多线程安全只能靠程序员自己保障了。所以atomic属性和使用property的多线程安全并没什么直接的联系。另外,atomic由于加锁也会带来一些性能损耗,所以我们在编写iOS代码的时候,一般声明property为nonatomic,在需要做多线程安全的场景,自己去额外加锁做同步。
@property (atomic, strong) NSString* stringA;
//thread A
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
//thread B
for (int i = 0; i < 100000; i ++) {
// getter 1
if (self.stringA.length >= 10) {
// getter 2
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
NSLog(@"Thread B: %@\n", self.stringA);
}
上面代码,在线程B中,getter2可能出现崩溃。原因是在getter1时,字符串长度大于10,而在getter2时字符串内容已经在线程A中被修改了,因此发生崩溃。
stringA属性是原子性的,它的set/get方法都是线程安全的,但是问题发生在set/get方法之外,两次对stringA内存区域的访问,内存内容已经发生了改变,因此需要加锁来保证这一段代码是原子性的。
//thread A
[_lock lock];
for (int i = 0; i < 100000; i ++) {
if (i % 2 == 0) {
self.stringA = @"a very long string";
}
else {
self.stringA = @"string";
}
NSLog(@"Thread A: %@\n", self.stringA);
}
[_lock unlock];
//thread B
[_lock lock];
if (self.stringA.length >= 10) {
NSString* subStr = [self.stringA substringWithRange:NSMakeRange(0, 10)];
}
[_lock unlock];
NSLock 可以协调同一应用程序中多个线程。可以保护全局数据以原子方式访问或代码片段以原子方式运行。
调用NSLock的unlock方法时,必须确保和调用lock方法的线程为同一个,如果是在不同线程可能发生不可预期的效果。
不应该使用NSLock来实现递归锁,在同一个线程上调用lock两次,线程将永远被锁住。递归锁应该使用NSRecursiveLock。
OSSpinLock会出现优先级反转的情况,也会出现空转耗CPU的情况,不适用于较长时间的任务,而且在iOS 10和macOS10.12标记了Deprecated(没彻底移除)。引入了一个新的os_unfair_lock,也是忙等机制。
在使用NSThread、NSOperation等OC对象的过程中涉及到多线程安全,常常会使用NSLock、NSConditionLock、NSRecursiveLock、NSCondition等类。
3.3GCD线程安全
3.3.1 dispatch_semaphore
信号量
dispatch_semaphore_t signal = dispatch_semaphore_create(1);
dispatch_time_t overTime = dispatch_time(DISPATCH_TIME_NOW, 3 * NSEC_PER_SEC);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作1 开始");
sleep(2);
NSLog(@"需要线程同步的操作1 结束");
dispatch_semaphore_signal(signal);
});
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
sleep(1);
dispatch_semaphore_wait(signal, overTime);
NSLog(@"需要线程同步的操作2");
dispatch_semaphore_signal(signal);
});
3.3.2 dispatch_barrier_(a)sync
+ 自定义并发队列
以SDWebImage中的代码为例
// SDImageCache.m
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManager new];
});
dispatch_async(self.ioQueue, ^{
// 执行缓存图片的增删查等操作
});
3.3.3 dispatch_sync
+ 自定义串行队列
// SDWebImageDownloader.m
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 对self.URLOperations的增删
dispatch_barrier_async(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:token.url];
}
});
参考文章: