iOS 常见的多线程方案
GCD 的用法
重要概念区别
1,确定是同步(sync)还是异步(async) 同步有能力开启新的线程,异步不能够开启新线程 2,确定是并发队列还是串行队列 队列一共分两种串行(主队列是串行的一种)和并行。 常见的用法, 开辟一个同步的并发线程。 回到主线程刷新UI
GCD 造成的死循环(死循环的原因在于sync同步要求立即在当前线程执行,而串行队列的特点是先进先出,并发队列一般不造成死锁)
简单死循环
- (void)interView {
// 问题:以下代码是在主线程执行的,会不会产生死锁?不会!
NSLog(@"执行任务1");
dispatch_queue_t queue = dispatch_get_main_queue();
// 立即在当前线程执行任务,执行完成才能继续往下执行
dispatch_sync(queue, ^{
NSLog(@"执行任务2");
});
NSLog(@"执行任务3");
// dispatch_async不要求立马在当前线程同步执行任务
}
1,任务2是要添加的主线程执行的。dispatch_sync 执行完本任务才能往下走。 2,队列的特点:FIFO first in first out 3,任务2是放在队列里面的,需要等队列里的任务执行完。主队列已经有任务了,就是任务1,3 。 4,死锁原因,任务2在等任务3,因为队列。任务3在等任务2,因为dispathc_sync 。
复杂死循环
dispatch_async(queue, ^{//blcok0
NSLog(@"执行任务2");
dispatch_sync(queue, ^{//blcok1
NSLog(@"执行任务3");
});
NSLog(@"执行任务4");
});
1,任务3要求立即执行,但是队列中还有任务4没有执行。 2,blcok0 先进入队列还没执行完,blcok1就要求立即执行。 3,任务3要等任务4,任务4要等任务3.
GCD 中的队列组(多个队列顺序执行)
// 创建队列组
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]);
}
});
});
GUstep
GNUstep是GNU计划的项目之一,它将Cocoa的OC库重新开源实现了一遍
源码地址:www.gnustep.org/resources/d…
虽然GNUstep不是苹果官方源码,但还是具有一定的参考价值
多线程的安全隐患
1,单线程为什么么没有安全隐患,因为主线程是串行执行的,数据时安全的。 2,多线程,是异步并发处理的,有可能存在同时操作一组数据的情况。(特别是有些异步操作时耗时的,拿到数据后要操作一段时间,此时别的线程拿到的又是老数据。) 例子:1000张票同时在2个窗口买,2个窗口有可能卖同一张票。
如何解决线程安全隐患(使用线程同步技术)常见的技术:加锁
OSSpinLock (已不安全)
1,创建一个全局的锁
2,对异步操作的代码加锁
3,OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源
目前已经不再安全,可能会出现优先级反转问题
如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。低优先级的任务先进入锁住线程,高优先级的任务进来后,会让线程进入忙等状态,阻塞了优先级低的任务的执行。 需要导入头文件#import <libkern/OSAtomic.h>
os_unfair_lock
1,引入头文件 #import <os/lock.h> 2,加锁,解锁,销毁。
pthread_mutex_t (互斥锁”,等待锁的线程会处于休眠状态)
1,引入头文件#import <pthread.h> 2,加锁,解锁,销毁。
pthread_mutex_t 递归锁
递归锁的概念:允许同一个线程对一把锁进行重复加锁
pthread_mutex – 条件
让线程处于休眠状态,激活一个等待的线程,激活所有等待该条件的线程
NSLock、NSRecursiveLock (更加符合OC习惯)
NSLock是对mutex普通锁的封装 NSRecursiveLock也是对mutex递归锁的封装,API跟NSLock基本一致
NSCondition (符合OC习惯,对mutex和条件的封装)
1,signal 条件触发,
2,wait休眠的锁已经被解开。
NSConditionLock(NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值)
dispatch_semaphore()
1,semaphore叫做”信号量” 2,信号量的初始值,可以用来控制线程并发访问的最大数量 3,信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步(相当于加锁,和加锁的本质是一样的)
dispatch_queue
直接使用GCD的串行队列,也是可以实现线程同步的
@synchronized (最简单的方法,但是性能并不高)
1,@synchronized是对mutex递归锁的封装
2,源码查看:objc4中的objc-sync.mm文件
3,@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
@synchronized(obj){//任务}
锁的 性能从高到低排序
os_unfair_lock OSSpinLock dispatch_semaphore pthread_mutex dispatch_queue(DISPATCH_QUEUE_SERIAL) NSLock NSCondition pthread_mutex(recursive) NSRecursiveLock NSConditionLock @synchronized
自旋锁、互斥锁比较
- 什么情况使用自旋锁比较划算?
预计线程等待锁的时间很短 加锁的代码(临界区)经常被调用,但竞争情况很少发生 CPU资源不紧张 多核处理器
- 什么情况使用互斥锁比较划算?
预计线程等待锁的时间较长 单核处理器 临界区有IO操作 临界区代码复杂或者循环量大 临界区竞争非常激烈
多线程读写安全
atomic
可以参考源码objc4的objc-accessors.mm
1,atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
2,它并不能保证使用属性的过程是线程安全的
比如创建一个数组,数组的set和get方法是线程安全的,但是往数组中添加和移除元素时线程不安全的。
3,在手机上使用是极其消耗性能的.
iOS 中的读写安全方案
多读单写
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许既有写的操作,又有读的操作
经常用于文件等数据的读写操作,iOS中的实现方案有
pthread_rwlock:读写锁 等待锁的线程会进入休眠
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
// [self barrier];
// 初始化锁
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:异步栅栏调用 1,这个函数传入的并发队列必须是自己通过dispatch_queue_cretate创建的
2,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async函数的效果
面试题
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");
打印结果是:1、3 原因 performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器 子线程默认没有启动Runloop
- (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];
}
程序会崩溃,因为thread 执行完线程就会退出。已经没有执行任务的能力了(thread并没有被立即销毁。)
解决方法,在线程中添加runloop,
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];
小结
GCD的常见用法
各种队列的执行效果
2个重要概念,并发和串行,异步和同步。
2,只有异步的并发队列,才能并发的执行任务。
gcd 造成的死锁的原因
使用sync函数往当前串行队列中添加任务,会卡住当前的串行队列(产生死锁)
- sync 要求在本任务立即执行。
- 串行队列要求先执行sync任务前的任务。
串行队列的特点和同步执行的特点导致的。
performSelector:withObject:afterDelay:的本质是往Runloop中添加定时器
子线程默认没有启动Runloop。
CGD的队列组
多线程的安全隐患
多个线程同时访问一组数据。
解决多线程安全的方法(加锁)
自旋锁和互斥锁
底层锁和高级锁,及OC锁
iOS中的读写安全
多读单写(写和读也不能同时)