本篇文章将会简单介绍 iOS 多线程相关的内容。对 NSOperation、NSOperationQueue 的使用进行介绍总结。还将会介绍线程锁相关的内容。
iOS 多线程
多线程在开发中被广泛使用,创建多个线程,每个线程上同时执行不同的任务,从而更快更好使用 CPU 来进行工作。iOS 中提供了多种创建线程的方法,方便开发者操作使用。
1、pthread
POSIX 线程,定义了创建和操纵线程的一套 C语言的 API,使用方法如下:
//#import <pthread.h>
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
pthread_t thread;
pthread_create(&thread, NULL, calculate, NULL);
}
void *calculate() {
NSLog(@"%@", [NSThread currentThread]);
return NULL;
}
//<NSThread: 0x600002d28c40>{number = 7, name = (null)}
2、NSThread
NSThread 是 OC 对 pthread 的一个封装。通过封装,可以更方便的操作线程。
NSThread * thread=[[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"abc"];
//NSThread *thread = [[NSThread alloc] initWithBlock:^{ }]; //iOS 10
thread.name=@"子线程";
[thread start];
// <NSThread: 0x600000e17a80>{number = 8, name = 子线程} -- abc
//自启动创建子线程的方法
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"abc"];
//[NSThread detachNewThreadWithBlock:^{ }]; //iOS 10
//<NSThread: 0x600000b21040>{number = 8, name = (null)} -- abc
//为了更加简化我们创建一个子线程的操作, NSObject对创建线程封装了一些方法
//内部会自动的创建一个子线程,并且把@selector中的方法交给子线程去做,返回值void
[self performSelectorInBackground:@selector(run:) withObject:@"abc"];
// <NSThread: 0x600001fd4c40>{number = 8, name = (null)} -- abc
//[self performSelector:@selector(run:) withObject:@"abc"];
// <NSThread: 0x60000174cf00>{number = 1, name = main} -- asdf
//线程间通信
[NSThread detachNewThreadWithBlock:^{
[self run:@"yyy"];
NSLog(@"on thread");
[NSThread sleepForTimeInterval:2];
NSLog(@"thread end sleep");
[self performSelector:@selector(run:) withObject:@"xxx"];
[self performSelectorOnMainThread:@selector(run:) withObject:@"abc" waitUntilDone:YES]; //从子线程转回主线程
[self performSelectorInBackground:@selector(run:) withObject:@"123"];
}];
// <NSThread: 0x600000015240>{number = 8, name = (null)} -- yyy
// on thread
// thread end sleep
// <NSThread: 0x600000015240>{number = 8, name = (null)} -- xxx
// <NSThread: 0x600000058f00>{number = 1, name = main} -- abc
// <NSThread: 0x600003bdaf00>{number = 9, name = (null)} -- 123
- (void)run:(id)obj {
NSLog(@"%@ -- %@", [NSThread currentThread], obj);
}
使用 pthread 或者 NSThread 是直接对线程操作,可能会引发的一个问题,如果你的代码和所基于的框架代码都创建自己的线程,那么活动的线程数量有可能以指数级增长,每个线程都会消耗内存和内核资源。这样管理多个线程比较困难,所以不推荐在多线程任务多的情况下使用。
苹果官方推荐使用 GCD、NSOperation 和 NSOperationQueue ,这样就不用直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。
有关 GCD 的介绍可查看之前的文章。
下面来介绍有关 NSOperation 和 NSOperationQueue 的操作。
3、NSOperation、NSOperationQueue
NSOperation、NSOperationQueue 是 iOS 中一种多线程实现方式,实际上是基于 GCD 更高一层的封装,NSOperation 和 NSOperationQueue 分别对应 GCD 的任务和队列。面向对象,比 GCD 更简单易用。
3.1、NSOperation
NSOperation是一个和任务相关的抽象类,不具备封装操作的能力,必须使用其子类 NSBlockOperation、NSInvocationOperation 或者使用自定义的继承自 NSOperation 的子类。
NSInvocationOperation
NSInvocationOperation *iop = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run:) object:@"123"];
[iop start]; //在主线程上运行,相当于同步执行
// <NSThread: 0x600003e8cf00>{number = 1, name = main} -- 123
NSBlockOperation
NSBlockOperation * bop=[NSBlockOperation blockOperationWithBlock:^{
[self run:@"blockOperationWithBlock"];
}];
[bop addExecutionBlock:^{
[self run:@"addExecutionBlock"];
}];
bop.completionBlock=^{
NSLog(@"所有任务都执行完成了");
};
[bop start];
// <NSThread: 0x600003e8cf00>{number = 1, name = main} -- blockOperationWithBlock
// <NSThread: 0x600003edd640>{number = 5, name = (null)} -- addExecutionBlock
// 所有任务都执行完成了
如果添加的操作多的话,blockOperationWithBlock:
中的操作也可能会在其他线程(非当前线程)中执行,这是由系统决定的,并不是说添加到 blockOperationWithBlock:
中的操作一定会在当前线程中执行。addExecutionBlock: 的在哪个线程执行也不一定。一般都是把操作加入队列,通过队列来控制执行方式,对于线程的操作不用我们来处理。正如前面提到的,我们不用直接跟线程打交道,只需添加任务即可。
自定义 NSOperation
我们可以通过重写 main 或者 start 方法 来定义自己的 operations 。
重写 main 这种方法简单,不需要管理一些状态属性(例如 isExecuting 和 isFinished),当 main 方法返回的时候,这个 operation 就结束了。这种方式使用起来非常简单,但是灵活性相对重写 start 来说要少一些。
@interface MyOperation : NSOperation
@end
@implementation MyOperation
- (void)main {
NSLog(@"my operation main() -- %@", [NSThread currentThread]);
//为了能使用操作队列所提供的取消功能,
//在长时间操作中时不时地检查 isCancelled 属性
while (notDone && !self.isCancelled) {
// 进行处理
}
}
@end
MyOperation *op = [[MyOperation alloc] init];
[op start];
//my operation main() -- <NSThread: 0x6000009780c0>{number = 1, name = main}
如果想拥有更多的控制权,以及在一个操作中可以执行异步任务,可以通过重写 start 方法实现:
@interface MyOperation ()
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@end
@implementation MyOperation
@synthesize executing = _executing;
@synthesize finished = _finished;
- (void)start {
self.executing = YES;
self.finished = NO;
NSLog(@"start - %@", [NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSThread sleepForTimeInterval:2];
NSLog(@"do - %@", [NSThread currentThread]);
[self done];
});
}
- (void)done {
self.finished = YES;
self.executing = NO;
}
- (void)setFinished:(BOOL)finished {
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
- (void)setExecuting:(BOOL)executing {
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
@end
MyOperation *op = [[MyOperation alloc] init];
[op start];
//start - <NSThread: 0x600000f54d80>{number = 1, name = main}
//do - <NSThread: 0x600000f0df00>{number = 6, name = (null)}
这种情况下,你必须手动管理操作的状态。 为了让操作队列能够捕获到操作的改变,需要将状态的属性以配合 KVO 的方式进行实现。如果你不使用它们默认的 setter 来进行设置的话,你就需要在合适的时候发送合适的 KVO 消息。
3.2、NSOperationQueue
NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。
NSInvocationOperation * iop1=[[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run:) object:@"queue iop1"];
NSBlockOperation * bop=[NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@ -- queue bop",[NSThread currentThread]);
}];
//添加依赖关系
//iop1 依赖于 bop 一定是在 bop 任务执行完成之后才会执行 iop1 中的任务。相当于间接的设定了任务的执行顺序。
//看下面的打印内容,添加依赖的两个任务,在同一个线程中执行,顺序执行。这个不确定,不用管线程
[iop1 addDependency:bop];
//创建一个队列, 把任务交给队列管理
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperation:iop1];
[queue addOperation:bop];
[queue addOperationWithBlock:^{
NSLog(@"%@ -- queue add block",[NSThread currentThread]);
}];
//waitUntilFinished 是否等待队列中的执行任务完成之后再去执行后面的逻辑代码
//[queue addOperations:@[iop1, bop] waitUntilFinished:YES];
/** 不能重复加入队列,不然崩溃
reason: operations are finished, executing, or already in a queue, and cannot be enqueued' */
NSLog(@" end -- ");
//任务加入队列,队列创建子线程并发执行,不需要调用 start 方法
// end -- //不阻塞主线程,这个先打印
// <NSThread: 0x600000ce8380>{number = 7, name = (null)} -- queue add block
// <NSThread: 0x600002851140>{number = 5, name = (null)} -- queue bop
// <NSThread: 0x60000287cf00>{number = 5, name = (null)} -- queue iop1
添加依赖关系
[iop1 addDependency:bop];
iop1 依赖于 bop 一定是在 bop 任务执行完成之后才会执行 iop1 中的任务。相当于间接的设定了任务的执行顺序。
根据上面打印内容,添加依赖的两个任务,在同一个线程中执行,顺序执行。这个不确定,不用管线程。
maxConcurrentOperationCount
queue.maxConcurrentOperationCount = 2 ;
用来控制一个特定队列中可以有多少个操作参与并发执行。
若将其设置为 1 的话,你将得到一个串行队列,这在以隔离为目的的时候会很有用。
addBarrierBlock
类似于 GCD 中的 dispatch_barrier_async 栅栏。类似分界线,阻碍后面的任务执行,直到 barrier block 执行完毕。
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSLog(@"%@ --1 ",[NSThread currentThread]);
}];
[queue addBarrierBlock:^{
NSLog(@"%@ -- barrier ",[NSThread currentThread]);
}];
[queue addOperationWithBlock:^{
NSLog(@"%@ --2 ",[NSThread currentThread]);
}];
NSLog(@" end -- ");
// end --
// <NSThread: 0x600002de8b00>{number = 7, name = (null)} --1
// <NSThread: 0x600002dbc180>{number = 5, name = (null)} -- barrier
// <NSThread: 0x600002dbd280>{number = 4, name = (null)} --2
操作之间的通信
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
NSLog(@"%@ -- do something ",[NSThread currentThread]);
[NSThread sleepForTimeInterval:2];
//任务完成,回到主线程
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
NSLog(@"%@ -- completed ",[NSThread currentThread]);
}];
}];
NSLog(@" end -- ");
// end --
// <NSThread: 0x600003550980>{number = 4, name = (null)} -- do something
// <NSThread: 0x60000351cd80>{number = 1, name = main} -- completed
线程安全
在多线程中访问共享资源,可能会遇到一些问题。比如,线程 A 和 B 都从内存中读取出了计数器的值,线程 A 将计数器值加一,同时线程 B 也将计数器值加一,这时计数器被加了两次,因为同时操作,结果只加一,这样就导致了数据的混乱。
为了防止出现这样的问题,多线程需要一种互斥的机制来访问共享资源,保证线程安全。
互斥访问就是同一时刻,只允许一个线程访问某个特定资源。为了保证这一点,每个希望访问共享资源的线程,首先需要获得一个共享资源的互斥锁,一旦某个线程对资源完成了操作,就释放掉这个互斥锁,这样别的线程就有机会访问该共享资源了。
加锁方式,常见的有,@synchronized、NSLock、dispatch_semaphore。
@synchronized
//创建两个操作,去访问 self.count
NSOperationQueue * queue1=[[NSOperationQueue alloc] init];
[queue1 addOperationWithBlock:^{
NSLog(@"1 -- %@",[NSThread currentThread]);
[self addCount];
}];
NSOperationQueue * queue2=[[NSOperationQueue alloc] init];
[queue2 addOperationWithBlock:^{
NSLog(@"2 -- %@ ",[NSThread currentThread]);
[self addCount];
}];
- (void)addCount {
@synchronized (self) {
self.count += 1;
}
}
@synchronized (self) 括号里的 self 为该锁的标识,只有当标识相同时,才满足互斥。
NSLock
//self.lock = [[NSLock alloc] init];
- (void)addCount {
[self.lock lock];
self.count += 1;
[self.lock unlock];
}
NSLock 也是我们经常所使用的锁,除 lock 和 unlock 方法外,还有方法:
tryLock :尝试加锁,如果锁不可用(已经被锁住),刚并不会阻塞线程,并返回NO。
lockBeforeDate: 会在所指定 Date 之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO。
类似的锁还有,NSConditionLock、NSRecursiveLock、NSCondition
dispatch_semaphore
GCD 中的 dispatch_semaphore 信号量,也可以用来加锁。
//@property (strong, nonatomic, nonnull) dispatch_semaphore_t someLock;
//self.someLock = dispatch_semaphore_create(1);
- (void)addCount {
dispatch_semaphore_wait(self.someLock, DISPATCH_TIME_FOREVER); //加锁
self.count += 1;
dispatch_semaphore_signal(self.someLock); //解锁
}
1、dispatch_semaphore_create 函数可以生成信号量,参数是信号量计数的初始值。
2、dispatch_semaphore_wait 函数,当信号量值为 0 时等待,等待直到超时,参数可设置超时时长。信号量值大于等于 1 时,不等待,同时将信号量值减 1。
3、dispatch_semaphore_signal 函数会让信号量值加 1,如果有通过dispatch_semaphore_wait 函数等待信号量值增加的线程,会由系统唤醒最先等待的线程执行。
除了以上这些方法之外,还有 pthread_mutex、OSSpinLock 等方法,这里不再介绍,自行查阅资料。
避免死锁
互斥锁解决了内存读写安全的问题,但这也引入了其他问题,其中一个就是死锁。当多个线程在相互等待着对方的结束时,就会发生死锁,这时程序可能会被卡住。
在线程之间共享的资源越多,使用的锁越多,程序被死锁的概率也越大。所以要尽量减少线程间资源共享,确保共享的资源尽量简单。
多线程注意事项
1、控制线程数量
使用并行队列,当任务过多且耗时较长时,队列会创建大量线程,而部分线程里面的耗时任务已经耗尽了 CPU 资源,所以其他的线程也只能等待 CPU 时间片,过多的线程也会让线程调度过于频繁。
GCD 中并行队列并不能限制线程数量,可以创建多个串行队列来模拟并行的效果。
2、减少队列切换
当线程数量超过 CPU 核心数量,CPU 核心通过线程调度切换用户态线程,意味着有上下文的转换(寄存器数据、栈等),过多的上下文切换会带来资源开销。虽然内核态线程的切换理论上不会是性能负担,开发中还是应该尽量减少线程的切换。
使用队列切换并不总是意味着线程的切换,代码层面可以减少队列切换来优化。
NSOperationQueue * queue=[[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
//...
}];
}];
References
苹果官网:Operation Queues
并发编程:API 及挑战
iOS中保证线程安全的几种方式与性能对比
iOS 如何高效的使用多线程