线程
操作系统任务调度的基本单元
把进程看作一个容器,线程就是容器里面可执行的基本单元。
- 合理使用多线程才能充分利用多核CPU
- 合理设置优先级让重要的任务更快完成(比如主线程)
共享一个进程内的资源
- 共享虚拟内存/描述符等
- 多个线程访问共享资源可能存在竞争
有独立的调用栈/本地变量/寄存器上下文
- 线程的创建/销毁/切换也是有一定开销的
API
常见API
- POSIX Thread
- 所有的UN*X系统都支持,比如Mac、linux;可移植性
- 开源
- 包含非标准的扩展“_np"
- C接口
- 用起来较为麻烦
- NSThread
- pthread的面向对象封装
- 只暴露了pthread的部分能力
- 可以通过KVO监听部分属性
- 可监听 isExecuting/isFinished/isCancelled 等属性
- GCD(用得最多、Apple平台较为先进的多线程管理调度API,由c实现)
- 内部管理了一个线程池
- 基于队列的抽象
- 支持block作为任务的载体
- 开源(源代码复杂)
- NSOperation(底层由cpp实现,提供了一些面向对象的封装)
- GCD的面向对象封装
- 支持GCD的部分功能
- 支持指定任务依赖
- 支持设定并发数
- 支持取消任务/KVO监听任务状态
GCD队列
-
FIFO(先进先出)
-
支持穿行和并行队列
-
支持队列的挂起和恢复
-
队列在创建时可以指定它是串行的还是并行的,线程和队列不一定是一一对应的
-
Main Thread(主线程)对应包含Main Queue,GCD Thread Pool(GCD线程池)包含 HighPriorityQueue, DefaultPriorityQueue, LowPriorityQueue,BackgroundPriorityQueue,优先级依次下降。
获取主队列 dispatch_queue_t queue = dispatch_get_main_queue(); 获取全局队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 创建串行队列 dispatch_queue_t queue = dispatch_queue_create("serial-queue", NULL); 创建并行队列 dispatch_queue_t queue = dispatch_queue_create("concurrent-queue", DISPATCH_QUEUE_CONCURRENT);
GCD任务
异步执行:任务加入队列后函数返回
dispatch_async(queue, ^{
NSLog(@"Do something");
});
同步执行:阻塞当前线程等待到任务完成
dispatch_sync(queue, ^{
NSLog(@"Do something");
});
在并行队列中使用barrier_async,在barrier之前
的异步任务执行完之后, 在barrier之后的任务才会执行。
dispatch_barrier_async(queue, ^{
NSLog(@"----barrier------");
});
注意事项
- 全局队列同时也是并行的
- 队列优先级尽量使用DEFAULT
- 队列和线程并不是一一对应
线程安全
- 定义 挡多个线程访问同一个对象时,如果不用考虑这些线程在运行环境下的调度和交替运行时,也不需要进行额外的同步,或者在调用房进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象时线程安全的。
- 线程不安全的对象
- 文档中未注明线程安全的对象(如NSMutableArray)
- 只能在某个线程使用的对象
- UIKit/CoreAnimation中几乎所有的对象只能在主线程使用
- 例外:UIImage/UIFont
难点
-
多线程下操作的顺序不可预测
@interface Sequence : NSObject{
NSInteger _value;
}
@end
@implenmentation Sequence
-(NSInteger)value{
return _value;
}
- (void)next {
_value++;
}
@end
//假设_value初始值是0,开两个线程++一次,会是什么结果?
// 不可预期,具体要看线程如何调度。
思考题:如果_value是个OC对象,两个线程同时调用会怎样?列出可能出现问题的步骤。
-
线程一
- 读取指针
- retain指针
- 赋予新值
- release老指针
-
线程二
- 读取指针
- retain指针
- 使用指针
- release指针 其中一种情况是:执行顺序为1.1->2.1->2.2->2.3->2.4->1.2;在线程一想要retain(谁用谁retain的规则)的时候,指针已经被release掉了。
-
编译器优化会重排代码
-
CPU会乱序执行指令
-
不要对执行顺序妄下假设
同步机制
-
@synchronized
@interface Sequence : NSObject{
NSInteger _value;
}
@end
@implenmentation Sequence
-(NSInteger)value{
@synchronized(self){
return _value;
}
}//读和写都要用同一把锁去保护
- (void)next {
@synchronized(self){
//两个线程抢一把锁,谁抢到谁 ++
_value++;
}
}
@end
@synchronized无死锁的隐患(递归锁)
-
NSLock/NSRecursiveLock
NSLock *theblock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
[theblock lock];
//Do SOMETHING
[theblock unlock];
}#;
因Do Something可以自定义,因此可以获得更高的效率;但因dosomething的不加限制,有可能会导致死锁:比如抛出异常或再加lock。 NSRecursiveLock解决单线程内嵌套调用同一个锁(注意多线程内还是会有可能死锁)
-
GCD串行队列
保证运行顺序
- (NSInteger)value{
_block NSInteger result = 0;
dispatch_sync(_queue, ^{
result = _value;
});
return result;
}
- (void)next {
dispatch_sync(_queue, ^{
_value++;
NSLog(@"%zd", self.value);//死锁
//自己等待自己
});
}
@end
- 尽量避免使用dispatch_sync
- (NSInteger)value{
__block NSInteger result = 0;
dispatch_async(_queue, ^{
result = _value;
});
return result;
}
- (void)next {
dispatch_async(_queue, ^{
_value++;
NSLog(@"%zd", self.value);
});
}
@end
调试工具
- XCode检查器
课后练习
- 游戏 The Deadlock Empire
- 用TSAN找bug
- 自己所在的项目
- 稳定分支
- 打开全源码编译
- 打开TSAN
- 找到多线程问题并解决
- 实现一个单例
-
线程安全
-
尽量高效(减少锁的竞争)
-
不能使用dispatch_once
-