多线程
相关面试题
- 你理解的多线程?
- iOS 中的多线程方案有哪几种?你更倾向于哪一种?
- 你在项目中用过 GCD 吗?
- GCD 的队列类型
- 说一下 OperationQueue 和 GCD 的区别,以及各自的优势
- 线程安全的处理手段有哪些?
- Objective-C 你了解的锁有哪些?在你回答基础上进行二次提问:
- 自旋锁和互斥锁对比?
- 使用以上锁需要注意哪些?
- 用 C/C++/Objective-C 任一语言实现自旋锁或互斥锁,口述即可
iOS 中的多线程方案
技术方案 | 简介 | 实现的语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | · 一套通用的多线程 API · 适用于 Unix、Linux、Windows 等系统 · 跨平台、可移植 · 使用难度大 |
C | 程序员管理 | 几乎不用 |
NSThread | · 提供面向对象的 API · 简单易用,可直接操作线程对象 |
Objective-C | 程序员管理 | 偶尔使用 |
GCD | · 旨在替代 NSThread 等线程技术 · 充分利用设备的多核心 |
C | 系统自动管理 | 经常使用 |
NSOperation | * 提供面向对象的 API * 基于 GCD 实现 * 比 GCD 多了一些更加简单实用的功能 |
Objective-C | 系统自动管理 | 经常使用 |
GCD 基础知识
什么是 GCD ?
GCD(Grand Central Dispatch) 是苹果公司为优化多核处理器性能而开发的并发编程技术,旨在简化并行任务的管理。
- GCD 中有 2 个用来执行任务的函数
- 用同步的方式执行任务
1
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- 用异步的方式执行任务
1
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
- 用同步的方式执行任务
1 | dispatch_queue_t queue = dispatch_get_global_queue(0, 0); |
GCD 源码,居然在 Swift 项目下
GCD 的队列有两种
- 串行队列:跟烤串一样,任务就像是烤串上一块一块的羊肉,执行时一个接着一个。
- 并发队列:不止一根烤串了,多根烤串同时执行任务。
同步、异步、并发、串行的概念
- 同步:不开启新线程
- 异步:有能力开启新线程
- 串行:一个任务接着一个任务执行
- 并发:多个任务可以同一时间执行
并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步函数 sync | * 不开新线程 * 任务串行 |
* 不开新线程 * 任务串行 |
* 不开新线程 * 任务串行 |
异步函数 async | * 开新线程 * 任务并发 |
* 开新线程 * 任务串行 |
* 不开新线程 * 任务串行 |
162:
死锁?
什么是死锁?
什么情况下产生死锁?
1 | dispatch_queue_t queue = dispatch_get_main_queue(); |
以上代码如果是在主线程执行就产生了死锁。如果不在主线程执行不会产生死锁。
1 | dispatch_queue_t queue = dispatch_get_main_queue(); |
以上代码不会产生死锁,异步主队列是在主线程上执行任务。很经典的场景。
1 | NSLog(@"执行任务1"); |
以上代码会产生死锁,两个block任务在同一个队列,又是同步函数
1 | NSLog(@"执行任务1"); |
以上代码不会产生死锁,
1 | NSLog(@"执行任务1"); |
以上代码也不会产生死锁。
1 | NSLog(@"执行任务1"); |
以上代码也不会产生死锁,并发队列
使用同步函数 sync 往当前的串行队列再次添加任务就会产生死锁
165:
面试题
以下代码的打印结果是什么:
1 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
打印结果是 1,3。performSelector: withObject: afterDelay: 方法依赖 RunLoop,子线程的 RunLoop 默认不开启,导致 2 的打印并不会执行。
166:
performSelector:withObject:afterDelay: 是在 CoreFoundation 库里面实现的
performSelector:withObject: 是在 objc4 库里面实现的
GNUstep 将 Cocoa 的 Objective-C 库重写了一遍并开源了,所以可以参考学习。
167:
面试题2
以下代码的打印结果是什么:
1 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
打印结果是 1,然后程序崩溃,打印完 1 之后线程退出了,没办法执行 test 了,要保活子线程就得开启 RunLoop,强引用也解决不了。
168:
调度组的使用
实现功能:发送多个网络请求,所有网络请求都完成的时候处理事情?
正是调度组的典型使用场景
1 | dispatch_group_t group = dispatch_group_create(); |
线程同步方案
169:
多线程存在的问题?同时做多件事情提高了效率,但是存在线程安全问题
资源共享问题:
多个线程访问和使用同一个资源
案例:
存钱取钱。
卖票案例。
解决办法:
线程同步技术:加锁
170-173:
iOS 中的线程同步方案有:
- OSSpinLock
- os_unfair_lock
- pthread_mutex
- NSLock
- NSRecursiveLock
- NSCondition
- NSConditionLock
- @synchronized
- dispatch_semaphore
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
然后是介绍 OSSpinLock 的使用了。导入 libkern/OSAtomic.h 头文件,OS_SPINLOCK_INIT 宏创建锁,OSSpinLockLock() 函数加锁 OSSpinLockUnlock() 函数解锁
OSSpinLock 叫自旋锁,等待锁的期间一直占用着 CPU,已经过时了,还有可能产生优先级问题。。。总是就是不要用了,那为什么还讲那么多七七八八的呢
原来 172 里是对代码的结构进行了调整。。。结合了一些封装设计在里面。。。站在讲解多线程锁的角度来讲,完全没有必要讲这块内容!
但是我想想有没有必要学习下怎么写的???
173:答疑
174:os_unfair_lock
这是自旋锁还是互斥锁呢?好像没讲呢?根据汇编得知是互斥锁,调用syscall让CPU休眠了
175:pthread_mutex
互斥锁,等待锁的线程会处于休眠状态。
176:pthread_mutex 递归锁
177:自旋锁,互斥锁的汇编实现
自旋锁底层是一个循环一直在执行。
互斥锁底层调用了syscall让CPU进入休眠
178:pthread_mutex 条件
讲了个什么场景没太理解。。。
感觉讲的真不好,总是磕磕绊绊来来回回重复叙述
pthread_cond_wait()
pthread_cond_signal()
意思好像是始终能保证 A 在 B 之前执行?好像是 OperationQueue 里面的依赖关系的?可能就是依赖的底层实现?
生产者?消费者?模式,意思是生产者必须先生产产品出来才可以给消费者消费。
179:
NSLock 是对 mutex 普通锁的封装
NSRecursiveLock 是对 mutex 递归锁的封装
NSCondition 是 mutex cond 的封装
180:课后答疑
181:
[self.confition signal] 解释
完全听不明白了,不知道在表达啥呢。。。
答疑 RunLoop,这不是前面讲过了,看看什么问题
- NSThread 的生命不是一个强引用就能保证的,它的 RunLoop 不执行,线程的入口函数执行完就释放了。
- 线程的入口函数执行了一段代码,但是如果不实现循环,它就跟普通的命令行程序执行完入口函数就结束了。
其实还是对线程的理解不够深入。确实线程对象比普通的对象更抽象。
182:NSConditionLock
NSConditionLock?不是有一个 NSCondition 吗,这个又是什么
原来 NSConditionLock 是对 NSCondition 的封装。提供了更多功能
条件锁,应用场景还是建立依赖。。。
183:串行队列
是的,GCD 的串行队列也可以实现线程同步
1 | dispatch_queue_t moneyQueue = dispatch_queue_create("custom_queue1", DISPATCH_QUEUE_SERIAL); |
这个好理解
184-185:dispatch_semaphore
信号量,通过信号量控制线程最大并发数量为 1 的话同样可以实现线程同步。
dispatch_semaphore_wait() 函数会让信号量减 1。如果信号量大于 0 会执行后续代码。如果信号量 <= 0 会进入休眠直到大于 0 才可以继续执行后续代码。
dispatch_semaphore_signal() 会让信号量加 1。
186:@synchronized 编译器语法糖
是 pthread_mutex_t 的封装。最简单的同步方案。
源码是在 objc4 库的 objc-sync.h objc-sync.mm 文件中,objc_sync_entry() objc_sync_exit()。发现是对 pthread_mutex_t 递归锁的封装。
187:
总结线程同步方案:
性能从高到底
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
188:自旋锁,互斥锁对比
- 什么情况使用自旋锁比较划算?
- 预计线程等待锁的时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU 资源不紧张
- 多核心处理器
- 什么情况使用互斥锁比较划算
- 预计线程等待锁的时间较长
- 单核处理器
- 加锁的代码有 IO 操作
- 加锁的代码复杂,循环量大
- 加锁的代码竞争频繁
189:atomic
属于属性的修饰符,但在 iOS 中只推荐使用 nonatomic。atomic 更多用于 macOS 中。atomic 的作用是使属性的 getter 和 setter 是原子的,意思是属性的 setter 方法和 getter 方法都进行了加锁。
源码在 objc4 中。objc-accessors.mm 中,可以看到 atomic 的情况下进行了加锁。
但是又说没有办法保证使用属性的线程是安全的?这是什么意思
这个还是需要 AI 再详细解读一下了
问清楚为什么在 iOS 中不推荐使用 atomic。
190-192:读写安全
文件的读写安全的实现。
- 信号量最大并发为1,可以实现,但是只能单读单写。如果要实现多读单写就不能使用信号量了。多个线程可以同时读取数据,但只有一个线程写入数据,读和写是不能同时进行(只能有一个线程写,且写的时候无法读,可以有多个线程读但读的时候无法写)
- pthread_rwlock 读写锁,等待锁的线程会进入休眠
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// 读锁
pthread_rwlock_rdlock(&lock);
// ......
pthread_rwlock_unlock(&lock);
// 写锁
pthread_rwlock_wrlock(&lock);
// ......
pthread_rwlock_unlock(&lock);
// 释放锁
pthread_rwlock_destroy(&lock); - dispatch_barrier_async 异步栅栏函数
1
2
3
4
5
6
7
8// 必须使用创建的并发队列,全局并发队列或串行队列都无效
dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
// 读操作...
});
dispatch_barrier_async(queue, ^{
// 写操作...
});