iOS多线程小结
iOS多线程小结
前言
在系统中,线程是程序真正的执行单元,并负责代码的执行。iOS中主要的技术方案有GCD、NSOperation、NSThread这三种。同时存在线程之间资源的访问、同步的问题,为了保证安全,就需要使用到锁
,本文就从GCD
、NSOperation
、NSThread
、多线程与锁
来展开讲解;
GCD
- 含义:GCD是Apple开发的一个多核编程的解决方法。主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。
- 主要有以下优点:
- GCD 可用于多核的并行运算
- GCD 会自动利用更多的 CPU 内核
- GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
- 程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
- 那么这里主要讲解以下几点:
- 同步/异步 和 串行/并发
- dispatch_barrier_async:异步栅栏调用,主要用来解决
多读单写
的问题 - dispatch_group
同步/异步和串行/并发
- 可以形成的组合:
- 同步串行:dispatch_sync(serial_queue, ^{//任务});
- 异步串行:dispatch_async(serial_queue, ^{//任务});
- 同步并发:dispatch_sync(concurrent_queue, ^{//任务});
- 异步并发:dispatch_async(concurrent_queue, ^{//任务});
同步串行
- 场景一:
- (void)viewDidLoad
{
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomething];
});
}
答案:上述代码会产生死锁
- 死锁原因:
- 场景二:
- (void)viewDidLoad
{
dispatch_sync(serial_queue, ^{
[self doSomething];
});
}
答案:没有问题
- 原因:
同步并发
- 场景
- (void)viewDidLoad
{
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 并发队列
NSLog(@"1");
dispatch_sync(global_queue, ^{
NSLog(@"2");
dispatch_sync(global_queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
答案:12345
异步串行
- 场景:
- (void)viewDidLoad
{
// 开发中频次最高的使用场景
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
}
异步并发
- 场景:
- (void)viewDidLoad
{
dispatch_queue_t global_queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 并发队列
dispatch_async(global_queue, ^{
NSLog(@"1");
[self performSelector:@selector(printLog) withObject:nil afterDelay:0];
NSLog(@"3");
});
}
- (void)printLog { NSLog(@"2"); }
答案:13
- 原因:
- 异步方式分派到全局并发队列,GCD底层维护的线程池中的某一个线程会去执行block;而线程池中的线程默认是不开启RunLoop的,而
performSelector:withObject:afterDelay
是需要提交到RunLoop上的,所以在上述场景,performSelector会失效。
- 异步方式分派到全局并发队列,GCD底层维护的线程池中的某一个线程会去执行block;而线程池中的线程默认是不开启RunLoop的,而
dispatch_barrier_async()
- 具体场景:怎样利用GCD实现多读单写?(或者:请设计一个多读单写模型)
- 需要以下特点
- 读者、读者并发
- 读者、写者互斥
- 写者、写者互斥
- 需要以下特点
- 实现:
- 解决方案:
dispatch_barrier_async(concurrent_queue, ^{//写操作});
// NSMutableDictionary *mutDic; // 数据容器
// 读操作
- (id)objectForKey:(NSString *)key
{
__block id obj;
// 同步读取数据:数据需要实时返回
dispatch_sync(concurrent_queue, ^{
obj = [mutDic objectForKey:key];
});
return obj;
}
// 写操作
- (void)setObject:(id)obj forKey:(NSString *)key
{
// 异步栅栏调用设置数据
dispatch_barrier_async(concurrent_queue, ^{
[mutDic setObject:obj forKey:key];
});
}
dispatch_group()
- 使用场景:使用GCD实现这个需求:A、B、C三个任务并发,完成后执行任务D?
- 解决方案:dispatch_group_async()
- 如:多图下载,拼合成一张图
NSArray *urls = @[@"1.jpg", @"2.jpg"];
dispatch_group_t group = dispatch_group_create();
for (NSString *path in urls) {
// 异步组分派s到并发队列中
dispatch_group_async(group, concurrent_group, ^{
// 根据图片url去下载图片
NSLog(@"path = %@", path);
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 当添加到组的所有任务执行完成后会调用改block
NSLog(@"所有图片已全部下载完成");
});
NSOperation
- NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。
- 有哪些优势和特点:
- 添加任务依赖:方便的控制任务执行顺序
任务执行状态控制
- 可以设置最大并发量
任务执行状态控制:有哪些?
-
isReady:是否处于就绪状态
-
isExecuting:是否处于执行中的状态
-
isFinished:是否已执行完成
-
isCancelled:是否已取消
状态控制:怎样控制NSOperation的任务状态
- 主要看我们是否重写了NSOperation的
start
和main
方法- 如果只重写
main
方法,那么底层控制变更任务执行完成状态,以及任务退出。 - 如果重写了
start
方法,则需要自行控制任务状态- 在源码的分析中,在start方法中,系统做了状态判断和控制,并通过KVO的方式,告知NSOperationQueue这个监听对象。
- 如果只重写
系统是怎样移除一个isFinished=YES的NSOperation的?
- 答案:通过KVO(会拓展KVO的了解)
NSThread
- 首先我们来看下NSThread的启动流程
- Q:那么NSThread的内部实现机制是怎样的(即start方法的实现过程)
- 在start方法中,会调用pthread_create()来创建线程,指定线程的启动函数
- 在线程的启动函数中,首先发送通知,线程已经启动了,再调用main方法,最后调用exit,关闭线程
-
在main中,会调用target中的selector方法,来执行创建Thread的时候指定的selector方法
- 可以通过NSThread的detachNewThreadSelector:toTarget:withObject来传递Target和selector方法;也可以通过初始化方法initWithTarget:selector:object来传递Target和selector方法;
- Q:怎样通过NSThread实现常驻线程?
- 为什么需要实现常驻线程?
- 由于每次开辟子线程都会消耗cpu,在需要频繁使用子线程的情况下,频繁开辟子线程会消耗大量的cpu,而且创建线程都是任务执行完成之后也就释放了,不能再次利用。常驻线程可以减少消耗cpu。
- 解决方案:
- 需要在创建NSThread的时候传递进来的selector方法中,去维护一个RunLoop事件循环
- 为什么需要实现常驻线程?
锁
- iOS当中都有哪些锁呢?(你在日常开发中都使用过哪些锁)
- @synchronized
- 业务场景:创建单例对象;NSMutableArray的赋值(多图上传)
- atomic :
- 属性关键字
- 对修饰的对象赋值操作是保证线程安全的,但对象的使用操作是不保证线程安全的;
- 如 @property(atomic) NSMutableArray *array;
- self.array = [NSMutableArray array]; 是线程安全的
- 如NSMutableArray的addObject; 就不是线程安全的
- OSSpinLock :自旋锁,特点如下:(在内存管理中,SideTable中有应用到)
- 循环等待询问,不释放当前资源;
- 用于轻量级数据访问
- NSLock
- 使用lock和unlock方法进行加锁和解锁操作
- 同一把锁,重入加锁,会导致
死锁
;- 解决方案:使用递归锁NSRecursiveLock,特点是可以重入
-
NSRecursiveLock:递归锁,特点是可以重入
- dispatch_semaphore_t:GCD信号量,主要考察线程同步的理解,来解决一些线程同步问题
- @synchronized
- NSLock死锁场景:
- 解决方案:
dispatch_semaphore_t
- 相关的实现方法:
- dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 创建信号量
- dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 等待信号量
- dispatch_semaphore_signal(semaphore); // 发送信号量
内部实现
- dispatch_semaphore_create
- semaphore的结构
struct semaphore { int value; List <thread>; }
- semaphore的结构
- dispatch_semaphore_signal
{
S.value = S.value + 1;
if S.value <= 0 then wakeup(S.List); // 若小于等于0意味着在释放钱有队列在排队,列表当中有线程需要唤醒;唤醒是一个被动行为
}
- dispatch_semaphore_wait
{
S.value = S.value - 1;
if S.value < 0 then Block(S.List); // 小于0意味着我们不能获取信号量,当前的线程主动阻塞起来。阻塞是一个主动行为
}
多线程总结
- 怎样用GCD实现多读单写?(dispatch_barrier_async)
- iOS系统为我们提供的几种多线程技术各自的特点是怎样的?(结合业务场景)
- GCD:一般在简单的线程同步,子线程的分派,资源同步的场景使用
- NSOperation:在AFN、SDWebImage的内部都有实现,主要是考虑其可以添加任务依赖,控制状态
- NSThread体现在常驻线程的使用中
- NSOperation对象在Finished之后是怎样从queue当中移除掉的?(通过KVO通知queue)
- 你都用过哪些锁?结合实际谈谈你是怎样使用的?(根据实际业务场景,可举例NSLock死锁问题)
参考文章
最后
如果对大家有帮助,请github上follow和star,本文发布在戴超的技术博客,转载请注明出处