多线程
官方文档:线程编程指南
GCD源码:https://github.com/apple/swift-corelibs-libdispatch
iOS 中常见的多线程方案
iOS 中常将的多线程方案如下:
GCD 多线程基本概念
- 同步/异步: 是否有开启新线程的能力
- 串行队列/并行队列: 是否具有并行执行任务的能力。主队列是一种特殊的串行队列
Note:
在主线程执行同步串行任务,会卡死主线程。
原理: 在串行队列里面执行同步任务,就会产生死锁。
多线程安全问题与解决
多线程安全问题在于:多个线程同时访问并修改同一变量值,会造成最终值不正确。
例如:存取钱问题、售票问题
解决方案: 使用线程同步技术(就是协同步调,按照预定的先后次序进行)。常见的线程同步技术为:加锁
原则: 对于修改同一个变量值,需要用同一个锁。如果只是读取,则无需加锁
常用的锁(效率从高到底):
- os_unfair_lock
- OSSpinLock
- dispatch_semaphore
- pthread_mutex
- dispatch_queue(DISPATCH_QUEUE_SERIAL)
- NSLock
- NSCondition
- pthread_mutex(recursive)
- NSRecursiveLock
- NSConditionLock
- @synchronized
OSSpinLock
OSSpinLock 自旋锁, 等待锁的线程会处于忙等状态(busy-wait),一直占用着CPU资源
目前已经不再安全,可能会出现线程优先级翻转问题。表现上也类似死锁:如果等待锁的线程优先级较高,它就会一直占用CPU资源,优先级低的线程就无法释放锁
已经在iOS10开始被废弃。需要引入头文件#import <libkern/OSAtomic.h>
,使用如下:
#import <libkern/OSAtomic.h>
// 初始化锁
OSSpinLock lock = OS_SPINLOCK_INIT;
// 加锁
OSSpinLockLock(&lock);
// 中间需要做的操作...
// 解锁
OSSpinLockUnlock(&lock);
/////////////////////////////////////////////
iOS 10之后替代 os_unfair_lock 头文件<os/lock.h>
os_unfair_lock
os_unfair_lock 作为 OSSpinLock 的替代品,解决了优先级反转问题,能做到让等待的线程处于真正的休眠状态,其接口与OSSpinLock 相似。需导入头文文件<os/lock.h>
#import <os/lock.h>
// 初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
// 加锁/尝试加锁
void os_unfair_lock_lock(os_unfair_lock_t lock);
bool os_unfair_lock_trylock(os_unfair_lock_t lock);
// 解锁
void os_unfair_lock_unlock(os_unfair_lock_t lock);
// 判断是否锁的拥有者是自己,
void os_unfair_lock_assert_owner(os_unfair_lock_t lock);
void os_unfair_lock_assert_not_owner(os_unfair_lock_t lock);
pthread_mutex
pthread_mutex 能做到让等待的线程处于休眠状态。需要引入头文件 <pthread.h>
互斥锁/递归锁/条件锁
// 普通互斥锁,属性传NULL
pthread_mutex_init(&_mutex, NULL);
pthread_mutex_lock(&_mutex);
// 中间需要的操作
pthread_mutex_unlock(&_mutex);
---递归锁 -----------------------
// 递归锁:允许同一个线程对一把锁进行重复加锁
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
pthread_mutex_lock(&_mutex);
// 中间需要的操作
pthread_mutex_unlock(&_mutex);
----条件锁--------------------------
当多线程执行任务有条件依赖的是可以用条件锁。
- (void)__remove
{
pthread_mutex_lock(&_mutex);
if (self.data.count == 0) {
// 等待
pthread_cond_wait(&_cond, &_mutex);
}
[self.data removeLastObject];
pthread_mutex_unlock(&_mutex);
}
// 线程2
// 往数组中添加元素
- (void)__add
{
pthread_mutex_lock(&_mutex);
sleep(1);
[self.data addObject:@"Test"];
// 信号 - 唤醒被该条件加的锁
pthread_cond_signal(&_cond);
// 广播
// pthread_cond_broadcast(&_cond);
pthread_mutex_unlock(&_mutex);
}
NSLock、NSRecursiveLock、NSCondition、NSConditionLock
这几个锁是基于 pthread_mutex 的 OC 封装。其使用更加简单、更加面向对象。
// NSLock - 封装自 pthread_mutex_lock 默认锁
self.lock = [[NSLock alloc] init];
[self.ticketLock lock];
// 加锁代码
[self.ticketLock unlock];
// NSCondition -- 封装自 pthread_mutex_lock 默认条件锁
self.condition = [[NSCondition alloc] init];
[self.condition lock];
// 等待
[self.condition wait];
// 信号
[self.condition signal];
// 广播
[self.condition broadcast];
[self.condition unlock];
// NSConditionLock -- 封装自 pthread_mutex_lock 条件锁,可加自定义条件
self.conditionLock = [[NSConditionLock alloc] initWithCondition:1];
// 以下三段代码可以按顺序执行
[self.conditionLock lock];
NSLog(@"__one");
[self.conditionLock unlockWithCondition:2];
[self.conditionLock lockWhenCondition:2];
NSLog(@"__two");
[self.conditionLock unlockWithCondition:3];
[self.conditionLock lockWhenCondition:3];
NSLog(@"__three");
[self.conditionLock unlock];
//
dispatch_queue(DISPATCH_QUEUE_SERIAL)
使用串行队列也能解决多线程资源竞争问题,将线程加入到串行队列按顺序执行。
self.serialQueue = dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_sync(self.serialQueue, ^{
// 处理变量赋值等核心功能代码
});
dispatch_semaphore
semaphore 叫做“信号量”,用来控制线程的最大并发数量。
如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码。
如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码。
dispatch_semaphore_signal(); 给对对应的信号量 +1
semaphore 初始值为1时候,非常适合做线程同步
// 设置最大允许并发数 5
self.semaphore = dispatch_semaphore_create(5);
- (void)test
{
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 相关代码
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@synchronized
@synchronized 是对 mutex 递归锁的封装。可以参考 runtime 源码 objc_sync源码。
// 参数即要设置为锁的值,就是一个指针
@synchronized([self class]) {
[super __drawMoney];
}
锁的使用小技巧: 宏
#define SemaphoreBegin \
static dispatch_semaphore_t semaphore; \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
semaphore = dispatch_semaphore_create(1); \
}); \
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
#define SemaphoreEnd \
dispatch_semaphore_signal(semaphore);
--------------
SemaphoreBegin;
// .....
SemaphoreEnd;
atomic 原子操作
写属性的时候常用 atomic、nonatomic
给属性加上 atomic 修饰,可以保证属性 setter 和 getter 方法都是原子性操作,也就是保证 setter 和 getter 内部都是线程同步的。这里可以参考 runtime 源码 objc-accessors
。本质上也是加锁,源码如下
// 获取属性对象
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) { // 对象本质为结构体,根据属性的在结构体内的 offset 获取。如果offset == 0 即获取结构体首地址,即 isa 地址
return object_getClass(self);
}
// Retain release world
// 根据 offset 获取结构体内 属性指针
id *slot = (id*) ((char*)self + offset);
// 如果是非原子属性,就直接返回属性指针
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
atomic 给 setter/getter 内部加锁保证了属性存取的安全,但是不能保证属性取出之后的操作安全。
因为存取方法使用过于频繁,所以 atomic 显得过于消耗性能。
iOS 中的读写安全方案
IO 操作 -> 文件读写操作 -> 【多度,单写】
实际操作条件:
- 同一时间,只能有一个线程进行写操作
- 同一时间,允许有多个线程进行读操作
- 同一时间,不允许既有写操作,又有读操作
方案如下:
- pthread_rwlock
- dispatch_barrier_sync
pthread_rwlock
pthread_rwlock 也是互斥锁,等待锁的进程会进入休眠。使用如下
// 创建读写锁属性
pthread_rwlockattr_t rwAttr;
pthread_rwlockattr_init(&rwAttr);
// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, &rwAttr); // pthread_rwlock_init(&lock, NULL);
// 读-加锁
pthread_rwlock_rdlock(&lock);
// 读-尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写-加锁
pthread_rwlock_wrlock(&lock);
// 写-尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁
pthread_rwlock_destroy(&lock);
dispatch_barrier_sync
- 这个函数闯入的并发队列,必须是自己通过
dispatch_queue_create
创建的 - 如果传入的是一个串行或者全局并发队列,那就相当于调用
dispatch_async
函数
// 创建队列
dispatch_queue_t _Nonnull queue = dispatch_queue_create("barrierQueue", DISPATCH_QUEUE_CONCURRENT);
// 读 - 异步线程,可以多线程同时访问
dispatch_barrier_async(queue, ^{
});
// 写 - 同步任务,只有一个线程可以写
dispatch_barrier_sync(queue, ^{
});
面试题
- 你理解的多线程?
线程是应用程序内部实现多个执行路径的相对轻量的方法。
系统->并行执行进程->进程执行一个或者多个线程。
这些线程可以同时或者几乎同时的方式执行不同的任务。
系统本身实际上管理这些执行的线程,安排它们在可用的内核上运行,并根据需要中断它们,将执行时间分配给其他线程。
多线程有点:
1. 可以提高程序的感知响应能力,
2. 可以提高应用程序在多核系统上的实时性能
缺点:
1. 增加代码复杂性,它们可以访问同样的资源,多个线程需协同合作,防止破坏程序的状态信息
2. 线程间的资源竞争问题,需要线程同步的技术来额外处理
- 以下代码执行情况如何?正确执行/奔溃?why?
- (void)interview01
{
// 会产生死锁!卡死主线程
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// dispatch_sync立马在当前线程同步执行任务
}
- (void)interview02
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// dispatch_async不要求立马在当前线程同步执行任务
}
- (void)interview03
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
- (void)interview04
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_SERIAL);
// dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue2 = dispatch_queue_create("myqueu2", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
- (void)interview05
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
- 下面代码打印什么?为什么?
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self test2];
}
- (void)test
{
NSLog(@"2");
}
- (void)test2
{
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");
});
}
// 打印 1、3
// 原因: performSelector:withObject:afterDelay 这个方法本质上是给Runloop 添加定时器。而子线程虽然已经创建了 runloop 但是并没有运行,所以不会打印,处理方式就是运行子线程的 runloop,让子线程保活
// 以下代码同理
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
- 如何实现如首页多个网络请求,最后一个请求基于前面的网络请求的情况
// 使用 dispatch_group 的方式。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 创建并发队列
dispatch_queue_t queue = dispatch_queue_create("my_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, queue, ^{
// dispatch_async(dispatch_get_main_queue(), ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务3-%@", [NSThread currentThread]);
// }
// });
// });
// dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// for (int i = 0; i < 5; i++) {
// NSLog(@"任务3-%@", [NSThread currentThread]);
// }
// });
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务3-%@", [NSThread currentThread]);
}
});
dispatch_group_notify(group, queue, ^{
for (int i = 0; i < 5; i++) {
NSLog(@"任务4-%@", [NSThread currentThread]);
}
});
}
- iOS 的多线程有几种方案,你更倾向于哪一种?
pthread
NSThread
GCD ---> 更倾向
NSOperation
- 你在项目中用过 GCD 吗?
用过
如:
dispatch_semaphore -> 信号量
dispatch_barrier
dispatch_queue
dispatch_group
dispatch_sync & dispatch_async
...
- GCD 的队列类型
串行队列 & 并行队列
- 说一下 OperationQueue 和 GCD 的区别,以及各自优势?
GCD:
基于C语言的API,旨在替代 NSThread 的线程技术,可以高效利用设备多核。
OperationQueue:
底层封装自 GCD,增加了很多使用功能,更加面向对象。
- 线程安全处理的手段有哪些?
1. 加锁
2. 使用 GCD 串行队列
3. 使用 GCD 信号量
- OC 你了解的锁有哪些?在你的回答基础上进行二次提问
- 1.自旋锁和互斥锁的对比
- 2.使用以上锁需要注意哪些?
- 3.使用 C/OC/C++,任选其一,实现自旋或互斥?口述即可
了解的锁:
OSSpinLock、os_unfair_lock、pthread_mutex、NSLock、NSCondition、NSRecursiveLock、NSConditionLock、@synchronized
自旋锁适合的场景
1. 预计线程等待锁的时间很短
2. 加锁的代码(临界区)经常被调用,但竞争情况不是很激烈
3. CPU 资源不是很紧张
4. 多核处理器
互斥锁比较适合的场景
1. 预计线程等待的时间较长
2. 单核处理器(减少CPU占用)
3. 临界区有 IO 操作(IO 操作本身占CPU)
4. 临界区代码复杂或者循环量很大
5. 临界区竞争激烈