方案 | 简介 | 语言 | 生命周期 | 实用频率 |
---|---|---|---|---|
pthread | 跨平台(Unix,Linux,Windows) 更底层 |
C 语言 | 程序员管理 | 很少使用 |
NSThread | 面向对象,简单易用 | OC 语言 | 程序员管理 | 偶尔实用 |
GCD | 旨在代替 NSThread 等线程技术 充分利用设备的多核 |
C 语言 | 自动管理 | 经常使用 |
NSOperation | 基于 GCD 的封装,更加面向对 比 GCD 多了一些简单实用的功能 |
OC 语言 | 自动管理 | 经常使用 |
GCD
异步方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
同步方式执行任务
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);
GCD 队列
- 并发队列
- 让多个任务同时执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(dispatch_async)函数下才有效
- 串行队列
- 让任务一个接着一个的执行(一个任务执行完毕后,在执行下一个任务)
术语:同步 异步 并发 串行
- 同步和异步:能不能开启新的线程
- 同步:在当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
- 并发和串行:任务的执行方式
- 并发:多个任务同时执行
- 串行:一个接一个执行
小总结:
==dispatch_sync 和 dispatch_async 具备了开启线程的能力
队列的类型:决定了任务执行方式:并发,串行==
==dispatch_sync:要求立马执行任务
dispatch_async:不要求立马执行任务,可以等上一个任务执行完成再执行==
- | 并发队列 | 串行队列 | 主队列 |
---|---|---|---|
同步 | ==没有==开启新线程 ==串行==执行任务 |
==没有==开启新线程 ==串行==执行任务 |
==没有==开启新线程 ==串行==执行任务 |
异步 | ==有==开启新线程 ==并发==执行任务 |
==有==开启新线程 ==串行==执行任务 |
==没有==开启新线程 ==串行==执行任务 |
死锁
// 问题:以下代码在主线程执行,会不会产生死锁?
NSLog(@"执行任务 1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"执行任务 2");
});
NSLog(@"执行任务 3");
答案:会,dispatch_get_main_queue 主队列,表示在主线程执行,在队列中 FIFO,先进先出,所以:dispatch_sync 要求立马执行,queue 请求一个一个的执行,但是当前 任务 3 还没有执行完,所以冲突了
// 问题:以下代码在主线程执行,会不会产生死锁?
NSLog(@"执行任务 1");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{ // block0
NSLog(@"执行任务 2");
dispatch_sync(queue, ^{ // block1
NSLog(@"执行任务 3");
});
NSLog(@"执行任务 4");
});
NSLog(@"执行任务 5");
答案:会,block1要求立马执行,但是block0还没有结束,所以死锁
怎么样不死锁:将block1放到另一个queue中
小总结:
==死锁==:使用 sync 往 当前 串行 队列中添加任务,会死锁
Lock 锁
-
OSSPinLock 自旋锁,等待锁的线程会处于忙等状态,一直占用CPU资源(高级锁,一直等),相当于while(是否加锁){}(iOS10以上弃用了,提示用os_unfair_lock代替)
- 目前已经不再安全,可能会出现优先级翻转问题
- 如果等待锁的线程优先级较高,它会一直占着CPU资源,优先级低的线程就无法释放锁
#import <libkern/OSAtomic.h> // 初始化 OSSpinLock lock = OS_SPINLOCK_INIT; // 尝试加锁(如果需要等待就不加锁, 直接返回 false,如果不需要等待就加锁,返回 true) // OSSpinLockTry(&lock) // 加锁 OSSpinLockLock(&lock); // 解锁 OSSpinLockUnlock(&lock);
-
os_unfair_lock 互斥锁,线程会处于休眠状态,并非忙等(低级锁,会休眠),用于取代OSSpinLock, 从iOS10开始
#import <os/lock.h> // 初始化 os_unfair_lock lock = OS_UNFAIR_LOCK_INIT; // 尝试加锁(如果需要等待就不加锁,直接返回 false,如果不需要等待就加锁,返回 true // os_unfair_lock_trylock(&lock) // 加锁 os_unfair_lock_lock(&lock); // 解锁 os_unfair_lock_unlock(&lock);
-
pthread_mutex 互斥锁,等待锁的线程会处于休眠状态(低级锁,会休眠)
- 普通锁(PTHREAD_MUTEX_DEFAULT)
- 递归锁(PTHREAD_MUTEX_RECURSIVE),允许同一个线程对一把锁进行重复加锁
- 错误锁,解决出错时使用,不常用
递归锁:
线程1 执行test(),加锁,继续 执行test(),加锁... 可以
线程2 执行test(),但是线程1已经加锁,只能等待#import <pthread.h> // 初始化 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; // 结构体只能在定义的时候进行复制,不能先定义,再赋值 // 或 { // 先初始化一个属性 pthread_mutexattr_t attr; pthread_mutexattr_init(&attr); pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT); pthread_mutex_init(&lock, &attr); // 初始化锁的时候带有属性,属性为NULL,则默认Default pthread_mutexattr_destroy(&attr); // 销毁属性 } // 尝试加锁(如果需要等待就不加锁,直接返回 false,如果不需要等待就加锁,返回 true // pthread_mutex_trylock(&lock) // 加锁 pthread_mutex_lock(&lock); // 解锁 pthread_mutex_unlock(&lock); // 销毁锁 pthread_mutex_destroy(&lock) // 条件锁 pthread_cond_init(&_cond, NULL); // 初始化一个条件 pthread_cond_wait(&_cond, &_lock); // 等待信号(休眠),释放锁,让其他线程先运行 pthread_cond_signal(&_cond); // 发信号 pthread_cond_broadcast(&_cond); // 发广播,对应多个线程等待 pthread_cond_destroy(&_cond); // 销毁条件
-
NSLock、NSRecursiveLock、NSCodition、NSCoditionLock
- NSLock 是对 mutex 普通锁的封装
- NSRecursiveLock 是对 mutex 的递归锁的封装
- NSCodition 是对 mutex 和 cond 的封装,
- NSCoditionLock 对 NSCodition,在线程中间等待 的进一步封装,在线程开始前等待,按顺序执行线程(线程同步)
-
dispatch_semaphore
- 线程同步
- 控制最大并发量
// 创建信号量为 5 的 semaphore dispatch_semaphore_t semaghore = dispatch_semaphore_create(5); // 如果信号量 > 0, 信号量 -1,继续执行 // 如果信号量 <= 0, 线程休眠等待 dispatch_semaphore_wait(semaghore, DISPATCH_TIME_FOREVER); // 信号量 +1 dispatch_semaphore_signal(semaghore);
-
@synchronized
- 线程同步
- synchronized 对 mutex 的封装,底层封装比较复杂,使用 HashMap,用对象作为 key,用封装 mutex 的对象作为 value,
- 递归锁,可以循环套用
加锁性能比较
性能 高 -> 低 os_unfair_lock - 互斥锁,等待时会休眠,iOS10 以上 OSSPinLock - 自旋锁,等待时不会休眠,不推荐使用,不安全 dispatch_semaphore - 信号量,线程同步,推荐使用 pthread_mutex - 互斥锁,等待时会休眠,推荐使用 dispatch_queue(SERIAL) - 线程同步 NSLock - 封装 mutex,面向对象 NSCodition - 封装 mutex-cond,面向对象 NSRecursiveLock - 递归锁,封装 mutex-PTHREAD_MUTEX_RECURSIVE,不推荐使用 NSCoditionLock - 条件锁,封装 mutex-PTHREAD_MUTEX_RECURSIVE,不推荐使用 @synchronized
推荐:pthread_mutex(跨平台,iOS所有版本都支持)、os_unfair_lock(iOS10以上)、dispatch_semaphore(iOS所有版本)
读写安全/多读单写
要求:
读写互斥
谢谢互斥
读读互斥
方案一:使用 pthread_rwlock 读写锁
#import <pthread.h>
@property (nonatomic, assign) pthread_rwlock_t lock;
// 初始化
pthread_rwlock_init(&_lock, NULL);
- (void)read {
pthread_rwlock_rdlock(&_lock);
//pthread_rwlock_tryrdlock(&_lock);
NSLog(@"%s", __func__);
sleep(2);
pthread_rwlock_unlock(&_lock);
}
- (void)write {
pthread_rwlock_wrlock(&_lock);
pthread_rwlockpthread_rwlock_rdlocksleep(1);
NSLog(@"%s", __func__);
sleep(3);
pthread_rwlock_unlock(&_lock);// 解锁
}
- (void)dealloc {
// 销毁读写锁
pthread_rwlockattr_destroy(&_lock);
}
方案二:使用 dispatch_barrier_async 异步栅栏调用
dispatch_barrier_async 同一个队列中,要求在执行barrier中的任务时,不允许其他任务执行
self. queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
- (void)read {
dispatch_sync(self.queue, ^{ // 这里可以同步也可以异步,看需求,如果有返回值就需要同步返回,如果直接在block中处理数据,可以使用异步
sleep(1);
NSLog(@"read");
});
}
- (void)write {
dispatch_barrier_async(self.queue, ^{
sleep(5);
NSLog(@"write");
});
}
面试题
1、你理解的多线程?
答案:
多线程是为了同步完成多项任务,不是为了提高运行效率,而是为了提高资源使用效率来提高系统的效率。多线程是在同一时间需要完成多项任务的时候实现的。
优点:
1)、可以加快程序的运行速度,因为用户界面可以在进行其它工作的同时一直处于活动状态
2)、可以把占据长时间的程序中的任务放到后台去处理
3)、当前没有进行处理的任务时可以将处理器时间让给其它任务
4)、可以并发执行多个任务,释放一些珍贵的资源如内存占用等等
5)、可以随时停止任务
6)、可以分别设置各个任务的优先级以优化性能
缺点:
1)、因为多线程需要开辟内存,而且线程切换需要时间因此会很消耗系统内存。
2)、线程的终止会对程序产生影响
3)、由于多个线程之间存在共享数据,因此容易出现线程死锁的情况
4)、对线程进行管理要求额外的CPU开销。线程的使用会给系统带来上下文切换的额外负担。
2、你在项目中用过 GCD 吗?
答案:
开启子线程,使用信号量,栅栏函数,timer等等,onetoken
3、GCD 的队列类型
答案:
并发队列
- 让多个任务同时执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步(dispatch_async)函数下才有效
串行队列
- 让任务一个接着一个的执行(一个任务执行完毕后,在执行下一个任务)
4、说一下 OperationQueue 和 GCD 的区别,以及各自的优势
答案:
GCD 是底层的C语言构成的 API,在队列中执行的是由block构成的任务,这是一个轻量级的数据结构
NSOperation 是对 GCD 的封装,更加面向对象,为我们提供了更多的选择,更简单的API入口
5、线程安全的处理手段有哪些?
答案:
@synchronized 关键字与 Lock 锁方式
synchronied 会自动释放锁;而 Lock 需手动为代码块加锁并释放锁
从性能上,Lock锁方式优于synchronized关键字
6、OC 你了解的锁有哪些?
答案:
os_unfair_lock - 互斥锁,等待时会休眠,iOS10 以上
OSSPinLock - 自旋锁,等待时不会休眠,不推荐使用
dispatch_semaphore - 信号量,线程同步,推荐使用
pthread_mutex - 互斥锁,等待时会休眠,推荐使用
dispatch_queue(SERIAL) - 线程同步
NSLock - 封装 mutex,面向对象
NSCodition - 封装 mutex-cond,面向对象
NSRecursiveLock - 递归锁,封装 mutex-PTHREAD_MUTEX_RECURSIVE,不推荐使用
NSCoditionLock - 条件锁,封装 mutex-PTHREAD_MUTEX_RECURSIVE,不推荐使用
@synchronized
追问一:自旋锁和互斥锁对比
答案:
自旋锁 - 等待锁的线程会处于忙等状态,一直占用 CPU 资源,如果等待锁的线程优先级较高,它会一直占着 CPU 资源,优先级低的线程就无法释放锁
互斥锁 - 线程会处于休眠状态
什么情况使用自旋锁?
预计线程等待锁的时间很短
加锁的代码(临界区)经常被调用,但竞争情况很少出现
CPU 资源不紧张
多核处理器
什么时候使用互斥锁?
预计线程等待锁的时间很长
单核处理器
临界区 IO 操作
临界区代码复杂或循环量大
临界区竞争非常激烈
7、打印结果
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
// test 方法实现 NSLog(@"2");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
});
答案:1,3
理由: performSelector:withObject:afterDelay: 的本质是往 RunLoop 中添加定时器, 而子线程默认没有启动 RunLoop