iOS多线程

前言

   本文参考自《iOS开发项目化经典教程》第一章,主要讲述iOS实现多线程的四种方式,线程间的安全和通信,GCD的基本操作。NSOperation的基本操作。

   多线程概念:由于一个线程同一时间内只能处理一个任务,因此一个线程内的任务需要按顺序执行。因此在遇到网络请求,下载等耗时操作时,需要等待此类操作结束才能进行接下来的操作,这段时间内用户不能进行任何操作,app也不会响应用户的操作。这是一种很糟糕的用户体验。因此在开发过程中,通常把比较耗时的操作放到一个线程中,把和用户交互放到主线程中。保证能够及时响应用户的操作行为。

进程和线程

   进程:进程是系统中正在运行的程序,进程拥有自己的地址空间,程序加载进内存开始运行时就会变为一个进程。进程是系统进行资源分配和调度的基本单位。有三个特点:1、独立性,进程有独立的资源和地址访问空间。2、动态性,进程是程序在系统中的一次执行过程,进程时一个正在系统中活动的指令的集合,其中加入了时间的概念。具有自己的生命周期和各自不同的状态。3、并发性,进程可以在单个处理器中并发执行,虽然宏观上是并发的,但是从微观上看,cpu在一个时间片内只能执行一条指令,因此在宏观时间内也是各个进程不断相互切换执行。
   线程:线程是任务调度和执行的基本单位,线程扩展了进程的概念,一个进程要运行至少需要一个线程,这个线程成为称为主线程。当进程被初始化后,线程就被创建了。进程内的每个线程是相互独立的。
  进程中包含若干线程,这些线程可以利用进程所拥有的资源。由于线程比进程更加轻量,对线程的调用开销就会比进程的开销小的多。因此多线程更能提高系统中多个程序间的并发程度。当使用多线程进行开发时需要注意:1、线程虽然占用资源较低,但是也需要一定的内存空间,如果开启大量线程,就会占用大量内存空间导致程序卡顿。3、开启的线程越多,cpu在进行调度时的开销就越大。因此不要同时开启超过5个线程。3、使用多线程时需要保证数据的统一性。可以采用加锁的方式实现数据的独占访问。

线程的串行和并行

  一个进程中如果只包含一个线程,那么当多个任务需要执行时只能由这个线程去一个一个串行执行。如果进程中包含不止一条线程,那每条线程可以同时执行不同的任务,称之为线程的并行,也即多线程。需要注意的是,cpu在同一个时间内只能处理一个线程,也就是同一时间只有一个线程在工作,不过cpu切换很快,用户感知不到。

多线程分类

   iOS提供了四种现实多线程的方式,pthread:使用c语言实现,是跨平台的多线程api。NSThread:使用oc实现。GCD:使用c实现,对线程管理进行了封装,是常用的多线程开发手段。NSOperation:基于GCD进行了二次封装,使其使用更加面向对象。

threads

线程的状态

  当线程被创建并启动时,线程并不是直接开始执行,而线程开始执行之后也不是一直在执行。而是随着cpu的时间片轮转不停的切换状态。下图是线程的状态切换过程图。

thread_state

1、创建,当创建一个线程之后,该线程就处于新建的状态,仅由系统分配了内存,初始化了其内部成员变量的值。2、就绪,当线程调用了start方法之后,线程就处于就绪态了,处于就绪态的线程并没有开始执行,只是表示可以开始执行了。何时开始执行由系统控制。 3、运行,当cpu分配的时间片调用当前线程时,其他线程处于挂起状态,该线程处于运行状态。4、终止,当线程执行结束,或者调用exit方式终止,或者执行过程中出现异常,线程变为终止状态。5、阻塞,如果当前线程需要暂停一段时间,则可以调用sleep方法让线程进入阻塞态。

线程间的安全隐患

  如果进程中的一块资源被多个线程共享,那么这块资源称为临界资源,当多个线程同时访问一块资源时,会出现资源的抢占和数据错乱等问题。为了实现数据的安全访问,可以使用线程间加锁的方式,下面通过一个卖票的例子展示一下:

saletickets

  从途中可以看到,多个线程访问临界资源时会发生数据错乱,为了解决这种问题,需要给临界资源加锁,加锁可以保证同一时刻只有一个线程能够访问临界资源,使用@synchronized关键字可以实现加锁.需要注意的是使用同步锁会消耗大量cpu资源,所以应该把同步锁内的代码量尽量减少.
thread_state2

线程间通信

  在一个进程中,其线程往往不是孤立存在的,他们之间可以相互通信,比如一个线程需要等到另外一个线程任务执行完毕后才能开始执行.在做任务转换的同时也可能有数据的转换.

使用NSThread实现多线程

  下面通过一个实例来演示NSThread的三种创建方式。

1
2
3
4
5
6
/// 需要调用start启动线程
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument;

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;

- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;

三种不同的线程创建方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
_btn = [UIButton buttonWithType:UIButtonTypeCustom];
[_btn setTitle:@"创建线程" forState:UIControlStateNormal];
[_btn setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
_btn.titleEdgeInsets = UIEdgeInsetsMake(5, 20, 5, 20);
_btn.frame = CGRectMake(self.view.frame.size.width/2-70, 300, 140, 50);
[_btn addTarget:self action:@selector(onClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:_btn];
}

- (void)onClick {
NSThread *currentThread = [NSThread currentThread];
NSLog(@"btnclick--%@--current",currentThread);
NSThread *mainThread = [NSThread mainThread];
NSLog(@"btnclick--%@--main",mainThread);
// [self threadCreate1];
// [self threadCreate2];
[self threadCreate3];
}

- (void)run:(NSString *)params {
NSThread *cur = [NSThread currentThread];
for (int i = 0; i < 10; i++) {
NSLog(@"%@---call run--%@",cur,params);
}
}
/// 第一种初始化
-(void)threadCreate1 {
NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"threadA"];
threadA.name = @"线程A";
/// 必须使用start启动线程
[threadA start];
NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"threadB"];
threadB.name = @"线程B";
[threadB start];
}
/// 第二种初始化
- (void)threadCreate2 {
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:@"threadCreate2"];
}
/// 第三种初始化
- (void)threadCreate3 {
[self performSelectorInBackground:@selector(run:) withObject:@"threadCreate3"];
}

  主线程的名字是main,手动创建的线程可以重新命名。

使用GCD实现多线程

  使用GCD可以比NSThread更快,更方便的实现多线程的处理,使用GCD需要明确两个概念,队列和任务.队列:队列是GCD中用来存放任务的集合,负责管理开发者提交的任务.队列的核心就是将长期执行的任务分成多个工作单元,并将任务添加到队列中,系统会带为管理这些队列.并放到多线程中执行.队列分为串行队列和并行队列.串行队列内部只维护一个线程,一次只能执行一个任务.任务串行执行.并行队列内部维护了多个线程,可以按照如队列顺序并行执行. 任务:任务时用户提交给队列的工作单元,也就是在队列中执行的代码块.任务提交后会由队列以多线程的方式执行.

创建队列

  全局并发队列,全局并发队列可以并行的执行多个任务,

1
2
3
4
5
6
//identifier 指定队列优先级
// flags 保留字段,方便以后扩展
dispatch_queue_global_t dispatch_get_global_queue(intptr_t identifier, uintptr_t flags)

// 实例
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

  创建串行队列和并行队列

1
2
3
4
// 创建队列
//label 表示队列的字符串(c字符串)
//attr 标识串行还是并行 DISPATCH_QUEUE_SERIAL 串行, DISPATCH_QUEUE_CONCURRENT 并行
dispatch_queue_t dispatch_queue_create(const char *_Nullable label,dispatch_queue_attr_t _Nullable attr);

  获取主队列

1
2
/// 获取主队列
dispatch_queue_t queue = dispatch_get_main_queue();

提交任务

  队列创建结束之后,可以通过同步和异步两种方式向队列提交任务. 同步执行:同步执行的任务只会在当前线程执行任务,不会开启新线程.

1
2
3
///同步执行
/// queue 将要提交到的任务队列, block 执行的代码块
dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

  异步执行任务,异步执行会在新线程中执行任务具备开启新线程的能力,

1
2
3
///异步执行
///queue 将要提交到的任务队列, block 执行的代码块
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

  下面是一个代码实例,展示了串行队列下的同步执行和并行队列下的同步执行,串行队列下的异步执行和并行队列下的异步执行.

同步执行不会开启新的线程,是在主线程完成任务的,同步的并行也不会开启新线程,两个都是串行执行任务.

syncSerial

异步执行串行队列的任务,会开启新线程,但是任务串行执行.

asyncSerial

异步执行并行队列任务,会开启新线程,任务并行执行.

asyncConcerrent

  下表展示了同步执行串行队列任务和并行队列任务,异步执行串行队列任务和并行队列任务的结果

outcome

单次或者重复执行任务

  如果想要某些操作只执行一次,可以使用dispatch_once()实现,干函数接收两个参数,第一个参数接收一个标识符,用来标识代码代码是否已经执行.第二个参数是只执行一次的代码块.如果想要某些操作重复执行多次,可以使用dispatch_apply();

1
2
3
4
/// iterations 需要执行的次数  queue 任务需要提交的目标队列  block 需要重复执行的代码块
void dispatch_apply(size_t iterations,
dispatch_queue_t DISPATCH_APPLY_QUEUE_ARG_NULLABILITY queue,
DISPATCH_NOESCAPE void (^block)(size_t));

调度队列组

  假设有如下一个一个场景,一个音乐app,需要执行多个下载任务,这些下载任务会放到后台的多个线程中执行,当全部下载任务结束之后,弹出一个提示框告知用于下载结束.
这种场景可以使用队列组在实现.使用dispatch_group_create()创建队列组,

1
2
/// 创建队列组
dispatch_group_t dispatch_group_create(void);

创建队列组后,可以使用dispatch_group_async()函数将需要执行的任务提交到队列组,会异步的执行任务组中的任务,

1
2
3
4
/// 异步执行队列组中的任务 group,队列组  queue,任务需要提交到的队列, block,需要执行的代码块
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);

任务组中的任务执行完之后可以通过dispatch_group_notify()来通知,

1
2
3
4
/// group, 创建的队列组 queue,将要执行的任务所添加到的队列  block,执行的代码块
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);

这里的demo展示的是使用队列组进行图片下载,在图片下载完成后,回到主队列进行渲染和展示.但是队列组中的每一次下载都是新开了线程,并且是并行执行的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)groupImage {
__block typeof(self) weakSelf=self;
dispatch_group_t group = dispatch_group_create();
__block UIImage *img1 = nil;
dispatch_group_async(group, global_queque, ^{
img1 = [weakSelf downloadImage:@"http://statics.888ppt.com/Upload/photothumb/6J93jXFHs24.jpg"];
NSLog(@"--%@--",[NSThread currentThread]);
});
__block UIImage *img2 = nil;
dispatch_group_async(group, global_queque, ^{
img2 = [weakSelf downloadImage:@"http://up.enterdesk.com/edpic/d4/32/68/d43268ae15cefc60c54b8b0f94a46c74.jpg"];
NSLog(@"--%@--",[NSThread currentThread]);
});
dispatch_group_notify(group, global_queque, ^{
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.imgView1.image = img1;
weakSelf.imgView2.image = img2;
NSLog(@"--%@--",[NSThread currentThread]);
});
});
}

使用NSOperation实现多线程

  NSOperation是使用oc封装的基于gcd的多线程处理方式,更加面向对象1、执行操作,如果要执行一个NSOperation对象,可以通过1、手动调用start方法实现,这个方法调用之后,就会在当前调用的线程同步执行任务.2、可以将NSOperation添加到NSOperationQueue中,NSOperationQueue会在NSOperation被添加进去后尽快执行,并且是异步执行.2、取消操作,当一个operation开始执行后,默认会一直执行到结束,也可以调用cancel取消操作的执行,需要注意的是,如果操作在队列中没有开始执行,这时取消这个操作,并将finished设置为YES,此时操作就直接取消了.如果操作正在执行,设置cancel方法也只能等待操作执行完.3、添加依赖,可以将多个耗时的异步操作分成若干部分,当前一个执行完后在执行另一个,可以通过addDependency方法,协调先后关系.注意:两个任务间不能相互依赖,比如A依赖B,B依赖A,这样会导致思索.当每个操作结束后需要将isFinished设置为Yes.4、监听操作,使用setCompletionBlock可以在一个operation结束后做一些其他事情.

NSOperationQueue

   NSOperationQueue与GCD队列一样,采用先进先出的方式,负责管理系统提交的多个NSOperation对象.NSOperationQueue负责管理其中持有的NSOperation对象,下面是一些常见的方法:
1、添加NSOperation到NSOperationQueue中,将NSOperation添加到队列中,并由队列中的线程池管理和调度,使用如下方法来管理,

1
2
/// ops,将要添加到队列的操作组, wait 是否等待操作结束后在返回,设置为yes会阻塞主线程
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait

也可以使用如下方法来添加任务,添加任务到队列中后不要在手动更改任务的状态,队列会自行进行管理.

1
2
/// block 将要执行的代码块
- (void)addOperationWithBlock:(void (^)(void))block;

2、修改NSOperation对象执行顺序,对于添加到队列中的操作对象,执行顺序依赖亮点,1、操作对象是否已经是就绪态,2、操作对象的优先级高低.可以通过手动设置queuePriority在更改operation的优先级.
3、设置操作队列的最大并发数量,当队列中线程过多时,会影响app的执行效率,因此可以设置对答并发数量来约束队列中的最大进程数
4、等待NSOperation操作执行完成,不要在主线程使用如下方法,这回阻塞主线程.app显示为无响应.
5、使用NSOperation子类进行操作,NSOperation是一个抽象基类,可以自行继承这个类创建自己的操作对象,也可以使用NSInvocationOperation和NSBlockOperatin来进行操作.

1
2
- (void)waitUntilFinished ;
- (void)waitUntilAllOperationsAreFinished;

  因此,使用NSOperation和NSOperationQueue两个类结合使用可以实现多线程,1、将需要执行的操作添加到NSOperation中.2、将NSOperation添加到NSOperationQueue中.3、系统自动取出NSOperationQueue中的对象.4、取出的对象会自动创建新线程进行.

iOS多线程小demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

NSLog(@"1");

dispatch_async(serialQueue, ^{

NSLog(@"2");
});

NSLog(@"3");

dispatch_sync(serialQueue, ^{

NSLog(@"4");
});

NSLog(@"5");

  上述代码执行结果为13245或者12345,结果是固定的.明确概念,任务是执行在队列上的,队列是运行在线程上的.一个线程上可以同时出现多个队列.同步任务会阻塞线程.异步任务不会阻塞线程.异步任务具有开启新线程的能力.如果是自定义队列,异步执行会为其开启新线程,如果是主队列,异步执行不会开启新线程,在主线程上执行.

  对于上面代码,首先打印1,然后异步执行串行队列上的任务,不会阻塞当前线程(主线程),打印3,4和5的顺序是固定的,因为4是同步任务,2和4的顺序是固定的,因为两者在同一个队列上,需要按照fifo的顺序执行.因此,只需要考虑2和3谁先谁后即可,应该说都有可能,因此最后结果是13245或者12345.

  上文中的demo参考此处即可.