3

iOS多线程总结

1、iOS多线程对比

1.NSThread
每个NSThread对象对应一个线程,真正最原始的线程。
1)优点:NSThread 轻量级最低,相对简单。
2)缺点:手动管理所有的线程活动,如生命周期、线程同步、睡眠等。

2.NSOperation
自带线程管理的抽象类。
1)优点:自带线程周期管理,操作上可更注重自己逻辑。
2)缺点:面向对象的抽象类,只能实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation。

3.GCD
Grand Central Dispatch (GCD)是Apple开发的一个多核编程的解决方法。
1)优点:最高效,避开并发陷阱。
2)缺点:基于C实现。

4.选择小结
1)简单而安全的选择NSOperation实现多线程即可。
2)处理大量并发数据,又追求性能效率的选择GCD。
3) 在频繁使用多线程的程序中一般不建议使用NSThread

2、NSThread

NSThread是Objective-C 中提供的对POSIX 线程 API的封装。

直接使用线程可能会引发的一个问题是,如果你的代码和所基于的框架代码都创建自己的线程时,那么活动的线程数量有可能以指数级增长。这在大型工程中是一个常见问题。例如,在 8 核 CPU 中,你创建了 8 个线程来完全发挥 CPU 性能。然而在这些线程中你的代码所调用的框架代码也做了同样事情(因为它并不知道你已经创建的这些线程),这样会很快产生成成百上千的线程。代码的每个部分自身都没有问题,然而最后却还是导致了问题。使用线程并不是没有代价的,每个线程都会消耗一些内存和内核资源。

2.1、三种实现开启线程方式

动态实例化
NSThread *thread = [[NSThread alloc] initWithTarget:self            selector:@selector(loadImageSource:) object:imgUrl];

thread.threadPriority = 1;// 设置线程的优先级(0.0 - 1.0,1.0最高级)
[thread start];
静态实例化
[NSThread detachNewThreadSelector:@selector(loadImageSource:) toTarget:self withObject:imgUrl];
隐式实例化
[self performSelectorInBackground:@selector(loadImageSource:) withObject:imgUrl];

//在指定线程上操作
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:YES];
自定义

需要继承NSThread,并且重写main方法,然后通过start方法启动。

2.2、常见操作方式

//取消线程
- (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;

3、GCD(Grand Centra Dispatch)

通过 GCD,开发者不用再直接跟线程打交道了,只需要向队列中添加代码块即可,GCD 在后端管理着一个线程池。GCD 不仅决定着你的代码块将在哪个线程被执行,它还根据可用的系统资源对这些线程进行管理。这样可以将开发者从线程管理的工作中解放出来,通过集中的管理线程,来缓解大量线程被创建的问题。

GCD 带来的另一个重要改变是,作为开发者可以将工作考虑为一个队列,而不是一堆线程,这种并行的抽象模型更容易掌握和使用。

GCD 公开有 5 个不同的队列:运行在主线程中的 main queue,3 个不同优先级的后台队列,以及一个优先级更低的后台队列(用于 I/O)。 另外,开发者可以创建自定义队列:串行或者并行队列。自定义队列非常强大,在自定义队列中被调度的所有 block 最终都将被放入到系统的全局队列中和线程池中。

<center>image_1ap71r5b01bfa96hrgk5971noa9.png-39kB</center>

在绝大多数情况下使用默认的优先级队列就可以了。如果执行的任务需要访问一些共享的资源,那么在不同优先级的队列中调度这些任务很快就会造成不可预期的行为。这样可能会引起程序的完全挂起,因为低优先级的任务阻塞了高优先级任务,使它不能被执行。

3.1、三种线程队列类型

主线程队列 main queue

它是全局可用的serial queue,它是在应用程序主线程上执行任务的。

dispatch_get_main_queue()
并行队列global dispatch queue

又称为global dispatch queue,可以并发地执行多个任务,但是执行完成的顺序是随机的。

dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

这里的两个参数得说明一下:第一个参数用于指定优先级,分别使用DISPATCH_QUEUE_PRIORITY_HIGH和DISPATCH_QUEUE_PRIORITY_LOW两个常量来获取高和低优先级的两个queue;第二个参数目前未使用到,默认0即可。

串行队列serial queues

又称为private dispatch queues,同时只执行一个任务。Serial queue通常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然它们各自是同步执行的,但Serial queue与Serial queue之间是并发执行的。

dispatch_queue_create("minggo.app.com", NULL);

凡是自己创建的队列默认为串行队列。可在创建参数上添加DISPATCH_QUEUE_CONCURRENT,构建并行队列。

  //串行队列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", NULL);
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_SERIAL);
  //并行队列
  dispatch_queue_t queue = dispatch_queue_create("tk.bourne.testQueue", DISPATCH_QUEUE_CONCURRENT);

3.2、两种线程类型

同步线程
  dispatch_sync(<#queue#>, ^{
      //code here
      NSLog(@"%@", [NSThread currentThread]);
  });
异步线程
  dispatch_async(<#queue#>, ^{
      //code here
      NSLog(@"%@", [NSThread currentThread]);
  });

3.3、6种多线程实现

后台执行线程创建
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self loadImageSource:imgUrl1];
});
UI线程执行(只是为了测试,长时间加载内容不放在主线程)
dispatch_async(dispatch_get_main_queue(), ^{
    [self loadImageSource:imgUrl1];
});
一次性执行(常用来写单例)
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    [self loadImageSource:imgUrl1];
});
并发地执行循环迭代
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
size_t count = 10;
dispatch_apply(count, queue, ^(size_t i) {
    NSLog(@"循环执行第%li次",i);
    [self loadImageSource:imgUrl1];
});
延迟执行
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self loadImageSource:imgUrl1];
});
自定义dispatch_queue_t
dispatch_queue_t urls_queue = dispatch_queue_create("minggo.app.com", NULL);
dispatch_async(urls_queue, ^{
    [self loadImageSource:imgUrl1];
});

3.4、队列组(dispatch_group_t)

队列组可以将很多队列添加到一个组里,这样做的好处是,当这个组里所有的任务都执行完了,队列组会通过一个方法通知我们。

dispatch_group_async可以实现监听一组任务是否完成,完成后得到通知执行其他的操作。这个方法很有用,比如你执行三个下载任务,当三个任务都下载完成后你才通知界面说完成的了。下面是一段例子代码

    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, ^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"group1");
    });
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:2];
        NSLog(@"group2");
    });
    dispatch_group_async(group, queue, ^{
        [NSThread sleepForTimeInterval:3];
        NSLog(@"group3");
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"updateUi");
    });
    dispatch_release(group);

3.5、其他用法

dispatch_barrier_async的使用

dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行。

dispatch_queue_t queue = dispatch_queue_create("gcdtest.rongfzh.yc", DISPATCH_QUEUE_CONCURRENT);  
dispatch_async(queue, ^{  
    [NSThread sleepForTimeInterval:2];  
    NSLog(@"dispatch_async1");  
});  
dispatch_async(queue, ^{  
    [NSThread sleepForTimeInterval:4];  
    NSLog(@"dispatch_async2");  
});  
dispatch_barrier_async(queue, ^{  
    NSLog(@"dispatch_barrier_async");  
    [NSThread sleepForTimeInterval:4];  
  
});  
dispatch_async(queue, ^{  
    [NSThread sleepForTimeInterval:1];  
    NSLog(@"dispatch_async3");  
});  

打印结果:
2012-09-25 16:20:33.967 gcdTest[45547:11203] dispatch_async1
2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_async2
2012-09-25 16:20:35.967 gcdTest[45547:11303] dispatch_barrier_async
2012-09-25 16:20:40.970 gcdTest[45547:11303] dispatch_async3

dispatch_apply的使用

执行某个代码片段N次。

dispatch_apply(5, globalQ, ^(size_t index) {
    // 执行5次
});

4、NSOperation

NSOperation 是苹果公司对 GCD 的封装,完全面向对象,NSOperationNSOperationQueue 分别对应 GCD 的任务和队列 。操作步骤也很好理解:

  1. 将要执行的任务封装到一个 NSOperation 对象中。

  2. 将此任务添加到一个 NSOperationQueue 对象中。

4.1、NSOperation类型

NSOperation 只是一个抽象类,所以不能封装任务。但它有 2 个子类用于封装任务。分别是:NSInvocationOperationNSBlockOperation 。创建一个 Operation 后,需要调用 start 方法来启动任务,它会 默认在当前队列同步执行。当然你也可以在中途取消一个任务,只需要调用其 cancel方法即可。

//1.创建NSInvocationOperation对象
  NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];

 //2.开始执行
 [operation start];
  
  //1.创建NSBlockOperation对象
  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"%@", [NSThread currentThread]);
  }];

  //2.开始任务
  [operation start];
自定义Operation

除了上面的两种 Operation 以外,我们还可以自定义 Operation。自定义 Operation 需要继承 NSOperation 类,并实现其 main() 方法,因为在调用 start() 方法的时候,内部会调用 main() 方法完成相关逻辑。所以如果以上的两个类无法满足你的欲望的时候,你就需要自定义了。你想要实现什么功能都可以写在里面。除此之外,你还需要实现 cancel() 在内的各种方法。

如果是需要并发执行的话,还必须要重写
start

asynchronous

executing

finished
方法。

4.2、两种队列(NSOperation)

NSOperationQueue 有两种不同类型的队列:主队列和自定义队列。主队列运行在主线程之上,而自定义队列在后台执行。在两种类型中,这些队列所处理的任务都使用 NSOperation 的子类来表述。

NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];  //主队列
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定义队列

//添加一个NSOperation
[queue addOperation:operation]

//添加一组NSOperation
[queue addOperations:operations waitUntilFinished:NO

//添加一个block形式的Operation
[queue addOperationWithBlock:^{
    //执行一个Block的操作        
}];

[queue setMaxConcurrentOperationCount:1];

//单个NSOperation取消
[operation cancel]

//取消NSOperationQueue中的所有操作
[queue cancelAllOperations]

// 暂停queue  
[queue setSuspended:YES];  
  
// 继续queue  
[queue setSuspended:NO]; 

我们可以通过设置maxConcurrentOperationCount 属性来控制并发任务的数量,当设置为 1 时, 那么它就是一个串行队列。主对列默认是串行队列,这一点和 dispatch_queue_t是相似的。

NSOperation 有一个非常实用的功能,那就是添加依赖。比如有 3 个任务:A: 从服务器上下载一张图片,B:给这张图片加个水印,C:把图片返回给服务器。这时就可以用到依赖了:

//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];

5、使用选择

目前在 iOS 和 OS X 中有两套先进的同步 API 可供我们使用:NSOperation 和 GCD 。其中 GCD 是基于 C 的底层的 API ,而 NSOperation 则是 GCD 实现的 Objective-C API。 虽然 NSOperation 是基于 GCD 实现的, 但是并不意味着它是一个 GCD 的 “dumbed-down” 版本, 相反,我们可以用NSOperation 轻易的实现一些 GCD 要写大量代码的事情。 因此, NSOperationQueue 是被推荐使用的, 除非你遇到了 NSOperationQueue 不能实现的问题。

为什么优先使用NSOperationQueue而不是GCD

曾经我有一段时间我非常喜欢使用GCD来进行并发编程,因为虽然它是C的api,但是使用起来却非常简单和方便, 不过这样也就容易使开发者忘记并发编程中的许多注意事项和陷阱。
比如你可能写过类似这样的代码(这样来请求网络数据):

dispatch_async(_Queue, ^{
  
//请求数据
NSData *data = [NSData dataWithContentURL:[NSURL URLWithString:@"http://domain.com/a.png"]];

    dispatch_async(dispatch_get_main_queue(), ^{

         [self refreshViews:data];
    });
});

没错,它是可以正常的工作,但是有个致命的问题:这个任务是无法取消的 dataWithContentURL:是同步的拉取数据,它会一直阻塞线程直到完成请求,如果是遇到了超时的情况,它在这个时间内会一直占有这个线程;在这个期间并发队列就需要为其他任务新建线程,这样可能导致性能下降等问题。
因此我们不推荐这种写法来从网络拉取数据。

操作队列(operation queue)是由 GCD 提供的一个队列模型的 Cocoa 抽象。GCD 提供了更加底层的控制,而操作队列则在 GCD 之上实现了一些方便的功能,这些功能对于 app 的开发者来说通常是最好最安全的选择。NSOperationQueue相对于GCD来说有以下优点:

  • 提供了在 GCD 中不那么容易复制的有用特性。

  • 可以很方便的取消一个NSOperation的执行

  • 可以更容易的添加任务的依赖关系

  • 提供了任务的状态:isExecuteing, isFinished.


参考文章
1、谈iOS多线程(NSThread、NSOperation、GCD)编程
2、并发编程:API 及挑战
3、iOS多线程编程之Grand Central Dispatch(GCD)介绍和使用
4、Cocoa深入学习:NSOperationQueue、NSRunLoop和线程安全


流云
323 声望15 粉丝

« 上一篇
iOS测试规范
下一篇 »
单元测试