iOS多线程学习笔记

时间:2023-01-23 16:51:54

iOS多线程

“iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。”–KenShinCui


1. NSThread

优点

NSThread 比其他两个轻量级

缺点

难控制线程顺序,线程总数也无法控制。需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销.并且可能会导致大量的bug.创建和维护的成本很高(引入少量或中量长期存在的线程,执行重量级的操作,比如:网络操作,数据库操作)

常用API

1)-currentThread// 获取当前线程
2)sleepForTimeInterval // 让当前线程休眠
3)简单的多线程可以直接使用NSObject的扩展方法;
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg:在后台执行一个操作,本质就是重新创建一个线程执行当前方法。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait:在指定的线程上执行一个方法,需要用户创建一个线程对象。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:在主线程上执行一个方法(前面已经使用过)。

多个线程按顺序启动,但实际执行未必按顺序加载照片:
1. 因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度。(虽然是按顺序启动,并不一定按顺序执行);
2. 每个线程执行时实际网络状况很可能不一致。
可以改变线程的优先级,线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。
改变最后一张图片加载的优先级,这样可以提高它被优先加载的几率,但是它也未必就第一个加载。因为首先其他线程是先启动的,其次网络状况我们没办法修改
Demo:倒计时


2. GCD

Grand Central Dispatch (GCD)是苹果开发的一个多核编程的解决方法。–唐巧

iOS4.0推出GCD,底层是C语言实现的。

优点

串行队列易于控制执行顺序,并发队列更高效
时间以纳秒为单位。

获取队列

串行队列

1)串行队列【一个线程】(先进先执行)serial queues一般用于按顺序同步访问,可创建任意数量的串行队列,各个串行队列之间是并发的。

dispatch_queue_t serialQueue = dispatch_queue_create("sequence", DISPATCH_QUEUE_SERIAL);

2)运行在主线程的Main queue,通过dispatch_get_main_queue获取。主队列其实就是系统默认的串行队列。

并行队列

1) 并行队列【多个线程】并行队列的执行顺序与其加入队列的顺序相同。

dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrency", DISPATCH_QUEUE_CONCURRENT);

2) 通过dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)获取,由系统创建三个不同优先级的dispatch queue以及后台队列。系统提供的队列如下图所示:

#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高优先级全局队列
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0// 默认的全局队列
#define DISPATCH_QUEUE_PRIORITY_LOW (-2)// 低优先级全局队列
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN// 后台全局队列

基本操作

调度队列: dispatch_async 异步执行, dispatch_sync 同步执行。
控制变量的保留和释放: dispatch_retain和dispatch_release
暂停/恢复: dispatch_susupend/dispatch_resume
延迟操作dispatch_after;
调度组dispatch_group
分派屏障(dispatch barrier) 在并发队列内部创建一个同步点,运行时,即使有并发的条件和空闲的处理器核心,队列中的其它块也不能运行.没有屏障的可以看做共享(读取)锁.
@synchronize 管理多线程访问,会在参数上加一个互斥锁.  易用,但当竞争很少时成本很高


3. 操作队列NSOperationQueue

NSOperationQueue VS GCD

  1. 不像GCD一样遵循先进先出,相应的支持设置优先级与依赖关系,也就是可以设置执行完A操作后才开始执行B操作。
  2. 也不像GCD有串行队列,如果要实现相应的功能,可以通过设置最大并发量为1或者将队列中的操作放入一个工作区中,设置依赖关系从而实现。

优点

可控制线程总数与线程依赖关系,相比于GCD效率更高,并可监听线程完成状态。不需要关心线程管理,数据同步[信号量,互斥锁机制]的事情,可以把精力放在自己需要执行的操作上。

基本API

Cocoa operation 相关的类是 NSOperation(操作,任务) ,NSOperationQueue(分派队列)。

队列

两种类型:主线程的队列与自定义的队列

// 主线程的队列,优先级最高
NSOpeationQueue *mainQueue = [NSOperationQueue mainQueue];
// 自定义队列,优先级为NSOperationQualityOfServiceBackground
NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];

创建操作

NSOperation有两个常用子类用于创建线程操作:NSInvocationOperation和NSBlockOperation,两种方式本质没有区别,但是是后者使用Block形式进行代码组织,使用相对方便。

// m1: NSInvocationOperation
NSInvocationOperation *invocationOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadImage) object:nil];
// m2:NSBlockOperation
NSBlockOperation *blockOp = [NSBlockOperation blockOperationWithBlock:^{
[self loadImage];
}];

添加依赖

添加操作依赖的注意点:
1.一定要在将操作添加到操作队列中之前添加操作依赖
2.不要添加循环依赖

4.锁机制

多个线程并发执行,可能出现同时访问同一资源,造成资源抢夺。
解决方法:

1. NSLock同步锁.

可以通过tryLock方法,此方法会返回一个BOOL型的值,如果为YES说明获取锁成功,否则失败。另外还有一个lockBeforeData:方法指定在某个时间内获取锁,同样返回一个BOOL值,如果在这个时间内加锁成功则返回YES,失败则返回NO。–from cuijiangtao

demo:

_lock = [[NSLock alloc] init];
[_lock lock];
// 关键部分
[_lock unlock];

2. @synchronized代码块

demo:

@synchronized(_myLockObj) {
// 关键部分
}

3. GCD中dispatch_semaphore_t信号量

demo:

_semaphore=dispatch_semaphore_create(1);
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
// 关键部分
dispatch_semaphore_signal(_semaphore);//信号通知

小结

GCD在实现例如停止加入queue的block的功能时,要写复杂的代码。
NSOperation iOS2.0推出,推出GCD以后,对底层进行了重写,基于GCD进行封装。可以取消准备执行的操作,操作依赖,设置优先级,
设置最大操作并发数;

最佳实践

苹果在推出更高抽象层级的API时并没有deprecated原先的API ,也就是表明不同的API是有相应的使用场景以及不同的限制。NSThread目前还不是很清楚它独有的使用场景。普通且简单的功能推荐使用GCD,但复杂一点的功能可能GCD就无法胜任了。NSOperationQueue对线程的依赖关系与队列中并发数目的控制等方面更为优秀,推荐使用NSOperationQueue。
r: 万事不离根本,单单会简单的使用多线程还是不够的。还是好好学习并发编程的基础,唯有掌握基础,才有可能真正理解琢磨透解决方案其内在机制。

并发编程

对象是过程的抽象,线程是调度的抽象 – James O Coplien

概述

并发是目的与时机的解耦,改进程序的吞吐量和结构。单线程模式下很多时候都在等待I/O,但并发不总是可以改进性能,因为开启多线程本身在性能上就有开销。 并发使用过程中的很多时候的问题根源是对共享数据或资源的使用。接下来会介绍下并发编程中一些常见的基础定义,如互斥,线程饥饿,死锁,活锁与限定资源。

基础定义

  1. 限定资源:
    比如数据库连接和固定尺寸读/写缓存。
  2. 互斥:
    每一时刻仅有一个线程访问共享数据或资源
  3. 线程饥饿:
    一个或一组现场在很长时间内或永久被禁止。 比如,总是让执行得快的线程先运行,假如执行得快的线程没完没了,那执行得久的线程就会“挨饿”
  4. 死锁:
    两个或多个线程互相等待执行结束。 每个线程都拥有其他线程需要的资源, 得不到其他线程拥有的资源就无法终止。
  5. 活锁:
    执行次序一致的线程,每个都想要开始执行,但发现其他线程已经“在路上”。 由于竞争的缘故,线程会持续起步,但很长时间内都无法如愿,甚至永远无法启动。

基础算法[TODO]

  1. 生产者-消费者模型: 生产者和消费者之间的队列是一种限定资源。
  2. 读者-作者模型:
  3. 宴席哲学家:

并发防御原则

  1. 单一权责原则(SRP) :
    并发相关代码有自己的开发,修改和调优生命周期; 与非并发代码不同有自己要对付的挑战。 建议–分离并发相关代码与其他代码。
  2. 限制数据作用域:
    两个线程修改共享对象的同一字段,可能互相干扰。 解决方案之一就是使用synchronized 解决资源抢夺问题 建议–谨记数据封装,严格限制对可能被共享的数据的访问。
  3. 使用数据复本:
    避免共享数据的好办法之一就是一开始避免共享数据。
  4. 线程应尽可能独立,不与其他线程共享数据。
    建议—尝试将数据分解到可被独立线程操作的独立子集。

小结

并发测试:编写有潜力暴露问题的测试,偶发问题需在不同的编程配置,系统配置和负载条件下频繁执行才更容易捕获,所以应编写可插拔的代码。 主要通过硬编码与自动化(使用异动策略,让线程以不同次序执行)装载试错代码。在编写调试过程中,不要同时追踪非线程缺陷和线程缺陷,应在编写并发代码前确保线程之外的代码可工作。
锁: 共享资源的读写导致大多数的并发问题,应锁定临界区域,但不要锁定不必锁定的代码。 也要避免在锁定区域调用其他锁定区域带阿妹。 尽可能减少锁定区域范围(临界区)。毕竟锁也是会带来延迟和额外的开销。

API

操作队列

NSOperationQueue

1) 队列有名字,并且队列有类型(比如通过类方法获取到主线程中的队列则为NSQualityOfServiceUserInteractive优先级是最高的,自己创建的队列默认为NSOperationQualityOfServiceBackground。如果操作对象NSOperation有设置执行类型,则会替换默认的优先级类型)。
2)提供了两个实例方法,用于获取当前线程中的队列以及主线程中的队列。
3)队列中的操作: 提供添加操作的接口,支持添加一个操作,block方式添加操作,批量添加操作(此时要考虑是否可以阻塞当前线程执行完这些操作,该功能也开放一个接口实现该功能);也提供属性获取操作数组,操作数目,并且可以设置最大的并发操作数目。
4)队列的状态:队列可以挂起也就是暂停(没有resume属性,我怀疑是通过将suspend属性设为NO就是代表恢复操作执行),也可以取消所有的操作。

@interface NSOperationQueue : NSObject
- (void)addOperation:(NSOperation *)op;// 添加一个操作
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);// 添加多个操作,wait为YES表示阻塞当前线程直到ops操作完成。
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);// 通过block方式添加操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;// 加入执行中的操作,操作完成会从该数组中移除
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);// 操作数目
@property NSInteger maxConcurrentOperationCount;// 最大并发操作数目
@property (getter=isSuspended) BOOL suspended;// 挂起
@property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);// 操作队列的名称
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);// 队列的优先级

@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);//

- (void)cancelAllOperations;// 取消所有操作
- (void)waitUntilAllOperationsAreFinished;// 主线程等待所有操作完成
+ (nullable NSOperationQueue *)currentQueue NS_AVAILABLE(10_6, 4_0);// 实例方法获取当前队列
+ (NSOperationQueue *)mainQueue NS_AVAILABLE(10_6, 4_0);// 实例方法获取主线程中的队列
@end

NSOperation

1) 可以设置依赖也可以移除依赖
2) 设置执行的优先级。
3) 调用cancel可以取消操作,如果操作已经完成,则取消方法没有效果,cancelled为false,操作正在执行,系统不会强制停止执行代码,但会将cancelled设为true;操作在队列中等待,则不会执行该操作,并把cancelled设为true。
4) 操作状态: cancelled是否取消,executing是否正在执行,finished是否执行完毕, ready是否准备好执行。
5) 执行完(finished属性为true时),还可以设置回调completionBlock。
6) 可以通过重写 main 或者 start 方法 来定义自己的 operations。原则是重写了start方法则不调用main方法,main方法相比于start方法灵活性低一点,因为执行完main方法则返回操作,而start方法有更多控制权。main的默认实现中是没有做任何工作,通过重写main方法添加操作的任务,不要调用super方法。如果要实现异步操作,则需通过重写start方法自行管理操作状态。

@interface NSOperation
- (void)start;
- (void)main;

@property (readonly, getter=isCancelled) BOOL cancelled;// 判断操作是否取消
- (void)cancel;// 取消操作
@property (readonly, getter=isExecuting) BOOL executing;// 判断操作是否正在执行
@property (readonly, getter=isFinished) BOOL finished;// 判断操作是否执行完毕
@property (readonly, getter=isConcurrent) BOOL concurrent; // To be deprecated; use and override 'asynchronous' below
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);// 判断操作是否是异步执行的
@property (readonly, getter=isReady) BOOL ready;// 判断操作是否准备好开始执行,这个属性取决于操作依赖是否设置好
- (void)addDependency:(NSOperation *)op;// 添加依赖
- (void)removeDependency:(NSOperation *)op;// 移除依赖
@property (readonly, copy) NSArray<NSOperation *> *dependencies;// 获取依赖列表
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};// 优先级列表
@property NSOperationQueuePriority queuePriority;// 设置操作优先级
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);// finished设为true则会调用这个回调
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);// 阻塞当前线程,直到操作结束

@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);// 操作的名称
@end


7th,Dec,2015
未完待续 今天要早点休息…
1st,June,2016
大体上对于多线程的总结相对完整了.
29th,August,2016
11st,Nov,2016
看了代码整洁之道中第13章关于并发编程的描述,进一步整理下对于并发编程的理解。
9th,March,2017
改善了文章结构与细节,原先是按API引入时间顺序,依次介绍了NSThread,NSOperation,GCD。然后调整为NSThread,GCD,NSOperation。调整成这样的考虑是NSOperation的底层在iOS4.0引入CGD后,基于GCD做了重新的实现。加深对多线程的理解。

参考资料:

iOS开发系列–并行开发其实很容易
Cocoa深入学习:NSOperationQueue、NSRunLoop和线程安全
iOS并发开发简要整理(上)
iOS并发开发简要整理(下)
iOS Concurrency: Getting Started with NSOperation and Dispatch Queues
iOS开发之多线程技术——NSOperation篇