iOS 多线程理解之NSThread

关于iOS中多线程知识已经有无数前辈总结过,文章的好坏参差不齐,多线程的问题似乎是成为一个高手必修炼的内功。如果说在面试过程中面试官必出的一道题,那么非多线程内容不可,iOS中的多线程开发真的有那么难么?下面是本人的一些总结,当然也参考了前人比较好的文章,只有去实践才能深刻的理解,下面会分三篇去讲解。

多线程

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。在一个程序中,独立运行的程序片段叫做“线程”(Thread)。每个正在系统上运行的程序都是一个进程,每个进程包含一到多个线程。线程是一组指令的集合,或者是程序的特殊段,它可以在程序里独立执行,线程基本上是轻量级的进程,它负责在单个程序里执行多个任务。通常由操作系统负责多个线程的调度和执行。线程是程序中一个单一的循序控制流程,在单个程序中同时运行多个线程完成不同的工作,称为多线程。

在单一的线程中,一件事情没有处理完林外一件事情就不能开始,这样则会影响用户体验。在单核处理器时期就有多线程,这个时候多线程更多的用于解决线程阻塞造成的用户等待(通常是操作完UI后用户不再干涉,其它的线程在等待队列中,CPU一旦空闲就继续执行,不影响用户其它UI操作),其处理能力并没有变化。如今无论移动还是PC,服务器都是多核处理,于是并行运算就更多的提及。一件事情我们可以划分为多个步骤,在没有顺序要求的情况下使用多线程技能解决线程阻塞又能充分利用多核处理器运行能力。

下图反映了一个包含8个操作的任务在一个有两核心的CPU中创建四个线程运行的情况。假设每个核心有两个线程,那么每个CPU中两个线程会交替执行,两个CPU之间的操作会并行运算。单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。当然,不管是多核还是单核开发人员不用过多的担心,因为任务具体分配给几个CPU运算是由系统调度的,开发人员不用过多关心系统有几个CPU。开发人员需要关心的是线程之间的依赖关系,因为有些操作必须在某个操作完成完才能执行,如果不能保证这个顺序势必会造成程序问题。

General preferences

优点

1.使用线程可以把占据时间长的程序中的任务放到后台去处理

2.程序的运行速度可能加快(多核更突出)

……

缺点

1.大量的的线程,会影响性能,因为操作系统需要在他们之间来回切换

2.更多的线程需要更多的内存空间

3.多线程的情况下可能会带来线程操作不当的bug,需要考虑线程之间的资源共享,线程死锁的情况。

iOS多线程

在iOS中每个应用程序(进程)启动后都会建立主线程(UI线程),这个线程被称作其它线程的父线程。如下代码中展示,在main函数中打印发现主线程已经创建:

1
2
3
4
5
6
int main(int argc, char * argv[]) {
NSLog(@"应用程序启动的时候在main函数中创建了主线程.......:%@",[NSThread currentThread]);
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

由于在iOS中除了主线程,其它子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面(同时UI的操作是非线程安全的,如果苹果大量UI部分UIKit设计为线程安全的,将会是非常耗性能的,同时会造成资源共享的bug等。当然新版本iOS中,部分UIFont,UIColor绘图等UI操作已经被设计为线程安全的,但是还是建议UI的操作保证在主线程中执行)。iOS中使用多线程并不复杂,关键是如何控制好各个线程的执行的循序,处理好资源竞争问题。常用的多线程开发有三种方式:

  • NSThread
  • NSOperation
  • GCD

当然使用各多线程的方式需要考虑当前具体的需求场景,甚至在项目中需要结合多种方式使用。具体的在了解完各自的具体使用后,具体的分析各自的优缺点及使用场景。

NSThread

NSThread是轻量级的多线程开发,使用起来并不复杂,但是需要管理线程的生命周期(线程的创建create,启动start,调度cancle等),无法有效控制线程数量。可以通过两种方式来创建线程,实例方法,类方法创建如下:

通过实例方法调用(需要管理其生命周期):

1
2
3
//使用alloc的方式创建一个新的线程,不会阻塞当前的ui主线程的操作
NSThread *myThread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImage:) object:nil];
[myThread start];//启动线程

通过类方法直接创建调用:

1
2
//通过类方法直接隐式创建,每次都会创建一条新的线程
[NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:nil];

通过NSThread可以解决线程阻塞的问题,在通过其它线程进行图片等资源下载过程中,保证主线程UI操作不会阻塞。其它线程操作完成,通过调用主线程的方式操作UI。

NSObject (NSThreadPerformAdditions)

为了简单操作,苹果提供了NSOject分类扩展方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@interface NSObject (NSThreadPerformAdditions)

//在主线程上执行一个方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes

//在指定的线程上执行一个方法,需要用户创建一个线程对象

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes

//在后台执行一个操作,本质就是重新创建一个线程执行当前方法
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end
多个线程并发(Concurrent)

使用NSThread创建多个线程,下载图片并且在相应的UIImageview中展示,通过以下场景来了解并发:

1
2
3
4
5
6
7
8
- (void)changeImage:(MultiModel *)model
{
NSLog(@"index....:%ld",model.index);
NSData *data = [self requestData:model.imagUrl];
model.imageData = data;
//更新的操作放到主线程中操作
[_multiThreadView performSelector:@selector(updateImageView:) onThread:[NSThread mainThread] withObject:model waitUntilDone:YES];
}

1.当前图片请求操作都在主线程执行,当前主线程阻塞,在点击加载按钮后图片没有请求完成的情况下无法,点击顶部的返回操作,所有图片全部请求完成后一起显示。

1
2
3
//当前主线程阻塞,在点击加载按钮后图片没有请求完成的情况下无法,点击顶部的返回操作
//所有图片全部请求完成后一起显示
[self changeImage:model];

2.每一个图片的展示使用一个NSThread线程进行请求图片的操作。由于selector不能传递多个参数,可以将数据进行对象封装处理。当点击加载后,可以点击返回到主页,不会阻塞当前的主线程的任何操作(返回首页后,子线程依旧输出内容)。虽然图片是按循序请求的,但是图片的显示顺序不是有序的,由于网络的原因每个请求线程时间都不一致。

1
2
3
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(changeImage:) object:model];
thread.name = [NSString stringWithFormat:@"线程%ld",index];
[thread start];

3.为了能够控制线程执行顺序,通过控制最后一张图片最后展示在最后展示,可以通过当前为1张图片的线程操作的时候,使得图片1的线程(线程0)休眠一段时间达到最后展示,实际测试多次发现并非图片1每一都是最后展示。

1
2
3
4
5
6
7
8
9
10
11
12
- (void)delay:(MultiModel *)model
{
NSLog(@"index....:%ld",model.index);
if (model.index == 0) {//控制第一张图片,最后展示
NSLog(@"[NSThread currentThread].....:%@",[NSThread currentThread]);
[NSThread sleepForTimeInterval:20];
}
NSData *data = [self requestData:model.imagUrl];
model.imageData = data;
//更新的操作放到主线程中操作
[_multiThreadView performSelector:@selector(updateImageView:) onThread:[NSThread mainThread] withObject:model waitUntilDone:YES];
}

4.当然我们也可以通过修改线程的优先级来控制,图片展示的顺序。线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。修改图片下载方法如下,改变最后一张图片加载的优先级,这样可以提高它被优先加载的几率,但是它也未必就第一个加载:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)priority:(MultiModel *)model
{
NSLog(@"index....:%ld",model.index);
if (model.index == 8) {//控制最后一个最先加载,设置优先级最高
NSLog(@"[NSThread currentThread].....:%@",[NSThread currentThread]);
[NSThread setThreadPriority:1.0];
}
NSData *data = [self requestData:model.imagUrl];
model.imageData = data;
//更新的操作放到主线程中操作
[_multiThreadView performSelector:@selector(updateImageView:) onThread:[NSThread mainThread] withObject:model waitUntilDone:YES];
}

说明NSThread的并发操作,并不能有效的保证执行顺序,由于图片加载的顺序以及网络的原因。

线程的状态

线程的状态有三种如下,都是只读操作:

1
2
3
@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);//判断是否在执行
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);//判断执行是否完成
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);//判断执行是否取消
线程的生命周期

在线程操作过程中可以让某个线程休眠等待,优先执行其他线程操作,而且在这个过程中还可以修改某个线程的状态或者终止某个指定线程。比喻上面并发过程中讲到的线程睡眠,优先级的操作。有下面方法可以控制线程的生命周期,包含类方法和实例方法:

1
2
3
4
5
6
7
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

+ (void)exit;

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;
1
2
3
4
5
6
7
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);

- (void)cancel NS_AVAILABLE(10_5, 2_0);

- (void)start NS_AVAILABLE(10_5, 2_0);

- (void)main NS_AVAILABLE(10_5, 2_0); // thread body method

在用户并发加载了图片操作,立马取消所有线程操作。通过线程的状态,改变线程的生命周期。

1
2
3
4
5
6
7
8
9
10
- (void)exitAllThread
{
NSLog(@"取消所有的线程");
//注意如果仅仅是在这里cancel,实际上是不能阻止子线程继续执行,还需要在线程加载数据的时候,取消掉exit
[self.threadArray enumerateObjectsUsingBlock:^(NSThread * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj isFinished]) {
[obj cancel];
}
}];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
{
NSLog(@"index....:%ld",model.index);
if (model.index > 6) {
[NSThread sleepForTimeInterval:2];//等待主线程中取消后2s再判断,7,8线程是否取消,取消后则退出当前线程,配合断点不怎好控制
if ([[NSThread currentThread] isCancelled]) {//当前在主线程中已经被cancle了
[NSThread exit];//取消掉当前的线程
}
}
NSData *data = [self requestData:model.imagUrl];
model.imageData = data;
//更新的操作放到主线程中操作
[_multiThreadView performSelector:@selector(updateImageView:) onThread:[NSThread mainThread] withObject:model waitUntilDone:YES];
}

使用NSThread在进行多线程开发过程中操作比较简单,但是要控制线程执行顺序并不容易(前面万不得已采用了休眠的方法),另外在这个过程中如果打印线程会发现循环几次就创建了几个线程,这在实际开发过程中是不得不考虑的问题,因为每个线程的创建也是相当占用系统开销的。

参考

1.本篇文章详细demomultiThreadingTest

2.多线程

3.iOS开发系列–并行开发其实很容易

4.超详细!iOS 并发编程之 Operation Queues

5.浅谈并发与并行

6.并发和并行的区别