iOS多线程
iOS中多线程的方案?有什么优缺点?
技术方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
---|---|---|---|---|
pthread | 1.一套通用的多线程API 2.跨平台\可移植 3.使用难度大 4.适用于Unix\Linux\Windows | C语言 | 程序员管理 | 几乎不用 |
NSThread | 1.使用更加面向对象 2.简单易用,可直接操作线程对象 | OC语言 | 程序员管理 | 偶尔使用 |
GCD | 1.旨在替代NSThread等线程技术 2.重复利用设备多核 | C | 自动管理 | 经常使用 |
NSOperation | 1.基于GCD(底层是GCD) 2.比GCD多了一些更简单实用的功能 3.使用更加面向对象 | OC | 自动管理 | 经常使用 |
NSThread 、GCD 、NSOperation底层都是依赖于pthread
GCD
队列
- 并发队列
- 可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
- 并发功能只有在异步dispatch_async函数中才有效
- 串行队列
- 让任务一个接一个的执行(一个任务执行完毕后,在执行下一个任务)
GCD多线程中使用dispatch_sync(同步)会在当前线程中执行,如果当前线程是主线程就会在主线程执行
GCD多线程中使用dispatch_async(异步)会在子线程中执行;
不管你是串行队列DISPATCH_QUEUE_SERIAL或者是并发队列DISPATCH_QUEUE_CONCURRENT 在同步sync中都是顺序执行
并发功能只有在异步(dispatch_async)函数中中才有效
- 同步
- 异步
容易混淆的术语
有四个比如术语比较容易混淆 同步 异步 并发 串行
- 同步和异步主要是影响:能不能开启新的线程
- 同步:当前线程中执行任务,不具备开启新线程的能力
- 异步:在新的线程中执行任务,具备开启新线程的能力
- 并发和串行主要影响:任务的执行方式
- 并发:多个任务并发(同时)执行
- 串行:一个任务执行完毕后,在执行下一个任务
固使用sync和async 只影响你在哪个线程执行 不在乎你是并发还是串行
队列的类型,决定了任务的执行方式(并发还是串行),列队是不能决定在哪个线程执行的,队列是决定任务是一个一个执行还是多个同时执行
是不是所有的异步async都会开启线程?
如果你的任务是放在主队列dispatch_get_main_queue(),主队列是一种特殊的串行队列
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_async(queue, ^{
});
各种队列的执行效果?
并发队列 | 手动创建的串行队列 | 主队列 | |
---|---|---|---|
同步 | 1.不会开始新的线程 2.串行执行任务 | 1.不会开启新的线程 2.串行执行任务 | 1.不会开启新的线程 2.串行执行任务 |
异步 | 1、会开启新的线程 2.并发执行任务 | 1、会开启新的线程 2.串行执行任务 | 1.不会开启新的线程 2.串行执行任务 |
01下面代码会不会产生死锁?
-(void)viewDidLoad{
[super viewDidLoad];
NSLog(@"任务一");
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{
NSLog(@"任务二");
});
NSLog(@"任务三");
}
原因:任务二是添加到主队列,意味着马上马上就要执行,以为这 dispatch_sync(queue, ^{
NSLog(@"任务二");
});添加到主线程执行,而且是同步函数意味着必须在当前线程执行,因为queue是个队列 首先他就具备FIFO的特性 ,先进先出,哪个任务先进来就先执行哪个任务,现在是将任务二放到主队列里面去,dispatch_sync是立马在当前线程执行任务,执行完毕才能继续向下执行
1、因为任务二是同步执行,同步执行意味着在当前线程执行任务二,必须在当前线程要执行完任务二才能往下走执行任务三, dispatch_sync(queue, ^{
NSLog(@"任务二");
});才有返回值,不然会一直卡在任务二,所以要想执行任务三必须要等任务二执行完毕,但是要执行完dispatch_sync(queue, ^{
NSLog(@"任务二");
});必须要等到上一个任务执行完,因为这个任务是放在主队列dispatch_get_main_queue中,也就意味着你viewDidLoad函数要执行完,也就是说你需要把 NSLog(@"任务三");这句执行完这个任务二才会执行,所以你要想执行任务二必须要把后面的NSLog(@"任务三");执行完,但是你要想执行NSLog(@"任务三");又得让dispatch_sync(queue, ^{
NSLog(@"任务二");
});这句代码过掉,dispatch_sync(queue, ^{
NSLog(@"任务二");
});这句代码要想过掉还要把任务二执行完才行,要想执行完还得保证viewDidLoad里面的函数执行完,总结就是任务二在等任务三执行完,任务三在等任务二
首先主线程这时候分别有 任务一 ,sync,任务3,因为任务二是在当前线程添加到主队列里的所以
主队列里有viewDidload函数、任务二。此时执行到sync函数会从主队列去取出任务二,但是主队列中任务二上方的任务是viewDidload,根据队列的先进先出原则,取出任务二去线程执行的前提需要是viewDidload先从出队列,但是viewDidload的出队需要先在主线程中执行完任务三,然后主线程中想要执行任务三需要先执行完sync,sync需要拿到任务二才能执行,这样就造成了死锁 主要是因为串行队列你必须要前面的任务执行完才能执行后面的任务
02下面代码会不会产生死锁?
- (void)interview03
{
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{ // 0 block
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1 block
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
会造成死锁 首先dispatch_async开启任务二,在串行队列里添加block 0,然后执行dispatch_sync立马在当前线程的串行队列中添加block 1来执行任务3,但是串行队列中先有block 0, dispatch_sync想要执行任务3 需要先执行完block0,也就是 block 0中NSLog(@"执行任务4"); 但是block 0中 NSLog(@"执行任务4")执行需要等到dispatch_sync函数执行完。所以就产生死锁
03下面代码会不会产生死锁?
- (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 block
NSLog(@"执行任务2");
dispatch_sync(queue2, ^{ // 1 block
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
原因分析:首先从dispatch_async开启子线程 这是block 0添加到queue 串行队列中 然后执行dispatch_sync 添加任务到queue2另一个队列中 所以执行任务3只需要在queue2队列中拿出block 1 NSLog(@"执行任务3")即可,不影响任务4执行 函数的打印顺序是
执行任务1、执行任务5、执行任务2、 执行任务3、执行任务4;
04 下面代码会不会产生死锁?
- (void)interview05
{
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_queue_create("myqueu", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{ // 0 block
NSLog(@"执行任务2");
dispatch_sync(queue, ^{ // 1 block
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
NSLog(@"执行任务5");
}
原因分析:因为queue是并发队列,首先记住一点 并发队列中的任务是可以同时执行多个的 所以这种情况并不会造成死锁
多线程中产生死锁场景?
使用sync函数王当前串行队列中添加任务 ,会卡住当前的(同一个)串行队列(产生死锁)
以下代码打印会输出什么结果?
1.例
- (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");
});
}
- (void)test
{
NSLog(@"2");
}
回答:打印结果是 1,3 。
2.例
- (void)test2
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil];
NSLog(@"3");
});
}
- (void)test
{
NSLog(@"2");
}
这种打印结果就是1,2,3
同样
3.例
- (void)test2
{
NSLog(@"1");
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
}
- (void)test
{
NSLog(@"2");
}
这样的打印结果是1、3、2
那么为什么加了 afterDelay:.0就打印不出2了呢? 因为[self performSelector:@selector(test) withObject:nil afterDelay:.0];这个方法是放在runloop中的 带有afterDelay方法的一般都在runloop中,这个方法的底层其实用到了NSTimer定时器,之所以上面这句能打印出来2 是因为在我们的主线程中是有runloop的。
这就解释了1例中为什么没有打印2 ,因为子线程默认是没有开启runloop的,那就意味着他的定时器是没法工作的 所以 1例中可以改成如下代码
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"1");
// 这句代码的本质是往Runloop中添加定时器
[self performSelector:@selector(test) withObject:nil afterDelay:.0];
NSLog(@"3");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
请问下面这道题打印结果是什么?
- (void)test
{
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{ //0 block
NSLog(@"1");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
答案是打印完1 之后直接崩溃,首先执行到[thread start]会去子线程打印 1,然后performSelector:@selector(test) onThread:thread 也在子线程执行test函数,子线程无法执行完
NSLog(@"1");同时执行test方法 ,这时候执行完0 block后子线程就退出了 这时候在performSelector:@selector(test) onThread:thread执行 线程已经退出了 所以就会崩溃;
所以下面代码需要修改为
- (void)test
{
NSLog(@"2");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc] initWithBlock:^{//0 block
NSLog(@"1");
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];//启动runloop
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
这样做执行完 0 block后线程不会马上退出 runloop会等待下一次的唤醒 这样就可以打印1,2. 这里面的 onThread:thread 相当于唤醒子线程中的runloop
队列组的使用
思考:如何用GCD实现以下功能?
- 异步并发执行任务1、任务2
- 等任务1、任务2都执行完毕后,再回到主线程执行任务3
主要用到了 dispatch_group
// 创建队列组
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]);
}
});
多线程安全隐患的解决方案?
解决方案:使用线程同步技术(同步 就是协同步调,an预定的先后次序进行)
常见的线程同步技术:加锁
加锁的注意事项 加完锁要记得解锁,如果不解锁别的线程就没法更改数据
iOS中线程同步方案有哪些?
- OSSpinlock
- os_unfair_lock
- pthread_mutex
- dispatch_semaphore 信号量
- dispatch_queue(DISPATCH_QUEUE_SERIAL)串行队列
- NSLock
- NSRecursiveLock 递归锁
- NSCondition
- NSConditionLock
- @synchronized
OSSpinlock
OSSpinlock 从iOS 10中过期 因为会出现优先级反转的问题
- 目前已经不再安全,可能会出现优先级反转问题
- 如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁
使用方法 导入头文件 #import <libkern/OSAtomic.h>
创建 OSSpinLock lock = OS_SPINLOCK_INIT;
加锁:OSSpinLockLock(&传一个地址)
解锁:OSSpinLockUnlock(&传一个地址)
os_unfair_lock
这种是自旋锁,就是一直在自,等待,是一个low-level lock 也就是一把低级锁,低级锁的特点就是等不到就开始休眠
os_unfair_lock用于取代不安全的OSSpinlock,从iOS10才开始支持
- 从底层上看,等待os_unfair_lock锁的线程会处于休眠状态,并非忙等
- 需要导入头文件#import <os/lock.h>
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//尝试加锁
os_unfair_lock_trylock(&lock);
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);
pthread mutex
像这种pthread开头的一般都是跨平台的可以支持Linux、 iOS的macOS、 windows都支持
mutex叫做互斥锁 ,等待锁的线程会处于休眠状态,互斥锁在等待的过程中是在休眠,这一点跟自旋锁不同
导入头文件 #import <pthread.h>
//静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
///下面这个属性是递归锁
//pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_init(mutex, NULL);
// 销毁属性
pthread_mutexattr_destroy(&attr);
pthread_cond是pthread_mutex锁的条件
递归锁
递归锁 允许重复加锁,加锁和解锁的次数相同,他是允许同一个线程对同一把锁进行重复加锁,如果线程一已经对进行加锁,线程二访问的时候也想进行加锁,这时候线程2只能等待,而不能进行加锁。除非这把锁已经被解开。
NSCondition
NSCondition是对mutex和cond的封装
NSConditionLock
条件锁 ,NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值
NSLock
NSLock是对mutex普通锁的封装
NSLock的底层就是pthread mutex
NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
@interface NSLock : NSObject <NSLocking> {
- (BOOL)tryLock;
///在这个limit时间之前能等到这把锁放开的话就给这把锁加锁,如果时间没到我就会一直等 处于堵塞睡眠的状态
- (BOOL)lockBeforeDate:(NSDate *)limit;
@protocol NSLocking
- (void)lock;
- (void)unlock;
@end
NSCondition是对mutex和cond的封装
dispatch_queue(DISPATCH_QUEUE_SERIAL)
串行队列可以保证按顺序执行,线程同步的本质就是不能让一条线程同时访问一个资源,
dispatch_semaphore
semaphore叫做信号量
- 信号量的初始值,可以用来控制线程并发访问的最大数量
- 信号量初始值为1,代表同时只允许1条线程访问资源,保证线程同步;
初始值设为1,就是同时允许1个线程来进行操作,写5就是5条线程同时操作
其实最大并发量也可以用
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
queue.maxConcurrentOperationCount = 5;
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
@synchronized
是对mutex递归锁的封装,苹果不推荐使用,因为性能比较差
@synchronized (objc) { ///objc是锁对象,相当于拿某个对象当锁
//任务
}
线程同步方案对比?
性能从高到低
os_unfair_lock
OSSpinLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutex(recursive)
NSRecursiveLock
NSConditionLock
@synchronized
推荐大家使用dispatch_semaphore 信号量和pthread_mutex,虽然pthread_mutex是C语言的,但是为了性能建议使用
自旋锁、互斥锁比较
什么情况用自旋锁比较划算?
- 预计线程等待时间很短
- 加锁的代码(临界区)经常被调用,但竞争情况很少发生
- CPU资源不紧张
- 多核处理器
什么情况使用互斥锁比较划算?
- 预计线程等待锁时间较长
- 单核处理器
- 临界区有IO操作
- 临界区代码复杂或者循环量大
- 临界区竞争非常激烈
其实在iOS中自旋锁已经不推荐使用了,其实基本就是互斥锁
atomic
atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁,原子性操作也就是保证了setter、getter内部是线程同步的,在物理学中原子就是最小的单位,意味着不可分割(现在研究原子中是由夸克构成)
-
例如
@interface MJPerson : NSObject @property (assign, nonatomic) int age; @property (copy, atomic) NSString *name; @property (strong, atomic) NSMutableArray *data; @end MJPerson *p = [[MJPerson alloc] init]; p.data = [NSMutableArray array]; //其实 p.data = [NSMutableArray array];也可以写为 [p setData:[NSMutableArray array]]; .语法就是set
atomic 一定是线程安全的吗?
NSMutableArray *array = p.data;
// 加锁
[array addObject:@"1"];
[array addObject:@"2"];
[array addObject:@"3"];
虽然data是线程安全的 但是[array addObject:@"1"];这个方法并不是线程安全,也就是说他可以保证set 和get的线程安全 但是像上面这种方法并不能保证
为什么iOS中不使用atomic
因为在iOS中setter和getter方法经常调用,使用atomic频繁加锁解锁会耗性能,因为iOS设备本身占内存有限,所以大量消耗CPU资源是非常的浪费的
多线程iOS的读写安全
IO操作,文件操作
从文件中读取内容
-
往文件中写入内容
不能同时进行
iOS中的多读单写
“多读单写”,经常用于文件等数据的读写操作,iOS中的实现方案有
pthread_rwlock:读写锁
dispatch_barrier_async:异步栅栏调用
pthread_rwlock
等待锁的线程会进入休眠 互斥锁
@property (assign, nonatomic) pthread_rwlock_t lock;
- (void)viewDidLoad {
[super viewDidLoad];
// 初始化锁
pthread_rwlock_init(&_lock, NULL);
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 10; i++) {
dispatch_async(queue, ^{
[self read];
});
dispatch_async(queue, ^{
[self write];
});
}
}
- (void)read {
pthread_rwlock_rdlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)write
{
pthread_rwlock_wrlock(&_lock);
sleep(1);
NSLog(@"%s", __func__);
pthread_rwlock_unlock(&_lock);
}
- (void)dealloc
{
pthread_rwlock_destroy(&_lock);
}
dispatch_barrier_async
这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
#import <pthread.h>
@interface ViewController ()
@property (strong, nonatomic) dispatch_queue_t queue;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// NSOperationQueue *queue = [[NSOperationQueue alloc] init];
// queue.maxConcurrentOperationCount = 5;
// dispatch_semaphore_create(5);
self.queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 10; i++) {
dispatch_async(self.queue, ^{
[self read];
});
dispatch_async(self.queue, ^{
[self read];
});
dispatch_async(self.queue, ^{
[self read];
});
dispatch_barrier_async(self.queue, ^{
[self write];
});
}
}
- (void)read {
sleep(1);
NSLog(@"read");
}
- (void)write
{
sleep(1);
NSLog(@"write");
}