iOS多线程

多线程是一种十分常用的技术,但它也容易发生各种问题。比如多个线程更新相同的资源会导致数据的不一致(数据竞争)、停止等待事件的线程会导致多个线程相互持续等待(死锁)、使用太多线程会消耗大量内存等。

iOS 目前有4套多线程方案:

  • pthread
  • NSThread
  • GCD
  • NSOperation

pthread

pthread 是一套 C 语言的接口,虽然因此具备很强的移植性,但是它用起来不怎么方便:

1
2
3
4
5
6
7
8
9
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
pthread_t thread;
//创建一个线程并自动执行
pthread_create(&thread, NULL, start, NULL);
}
void *start(void *data) {
NSLog(@"%@", [NSThread currentThread]);
return NULL;
}

NSThread

看名字就明白,NSThread 是用 Objective-C 封装过的一套接口,实现了面向对象。用来要比 pthread 方便一点,但它的生命周期还是需要我们手动管理。

先创建线程类,再启动

1
2
3
4
// 创建
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:nil];
// 启动
[thread start];

创建并自动启动

1
[NSThread detachNewThreadSelector:@selector(run:) toTarget:self withObject:nil];

使用 NSObject 的方法创建并自动启动

1
[self performSelectorInBackground:@selector(run:) withObject:nil];

其他方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//取消线程
- (void)cancel;
//启动线程
- (void)start;
//判断某个线程的状态的属性
@property (readonly, getter=isExecuting) BOOL executing;
@property (readonly, getter=isFinished) BOOL finished;
@property (readonly, getter=isCancelled) BOOL cancelled;
//设置和获取线程名字
-(void)setName:(NSString *)n;
-(NSString *)name;
//获取当前线程信息
+ (NSThread *)currentThread;
//获取主线程信息
+ (NSThread *)mainThread;
//使当前线程暂停一段时间,或者暂停到某个时刻
+ (void)sleepForTimeInterval:(NSTimeInterval)time;
+ (void)sleepUntilDate:(NSDate *)date;

GCD

Grand Central Dispatch(GCD)是异步执行任务的技术之一。开发者只需要定义想执行的任务并追加到适当的 Dispatch Queue 中,GCD 就能生成必要的线程并计划执行任务。由于线程管理是作为系统的一部分来实现的,因此可统一管理,也可执行任务,这样就比以前的线程更有效率。

在 GCD 之前,Cocoa 框架提供了 NSObject 类的performSelectorInBackground:withObject实例方法和performSelectorOnMainThread实例方法等简单的多线程编程技术。这类方法要比使用 NSThread 类进行多线程编程简单,但 GCD 更为简洁,且 GCD 提供的系统级线程管理提高执行效率。

Dispatch Queue

开发者要做的只是定义想执行的任务并追加到适当的 Dispatch Queue 中。

“Dispatch Queue”就是执行处理的等待队列,它按照追加的顺序(FIFO)执行处理。 有两种 Dispatch Queue:

  • 等待现在执行中处理的 Serial Dispatch Queue
  • 不等待现在执行中处理的 Concurrent Dispatch Queue

简单的说,一个是串行队列而另一个是并行队列。并行执行的处理数量取决于当前系统的状态(iOS 和 OS X 基于 Dispatch Queue 中的处理数、CPU 核数以及 CPU 负荷等当前系统的状态来决定 Concurrent Dispatch Queue 中并行执行的处理数)

获取 Dispatch Queue

第一种方法是通过 GCD 的 API 生成 Dispatch Queue。

1
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.MySerialDispatchQueue", NULL);

第一个参数指定 Serial Dispatch Queue 的名称,用于调试。第二个参数指定为 NULL 生成 Serial Dispatch Queue,生成 Concurrent Dispatch Queue 时,指定为DISPATCH_QUEUE_CONCURRENT

dispatch_queue_create函数可生成任意多个 Dispatch Queue。

由于 ARC 只对 Objective-C 对象有效,所以通过 dispatch_queue_create 函数生成的 Dispatch Queue 在使用结束后要通过dispatch_release函数释放。

1
dispatch_release(mySerialDispatchQueue);

相应也存在dispatch_retain函数。

1
dispatch_retain(myConcurrentDispatchQueue);

系统对于一个 Serial Dispatch Queue 就只生成并使用一个线程,但不可大量生成,如果过多使用多线程,就会消耗大量内存,大幅度降低系统性能。只在为了避免多线程编程问题之一——多个线程更新相同资源导致数据竞争时使用 Serial Dispatch Queue。当想并行执行不发生数据竞争等问题的处理时,使用 Concurrent Dispatch Queue。对于后者来说,不管生成多少,由于 XNU 内存只使用有效管理的线程,因此不会发生 Serial Dispatch Queue 的那些问题。

第二种方法是获取系统标准提供的 Dispatch Queue:Main Dispatch QueueGlobal Dispatch Queue

Main Dispatch Queue 是在主线程中执行的 Dispatch Queue,它是 Serial Dispatch Queue。

Global Dispatch Queue 是所有应用程序都能够使用的 Concurrent Dispatch Queue,它有4个执行优先级,分别是高优先级(High Priority)、默认优先级(Default Priority)、低优先级(Low Priority)和后台优先级(Background Priority)。优先级只能做大致的判断。

1
2
3
4
5
6
7
8
9
/*
* Main Dispatch Queue 的获取方法
*/
dispatch_queue_t mainDispatchQueue = dispatch_get_main_queue();
/*
* Global Dispatch Queue (高优先级)的获取方法
*/
dispatch_queue_t globalDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);

对于 Main Dispatch Queue 和 Global Dispatch Queue 执行 dispatch_retain 函数和 dispatch_release 函数不会引起任何变化,也不会有任何问题。

dispatch_set_target_queue

dispatch_queue_create 函数生成的 Dispatch Queue 不管是 Serial Dispatch Queue 还是 Concurrent Dispatch Queue,都使用与默认优先级 Global Dispatch Queue 相同执行优先级的线程。可通过dispatch_set_target_queue变更优先级。

1
2
3
dispatch_queue_t mySerialDispatchQueue = dispatch_queue_create("com.example.gcd.mySerialDispatchQueue", NULL);
dispatch_queue_t globalDispatchQueueBackground = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
dispatch_set_target_queue(mySerialDispatchQueue, globalDispatchQueueBackground);

第一个参数为要变更执行优先级的 Dispatch Queue,第二个参数是与要使用的执行优先级相同优先级的 Global Dispatch Queue。

dispatch_set_target_queue 还可以改变 Dispatch Queue 的执行层次。如果在多个 Serial Dispatch Queue 中用 dispatch_set_target_queue 函数指定目标为某个 Serial Dispatch Queue,那么原本应并行执行的多个 Serial Dispatch Queue,在目标 Serial Dispatch Queue 上只能同时执行一个处理。一般都是把一个任务放到一个串行的queue中,如果这个任务被拆分了,被放置到多个串行的queue中,但实际还是需要这个任务同步执行,那么就会有问题,因为多个串行queue之间是并行的。这时候dispatch_set_target_queue将起到作用。

dispatch_after

在3秒后执行处理:

1
2
3
4
5
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 3ull * NSEC_PER_SEC);
dispatch_after(time, dispatch_get_main_queue(), ^{
NSLog(@"waited at least three seconds");
});

dispatch_after函数并不是在指定时间后执行处理,而是在指定时间追加处理到 Dispatch Queue。

第一个参数是dispatch_time_t类型的值,该值使用dispatch_time函数或dispatch_walltime函数作成,这两者分别用于计算相对时间与绝对时间。dispatch_time函数能够获取从第一个参数指定的时间开始,到第二个参数指定的毫微秒单位时间后的时间。如果使用NSEC_PER_MSEC则可以以毫秒为单位计算。

Dispatch Group

在追加到 Dispatch Queue 中的多个处理全部结束后想执行结束处理,这种情况可以使用 Dispatch Group。

1
2
3
4
5
6
7
8
9
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{NSLog(@"done");});
dispatch_release(group);

另外,在 Dispatch Group 中也可以使用 dispatch_group_wait 函数仅等待全部处理执行结束。

1
2
3
4
5
6
7
8
9
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{NSLog(@"blk0");});
dispatch_group_async(group, queue, ^{NSLog(@"blk1");});
dispatch_group_async(group, queue, ^{NSLog(@"blk2");});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
dispatch_release(group);

dispatch_group_wait函数的第二个参数指定为等待的时间(超时)。DISPATCH_TIME_FOREVER意味着永久等待。也可以指定等待1秒:

1
2
3
4
5
6
7
8
9
10
11
12
13
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 1ull * NSEC_PER_SEC);
long result = dispatch_group_wait(group, time);
if (result == 0) {
/*
* 属于 Dispatch Group 的全部处理执行结束
*/
} else {
/*
* 属于 Dispatch Group 的某一个处理还在执行中
*/
}

”等待“意味着 dispatch_group_wait 函数处理调用状态而不返回,即在当前线程停止。

dispatch_barrier_async

为了避免读写数据时的竞争问题,并使读能够并行处理,可以使用dispatch_barrier_async函数。

1
2
3
4
5
6
7
8
9
dispatch_async(queue, blk0_for_reading);
dispatch_async(queue, blk1_for_reading);
dispatch_async(queue, blk2_for_reading);
dispatch_async(queue, blk3_for_reading);
dispatch_barrier_async(queue, blk_for_writing);
dispatch_async(queue, blk4_for_reading);
dispatch_async(queue, blk5_for_reading);
dispatch_async(queue, blk6_for_reading);
dispatch_async(queue, blk7_for_reading);

这样,dispatch_barrier_async函数会等待追加到 Concurrent Dispatch Queue 上的并行执行的处理全部结束之后,然后由 dispatch_barrier_async 函数追加的处理执行完毕后,Concurrent Dispatch Queue 才恢复一般的动作。

dispatch_sync

dispatch_sync是将指定的 Block”同步“追加到指定的 Dispatch Queue 中。在追加 Block 结束之前,dispatch_sync函数会一直等待。

不过这个方法稍有不慎就会导致死锁:

1
2
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_sync(queue, ^{NSLog(@"Hello?");});

dispatch_apply

dispatch_apply函数式 dispatch_sync 函数和 Dispatch Group 的关联 API。该函数按指定的次数将指定的 Block 追加到指定的 Dispatch Queue 中,并等待全部处理执行结束。

1
2
3
4
5
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_apply(10, queue, ^(size_t index){
NSLog(@"zu", index);
});
NSLog(@"done");

执行结果为

1
2
3
4
5
6
7
8
9
10
11
4
1
0
3
5
2
6
8
9
7
done

因为在 Global Dispatch Queue 中执行处理,所以各个处理的执行时间不定。但是 done 必定在最后的位置上,因为 dispatch_apply 函数会等待全部处理执行结束。

dispatch_suspend / dispatch_resume

dispatch_suspend 函数挂起指定的 Dispatch Queue

1
dispatch_suspend(queue);

dispatch_resume 函数恢复指定的 Dispatch Queue

1
dispatch_resume(queue);

这些函数对已经执行的处理没有影响。挂起后,追加到 Dispatch Queue 中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。

Dispatch Semaphore

Dispatch Semaphore 是持有计数的信号,计数为0时等待,计数为1或大于1时,减去1而不等待。

dispatch_once

dispatch_one 函数是保证在应用程序执行中只执行一次指定处理的 API,在多线程环境下也可保证百分之百安全。一般用于单例模式。

Dispatch I/O

使用 Dispatch I/O 和 Dispatch Data 可以在读取大文件时,将文件分成合适的大小进行并行读取,提高读取速度。

NSOperation

其实 NSOperation 比 GCD 早诞生,但是后来 NSOperation 又重新基于 GCD 进行了封装。NSOperationQueue 对应于 GCD 的队列。

NSOperation 是一个抽象类,不能实例化,所以需要使用它的两个子类:NSInvocationOperationNSBlockOperation。创建一个 operation 后,需要调用 start 方法来启动任务,它会默认在当前队列同步执行。

1
2
3
4
5
6
7
8
9
NSInvocationOperation *operation1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
[operation1 start];
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
[operation2 start];

其中,NSBlockOperation还有一个方法addExecutionBlock:,通过这个方法可以个 Operation 添加多个执行 Block,被添加的任务会在主线程和其他线程中并发执行

运行队列

NSOperationQueue 分为主队列和其他队列。主队列可以直接获取,而自己创建的队列则属于其他队列,其他队列上的任务会在其他线程上执行。

1
2
3
4
5
6
7
8
9
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
}];
[queue addOperation:operation];

被添加到 NSOperationQueue 的任务会自动并行执行,如果想要达到串行的目的,可以将 NSOperationQueue 的参数maxConcurrentOperationCount设置为1。

相比 GCD

NSOperation 还有添加依赖,比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。

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
//1.任务一:下载图片
NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"下载图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//2.任务二:打水印
NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"打水印 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//3.任务三:上传图片
NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"上传图片 - %@", [NSThread currentThread]);
[NSThread sleepForTimeInterval:1.0];
}];
//4.设置依赖
[operation2 addDependency:operation1]; //任务二依赖任务一
[operation3 addDependency:operation2]; //任务三依赖任务二
//5.创建队列并加入任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];

此外,我们能将 KVO 应用在 NSOperation 中,可以监听一个 Operation 是否完成或取消,这样子能比 GCD 更加有效地掌控我们执行的后台任务。也可以针对 NSOperation 设置优先级。

NSOperation 作为对象,能够让开发者应用更多面向对象的方式进行管理和开发,如通过继承给 NSOperation 添加属性和方法,达到更好复用的目的。

线程同步

互斥锁

1
2
3
@synchronized(self) {
//需要执行的代码块
}

GCD 中的方法

dispatch_barrier_asyncDispatch Semaphore

同步执行

将任务添加到串行队列中进行执行。

参考资料