1. 提个问题, 什么是线程安全?
- 多线程同时访问一段代码, 不会造成数据混乱的情况.
1.1 哪些场景运用到了线程安全?
- 举例12306同一列火车的车票, 同一时间段多人抢票, 如何解决互斥锁使用格式?
synchronized(锁对象) {
// 需要锁定的代码
// 锁定一份代码只用一把锁, 用多把锁是无效的
}
- 互斥锁的优缺点
-
- 优点: 能有效防止因多线程抢夺资源造成的数据安全问题
-
- 缺点: 需要消耗大量的CPU资源
- 互斥锁的使用前提: 多条线程抢夺同一块资源
- 线程同步是指: 多条线程按顺序地执行任务
- 互斥锁就是使用了线程同步技术
1.2 OC中的原子和非原子属性
- OC在定义属性时有nonatomic和atomic两种选择
atomic(默认): 用于保证属性setter、getter的原子性操作, 相当于在getter和setter内部加了线程同步的锁-
- 线程安全, 需要消耗大量的资源
-
- 不一定能保证使用属性的过程是线程安全的
-
-
- 一个线程在连续多次读取某条属性值的时候, 同时别的线程在改值, 这样还是会读取到不同的属性值.
-
-
-
- 一个线程在获取当前属性的值, 另一个线程把这个属性释放了, 可能会造成崩溃
-
nonatomic: 非原子属性, 不加锁-
- 非线程安全, 适合内存小的移动设备
// atomic加锁原理:
@property (atomic, assign) int age;
- (void)setAge:(int)age {
@synchronized(self) {
_age = age;
}
}
- 开发中建议所有属性都声明为nonatomic, 尽量避免多线程抢夺同一块资源,
- 将加锁、资源抢夺的业务逻辑交给服务端处理
1.3 @synchronized
@synchronized是对mutex递归锁的封装- 源码查看: objc4中的objc-sync.mm文件
- @synchronized(obj)内部会生成obj对应的递归锁, 然后进行加锁、解锁操作
@synchronized(obj) {
// 任务
}
2. 各种锁
2.1 OSSpinLok
- 操作系统多线程轮转算法
2.2 os_unfair_lock
2.3 pthread_mutex
2.4 pthread_mutex - 递归锁
2.5 pthread_mutex - 条件
2.6 NSLock、NSRecursiveLock、NSCondition
NSLock是对mutex普通锁的封装
-
NSRecursiveLock也是对mutex递归锁的封装, API跟NSLock基本一致 -
NSCondition是对mutex和cond的封装 -
- 对锁和条件的封装
-
- 把C语言的一些数据结构屏蔽了
- 把C语言的一些数据结构屏蔽了
-
NSConditionLock是对NSCondition的进一步封装, 可以设置具体的条件值
3. 自旋锁、互斥锁比较
-
什么情况使用自旋锁比较划算?
-
- 预计线程等待锁的时间很短
-
- 加锁的代码(临界区)经常被调用, 但竞争情况很少发生
-
- CPU资源不紧张
-
- 多核处理器
-
什么情况使用互斥锁比较好?
-
- 预计线程等待的时间较长
-
- 单核处理器
-
- 临界区有IO操作
-
- 临界区代码复杂或者循环量大
-
- 临界区竞争非常激烈
3.1 iOS线程同步方案加锁性能比较
- 性能从高到低排序
4. iOS中的多线程
4.1 iOS的多线程方案有哪几种?
- GCD
- 只要是同步或主队列, 都是串行执行
- 死锁的产生, 出现了相互等待产生死锁
- 使用
sync函数往当前串行队列中添加任务, 会卡主当前的串行队列产生死锁 - sync执行完旧任务才能执行新任务
- (void)testDeadLock {
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("myqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue3 = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{// block0
NSLog(@"执行任务2");
// sync + queue, 同步串行 同一个队列, 死锁
// sync + queu2, 同步并发 不同的队列, 不死锁
// sync + queue3, 同步串行 不同的队列, 不死锁
// async + queue, 异步串行, 不死锁
dispatch_sync(queue3, ^{// block1
NSLog(@"执行任务3 - %@", [NSThread currentThread]);
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
- 全局并发队列只有一份, 打印出来的内存地址一样
4.2 队列组的使用
4.3 NSNotification的多线程
- 通知中心采用单例的模式,整个系统只有一个通知中心。可以通过
[NSNotificationCenter defaultCenter]来获取对象。 - 在多线程应用中,Notification在哪个线程中post,就在哪个线程中被转发,而不一定是在注册观察者的那个线程中。
-
- Notification的发送与接收处理都是在同一个线程中。
-
- 虽然我们在主线程中注册了通知的观察者,但在全局队列中post的Notification,并不是在主线程处理的。
-
- 如果我们想在回调中处理与UI相关的操作,需要确保是在主线程中执行回调。
- 那么怎么才能做到一个Notification的post线程与转发线程不是同一个线程呢?
-
- 设置[NSOperationQueuemainQueue],就可以实现在主线程中刷新UI的操作。
[[NSNotificationCenter defaultCenter] addObserverForName:@"Test_Notification" object:nil queue [NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
NSLog(@"接收和处理通知的线程%@", [NSThread currentThread]);
}];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"Test_Notification" object:nil userInfo:nil];
NSLog(@"发送通知的线程为%@", [NSThread currentThread]);
});
5. 多线程的坑
- 写 UIKit、AFNetworking、FMDB 这些库的“大神”们,并不是解决不了多线程技术可能会带来的问题,
- 而相反正是因为他们非常清楚这些可能存在的问题,所以为避免使用者滥用多线程,亦或是出于性能考虑,
- 而选择了使用单一线程来保证这些基础库的稳定可用。
5.1 常驻线程
- 常驻线程
-
- 指的就是那些不会停止,一直存在于内存中的线程。
-
- AFNetworking 2.0 专门创建了一个线程来接收 NSOperationQueue 的回调,这个线程其实就是一个常驻线程。
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
// 先用 NSThread 创建了一个线程
[[NSThread currentThread] setName:@"AFNetworking"];
// 使用 run 方法添加 runloop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
- 虽然说,在一个 App 里网络请求这个动作的占比很高,但也有很多不需要网络的场景,所以线程一直常驻在内存中,也是不合理的。
- AFNetworking 在 3.0 版本时,使用苹果公司新推出的 NSURLSession 替换了 NSURLConnection,从而避免了常驻线程这个坑。
-
- NSURLSession 可以指定回调 NSOperationQueue,这样请求就不需要让线程一直常驻在内存里去等待回调了。
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
-
- NSURLSession 发起的请求,可以指定回调的 delegateQueue,不再需要在当前线程进行代理方法的回调。所以说,NSURLSession 解决了 NSURLConnection 的线程回调问题。
- AFNetworking 2.0 使用常驻线程也是无奈之举,一旦有方案能够替代常驻线程,它就会毫不犹豫地废弃常驻线程。
5.2 并发
- 总结来讲,类似数据库这种需要频繁读写磁盘操作的任务,尽量使用串行队列来管理,避免因为多线程并发而出现内存问题。
- 创建线程的过程,需要用到物理内存,CPU 也会消耗时间。
-
- 而且,新建一个线程,系统还需要为这个进程空间分配一定的内存作为线程堆栈。
-
- 堆栈大小是 4KB 的倍数。在 iOS 开发中,主线程堆栈大小是 1MB,新创建的子线程堆栈大小是 512KB。
-
- 除了内存开销外,线程创建得多了,CPU 在切换线程上下文时,还会更新寄存器,更新寄存器的时候需要寻址,而寻址的过程还会有较大的 CPU 消耗。
- 线程过多时内存和 CPU 都会有大量的消耗,从而导致 App 整体性能降低,使得用户体验变成差。
5.3 多线程避坑
- 一提到多线程技术,我们往往都会联想到死锁等锁的问题,但其实锁的问题是最容易查出来的,
-
- 反而是那些藏在背后,会慢慢吃尽你系统资源的问题,才是你在使用多线程技术时需要时刻注意的。
- 常驻线程一定不要滥用,最好不用。
- 除非是并发数量少且可控,或者必须要在短时间内快速处理数据的情况,否则我们在一般情况下为避免数量不可控的并发处理,都需要把并发队列改成串行队列来处理。
发文不易, 喜欢点赞的人更有好运气👍 :), 定期更新+关注不迷路~
ps:欢迎加入笔者18年建立的研究iOS审核及前沿技术的三千人扣群:662339934,坑位有限,备注“掘金网友”可被群管通过~