字学镜像计划-多线程及GCD(大纲)

152 阅读4分钟

线程

操作系统任务调度的基本单元

把进程看作一个容器,线程就是容器里面可执行的基本单元。
  • 合理使用多线程才能充分利用多核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对象,两个线程同时调用会怎样?列出可能出现问题的步骤。

  • 线程一

    1. 读取指针
    2. retain指针
    3. 赋予新值
    4. release老指针
  • 线程二

    1. 读取指针
    2. retain指针
    3. 使用指针
    4. 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 = 0dispatch_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