相关面试题

  • 你理解的多线程?
  • 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
2
3
4
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"执行任务,线程:%@", [NSThread currentThread]);
});

GCD 源码,居然在 Swift 项目下

GCD 的队列有两种
- 串行队列:跟烤串一样,任务就像是烤串上一块一块的羊肉,执行时一个接着一个。
- 并发队列:不止一根烤串了,多根烤串同时执行任务。

同步、异步、并发、串行的概念
- 同步:不开启新线程
- 异步:有能力开启新线程
- 串行:一个任务接着一个任务执行
- 并发:多个任务可以同一时间执行

并发队列 手动创建的串行队列 主队列
同步函数 sync * 不开新线程
* 任务串行
* 不开新线程
* 任务串行
* 不开新线程
* 任务串行
异步函数 async * 开新线程
* 任务并发
* 开新线程
* 任务串行
* 不开新线程
* 任务串行

162:
死锁?
什么是死锁?
什么情况下产生死锁?

1
2
3
4
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务");
});

以上代码如果是在主线程执行就产生了死锁。如果不在主线程执行不会产生死锁。

1
2
3
4
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务");
});

以上代码不会产生死锁,异步主队列是在主线程上执行任务。很经典的场景。

1
2
3
4
5
6
7
8
9
10
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("custom_queue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");

以上代码会产生死锁,两个block任务在同一个队列,又是同步函数

1
2
3
4
5
6
7
8
9
10
11
NSLog(@"执行任务1");
dispatch_queue_t queue1 = dispatch_queue_create("custom_queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("custom_queue2", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue1, ^{
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");

以上代码不会产生死锁,

1
2
3
4
5
6
7
8
9
10
11
NSLog(@"执行任务1");
dispatch_queue_t queue1 = dispatch_queue_create("custom_queue1", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t queue2 = dispatch_queue_create("custom_queue2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue1, ^{
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");

以上代码也不会产生死锁。

1
2
3
4
5
6
7
8
9
10
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("custom_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
NSLog(@"执行任务2");
dispatch_sync(queue, ^{
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");

以上代码也不会产生死锁,并发队列

使用同步函数 sync 往当前的串行队列再次添加任务就会产生死锁

165:
面试题
以下代码的打印结果是什么:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
}

- (void)test {
NSLog(@"2");
}

打印结果是 1,3。performSelector: withObject: afterDelay: 方法依赖 RunLoop,子线程的 RunLoop 默认不开启,导致 2 的打印并不会执行。

166:
performSelector:withObject:afterDelay: 是在 CoreFoundation 库里面实现的
performSelector:withObject: 是在 objc4 库里面实现的

GNUstep 将 Cocoa 的 Objective-C 库重写了一遍并开源了,所以可以参考学习。

167:
面试题2
以下代码的打印结果是什么:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
}];
[thread start];

[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}

- (void)test {
NSLog(@"2");
}

打印结果是 1,然后程序崩溃,打印完 1 之后线程退出了,没办法执行 test 了,要保活子线程就得开启 RunLoop,强引用也解决不了。

168:
调度组的使用
实现功能:发送多个网络请求,所有网络请求都完成的时候处理事情?
正是调度组的典型使用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_queue_create("custom_queue", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务1,线程:%@", [NSThread currentThread]);
}
});
dispatch_group_async(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务2,线程:%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3,线程:%@", [NSThread currentThread]);
}
});

线程同步方案

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,这不是前面讲过了,看看什么问题

  1. NSThread 的生命不是一个强引用就能保证的,它的 RunLoop 不执行,线程的入口函数执行完就释放了。
  2. 线程的入口函数执行了一段代码,但是如果不实现循环,它就跟普通的命令行程序执行完入口函数就结束了。
    其实还是对线程的理解不够深入。确实线程对象比普通的对象更抽象。

182:NSConditionLock
NSConditionLock?不是有一个 NSCondition 吗,这个又是什么
原来 NSConditionLock 是对 NSCondition 的封装。提供了更多功能
条件锁,应用场景还是建立依赖。。。

183:串行队列
是的,GCD 的串行队列也可以实现线程同步

1
2
3
4
dispatch_queue_t moneyQueue = dispatch_queue_create("custom_queue1", DISPATCH_QUEUE_SERIAL);
dispatch_sync(moneyQueue, ^{

});

这个好理解

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:
总结线程同步方案:
性能从高到底

  1. os_unfair_lock
  2. OSSpinLock
  3. dispatch_semaphore
  4. pthread_mutex
  5. dispatch_queue(DISPATCH_QUEUE_SERIAL)
  6. NSLock
  7. NSCondition
  8. pthread_mutex(recursive)
  9. NSRecursiveLock
  10. NSConditionLock
  11. @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. 信号量最大并发为1,可以实现,但是只能单读单写。如果要实现多读单写就不能使用信号量了。多个线程可以同时读取数据,但只有一个线程写入数据,读和写是不能同时进行(只能有一个线程写,且写的时候无法读,可以有多个线程读但读的时候无法写)
  2. pthread_rwlock 读写锁,等待锁的线程会进入休眠
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #import <pthread.h>

    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);
  3. 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, ^{
    // 写操作...
    });