多线程
当用户播放音频、下载资源、进行图像处理时往往希望做这些事情的时候其他操作不会被中 断或者希望这些操作过程中更加顺畅。在单线程中一个线程只能做一件事情,一件事情处理不完另一件事就不能开始,这样势必影响用户体验。早在单核处理器时期 就有多线程,这个时候多线程更多的用于解决线程阻塞造成的用户等待(通常是操作完UI后用户不再干涉,其他线程在等待队列中,CPU一旦空闲就继续执行, 不影响用户其他UI操作),其处理能力并没有明显的变化。如今无论是移动操作系统还是PC、服务器都是多核处理器,于是“并行运算”就更多的被提及。一件 事情我们可以分成多个步骤,在没有顺序要求的情况下使用多线程既能解决线程阻塞又能充分利用多核处理器运行能力。
常见的多线程的开发方式:
1.NSThread
2.NSOperation
3.GCD
NSThread是轻量级的多线程的开发,使用起来很简单。但是使用NSThread需要自己管理线程的生命周期。
启动线程有下面两种方式:
第一种:直接将操作添加到线程中并启动
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument
第二种:创建一个线程对象,然后调用start方法启动线程
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument
解决线路阻塞问题
在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用
多线程可能用户完成一个下载操作需要长时间的等待,这个过程中无法进行其他操作。下面演示一个采用多线程下载图片的过程,在这个示例中点击按钮会启动一个
线程去下载图片,下载完成后使用UIImageView将图片显示到界面中。可以看到用户点击完下载按钮后,不管图片是否下载完成都可以继续操作界面,不
会造成阻塞
[objc] view plain copy
- //
- // NSThread实现多线程
- // MultiThread
- //
- // Created by Kenshin Cui on 16-5-20.
- // Copyright (c) 2016年 zhy. All rights reserved.
- //
- #import "KCMainViewController.h"
- @interface KCMainViewController (){
- UIImageView *_imageView;
- }
- @end
- @implementation KCMainViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- [self layoutUI];
- }
- #pragma mark 界面布局
- -(void)layoutUI{
- _imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
- _imageView.contentMode=UIViewContentModeScaleAspectFit;
- [self.view addSubview:_imageView];
- UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
- button.frame=CGRectMake(50, 500, 220, 25);
- [button setTitle:@"加载图片" forState:UIControlStateNormal];
- //添加方法
- [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview:button];
- }
- #pragma mark 将图片显示到界面
- -(void)updateImage:(NSData *)imageData{
- UIImage *image=[UIImage imageWithData:imageData];
- _imageView.image=image;
- }
- #pragma mark 请求图片数据
- -(NSData *)requestData{
- //对于多线程操作建议把线程操作放到@autoreleasepool中
- @autoreleasepool {
- NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
- NSData *data=[NSData dataWithContentsOfURL:url];
- return data;
- }
- }
- #pragma mark 加载图片
- -(void)loadImage{
- //请求数据
- NSData *data= [self requestData];
- /*将数据显示到UI控件,注意只能在主线程中更新UI,
- 另外performSelectorOnMainThread方法是NSObject的分类方法,每个NSObject对象都有此方法,
- 它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法
- Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装)
- waitUntilDone:是否线程任务完成执行
- */
- [self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
- }
- #pragma mark 多线程下载图片
- -(void)loadImageWithMultiThread{
- //方法1:使用对象方法
- //创建一个线程,第一个参数是请求的操作,第二个参数是操作方法的参数
- // NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
- // //启动一个线程,注意启动一个线程并非就一定立即执行,而是处于就绪状态,当系统调度时才真正执行
- // [thread start];
- //方法2:使用类方法
- [NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
- }
- @end
上面的程序比较简单,点击“加载图片”的按钮后会启动一个新的线程,在这个线程没有完全执行完成(图片没有显示出来的时候)的时候,用户依然可以进行其他操作,在图片下载完成之后将图片显示到界面中(这个操作瞬间完成)。更新UI的时候是在UI线程进行更新。
多个线程并发执行:
大家应该注意到不管是使
用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target
withObject:(id)argument、- (instancetype)initWithTarget:(id)target
selector:(SEL)selector object:(id)argument 方法还是使用-
(void)performSelectorOnMainThread:(SEL)aSelector
withObject:(id)arg
waitUntilDone:(BOOL)wait方法都只能传一个参数,由于更新图片需要传递UIImageView的索引和图片数据,因此这里不妨定
义一个类保存图片索引和图片数据以供后面使用。
KCImageData.h
[objc] view plain copy
- <span style="font-size:14px;">/
- // KCImageData.h
- // MultiThread
- //
- // Created by Kenshin Cui on 16-5-20.
- </span><span style="font-size:14px;"></span><pre name="code" class="objc"><span style="font-size:14px;">// Copyright (c) 2016年 zhy. All rights reserved.
- //
- #import <Foundation/Foundation.h>
- @interface KCImageData : NSObject
- #pragma mark 索引
- @property (nonatomic,assign) int index;
- #pragma mark 图片数据
- @property (nonatomic,strong) NSData *data;
- @end</span>
接下来将创建多个UIImageView并创建多个线程用于往UIImageView中填充图片。
KCMainViewController.m
[objc] view plain copy
- //
- // NSThread实现多线程
- // MultiThread
- //
- // Created by Kenshin Cui on 16-5-20.
- // Copyright (c) 2016年 zhy. All rights reserved.
- //
- #import "KCMainViewController.h"
- #import "KCImageData.h"
- #define ROW_COUNT 5
- #define COLUMN_COUNT 3
- #define ROW_HEIGHT 100
- #define ROW_WIDTH ROW_HEIGHT
- #define CELL_SPACING 10
- @interface KCMainViewController (){
- NSMutableArray *_imageViews;
- }
- @end
- @implementation KCMainViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- [self layoutUI];
- }
- #pragma mark 界面布局
- -(void)layoutUI{
- //创建多个图片控件用于显示图片
- _imageViews=[NSMutableArray array];
- for (int r=0; r<ROW_COUNT; r++) {
- for (int c=0; c<COLUMN_COUNT; c++) {
- UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
- imageView.contentMode=UIViewContentModeScaleAspectFit;
- // imageView.backgroundColor=[UIColor redColor];
- [self.view addSubview:imageView];
- [_imageViews addObject:imageView];
- }
- }
- UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
- button.frame=CGRectMake(50, 500, 220, 25);
- [button setTitle:@"加载图片" forState:UIControlStateNormal];
- //添加方法
- [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview:button];
- }
- #pragma mark 将图片显示到界面
- -(void)updateImage:(KCImageData *)imageData{
- UIImage *image=[UIImage imageWithData:imageData.data];
- UIImageView *imageView= _imageViews[imageData.index];
- imageView.image=image;
- }
- #pragma mark 请求图片数据
- -(NSData *)requestData:(int )index{
- //对于多线程操作建议把线程操作放到@autoreleasepool中
- @autoreleasepool {
- NSURL *url=[NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"];
- NSData *data=[NSData dataWithContentsOfURL:url];
- return data;
- }
- }
- #pragma mark 加载图片
- -(void)loadImage:(NSNumber *)index{
- // NSLog(@"%i",i);
- //currentThread方法可以取得当前操作线程
- NSLog(@"current thread:%@",[NSThread currentThread]);
- int i=[index integerValue];
- // NSLog(@"%i",i);//未必按顺序输出
- NSData *data= [self requestData:i];
- KCImageData *imageData=[[KCImageData alloc]init];
- imageData.index=i;
- imageData.data=data;
- [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
- }
- #pragma mark 多线程下载图片
- -(void)loadImageWithMultiThread{
- //创建多个线程用于填充图片
- for (int i=0; i<ROW_COUNT*COLUMN_COUNT; ++i) {
- // [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
- NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
- thread.name=[NSString stringWithFormat:@"myThread%i",i];//设置线程名称
- [thread start];
- }
- }
- @end
NSOperation有两个常用子类用于创建线程操作:NSInvocationOperation和NSBlockOperation,两种方式本质没有区别,但是是后者使用Block形式进行代码组织,使用相对方便
NSInvocationOperation
首先使用NSInvocationOperation进行一张图片的加载演示,整个过程就是:创建一个操作,在这个操作中指定调用方法和参数,然后加入到操作队列。其他代码基本不用修改,直接修加载图片方法如下:
[objc] view plain copy
- -(void)loadImageWithMultiThread{
- /*创建一个调用操作
- object:调用方法参数
- */
- NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
- //创建完NSInvocationOperation对象并不会调用,它由一个start方法启动操作,但是注意如果直接调用start方法,则此操作会在主线程中调用,一般不会这么操作,而是添加到NSOperationQueue中
- // [invocationOperation start];
- //创建操作队列
- NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
- //注意添加到操作队后,队列会开启一个线程执行此操作
- [operationQueue addOperation:invocationOperation];
- }
NSBlockOperation
下面采用NSBlockOperation创建多个线程加载图片。
[objc] view plain copy
- #import "KCMainViewController.h"
- #import "KCImageData.h"
- #define ROW_COUNT 5
- #define COLUMN_COUNT 3
- #define ROW_HEIGHT 100
- #define ROW_WIDTH ROW_HEIGHT
- #define CELL_SPACING 10
- @interface KCMainViewController (){
- NSMutableArray *_imageViews;
- NSMutableArray *_imageNames;
- }
- @end
- @implementation KCMainViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- [self layoutUI];
- }
- #pragma mark 界面布局
- -(void)layoutUI{
- //创建多个图片控件用于显示图片
- _imageViews=[NSMutableArray array];
- for (int r=0; r<ROW_COUNT; r++) {
- for (int c=0; c<COLUMN_COUNT; c++) {
- UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
- imageView.contentMode=UIViewContentModeScaleAspectFit;
- // imageView.backgroundColor=[UIColor redColor];
- [self.view addSubview:imageView];
- [_imageViews addObject:imageView];
- }
- }
- UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
- button.frame=CGRectMake(50, 500, 220, 25);
- [button setTitle:@"加载图片" forState:UIControlStateNormal];
- //添加方法
- [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview:button];
- //创建图片链接
- _imageNames=[NSMutableArray array];
- for (int i=0; i<IMAGE_COUNT; i++) {
- [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
- }
- }
- #pragma mark 将图片显示到界面
- -(void)updateImageWithData:(NSData *)data andIndex:(int )index{
- UIImage *image=[UIImage imageWithData:data];
- UIImageView *imageView= _imageViews[index];
- imageView.image=image;
- }
- #pragma mark 请求图片数据
- -(NSData *)requestData:(int )index{
- //对于多线程操作建议把线程操作放到@autoreleasepool中
- @autoreleasepool {
- NSURL *url=[NSURL URLWithString:_imageNames[index]];
- NSData *data=[NSData dataWithContentsOfURL:url];
- return data;
- }
- }
- #pragma mark 加载图片
- -(void)loadImage:(NSNumber *)index{
- int i=[index integerValue];
- //请求数据
- NSData *data= [self requestData:i];
- NSLog(@"%@",[NSThread currentThread]);
- //更新UI界面,此处调用了主线程队列的方法(mainQueue是UI主线程)
- [[NSOperationQueue mainQueue] addOperationWithBlock:^{
- [self updateImageWithData:data andIndex:i];
- }];
- }
- #pragma mark 多线程下载图片
- -(void)loadImageWithMultiThread{
- int count=ROW_COUNT*COLUMN_COUNT;
- //创建操作队列
- NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
- operationQueue.maxConcurrentOperationCount=5;//设置最大并发线程数
- //创建多个线程用于填充图片
- for (int i=0; i<count; ++i) {
- //方法1:创建操作块添加到队列
- // //创建多线程操作
- // NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
- // [self loadImage:[NSNumber numberWithInt:i]];
- // }];
- // //创建操作队列
- //
- // [operationQueue addOperation:blockOperation];
- //方法2:直接使用操队列添加操作
- [operationQueue addOperationWithBlock:^{
- [self loadImage:[NSNumber numberWithInt:i]];
- }];
- }
- }
- @end
对比之前NSThread加载张图片很发现核心代码简化了不少,这里着重强调两点:
- 使用NSBlockOperation方法,所有的操作不必单独定义方法,同时解决了只能传递一个参数的问题。
- 调用主线程队列的addOperationWithBlock:方法进行UI更新,不用再定义一个参数实体(之前必须定义一个KCImageData解决只能传递一个参数的问题)。
- 使用NSOperation进行多线程开发可以设置最大并发线程,有效的对线程进行了控制(上面的代码运行起来你会发现打印当前进程时只有有限的线程被创建,如上面的代码设置最大线程数为5,则图片基本上是五个一次加载的)。
GCD是基于C语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法。前面也说过三种开发中GCD抽象层次最高,当然是用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而是完全面向过程的。
GCD中也有一个类似于NSOperationQueue的队列,GCD统一管理整个队列中的任务。但是GCD中的队列分为并行队列和串行队列两类:
- 串行队列:只有一个线程,加入到队列中的操作按添加顺序依次执行。
- 并发队列:有多个线程,操作进来之后它会将这些队列安排在可用的处理器上,同时保证先进来的任务优先处理。
其实在GCD中还有一个特殊队列就是主队列,用来执行主线程上的操作任务
串行队列
使用串行队列时首先要创建一个串行队列,然后调用异步调用方法,在此方法中传入串行队列和线程操作即可自动执行。下面使用线程队列演示图片的加载过程,你会发现多张图片会按顺序加载,因为当前队列中只有一个线程。
[objc] view plain copy
- #import "KCMainViewController.h"
- #import "KCImageData.h"
- #define ROW_COUNT 5
- #define COLUMN_COUNT 3
- #define ROW_HEIGHT 100
- #define ROW_WIDTH ROW_HEIGHT
- #define CELL_SPACING 10
- @interface KCMainViewController (){
- NSMutableArray *_imageViews;
- NSMutableArray *_imageNames;
- }
- @end
- @implementation KCMainViewController
- - (void)viewDidLoad {
- [super viewDidLoad];
- [self layoutUI];
- }
- #pragma mark 界面布局
- -(void)layoutUI{
- //创建多个图片控件用于显示图片
- _imageViews=[NSMutableArray array];
- for (int r=0; r<ROW_COUNT; r++) {
- for (int c=0; c<COLUMN_COUNT; c++) {
- UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)];
- imageView.contentMode=UIViewContentModeScaleAspectFit;
- // imageView.backgroundColor=[UIColor redColor];
- [self.view addSubview:imageView];
- [_imageViews addObject:imageView];
- }
- }
- UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
- button.frame=CGRectMake(50, 500, 220, 25);
- [button setTitle:@"加载图片" forState:UIControlStateNormal];
- //添加方法
- [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
- [self.view addSubview:button];
- //创建图片链接
- _imageNames=[NSMutableArray array];
- for (int i=0; i<ROW_COUNT*COLUMN_COUNT; i++) {
- [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]];
- }
- }
- #pragma mark 将图片显示到界面
- -(void)updateImageWithData:(NSData *)data andIndex:(int )index{
- UIImage *image=[UIImage imageWithData:data];
- UIImageView *imageView= _imageViews[index];
- imageView.image=image;
- }
- #pragma mark 请求图片数据
- -(NSData *)requestData:(int )index{
- NSURL *url=[NSURL URLWithString:_imageNames[index]];
- NSData *data=[NSData dataWithContentsOfURL:url];
- return data;
- }
- #pragma mark 加载图片
- -(void)loadImage:(NSNumber *)index{
- //如果在串行队列中会发现当前线程打印变化完全一样,因为他们在一个线程中
- NSLog(@"thread is :%@",[NSThread currentThread]);
- int i=[index integerValue];
- //请求数据
- NSData *data= [self requestData:i];
- //更新UI界面,此处调用了GCD主线程队列的方法
- dispatch_queue_t mainQueue= dispatch_get_main_queue();
- dispatch_sync(mainQueue, ^{
- [self updateImageWithData:data andIndex:i];
- });
- }
- #pragma mark 多线程下载图片
- -(void)loadImageWithMultiThread{
- int count=ROW_COUNT*COLUMN_COUNT;
- /*创建一个串行队列
- 第一个参数:队列名称
- 第二个参数:队列类型
- */
- dispatch_queue_t serialQueue=dispatch_queue_create("myThreadQueue1", DISPATCH_QUEUE_SERIAL);//注意queue对象不是指针类型
- //创建多个线程用于填充图片
- for (int i=0; i<count; ++i) {
- //异步执行队列任务
- dispatch_async(serialQueue, ^{
- [self loadImage:[NSNumber numberWithInt:i]];
- });
- }
- //非ARC环境请释放
- // dispatch_release(seriQueue);
- }
- @end
在上面的代码中更新UI还使用了GCD方法的主线程队列dispatch_get_main_queue(),其实这与前面两种主线程更新UI没有本质的区别。
并发队列
并发队列同样是使用dispatch_queue_create()方法创建,只是最后一个参数指定为DISPATCH_QUEUE_CONCURRENT进
行创建,但是在实际开发中我们通常不会重新创建一个并发队列而是使用dispatch_get_global_queue()方法取得一个全局的并发队列
(当然如果有多个并发队列可以使用前者创建)。下面通过并行队列演示一下多个图片的加载。代码与上面串行队列加载类似,只需要修改照片加载方法如下:
[objc] view plain copy
- -(void)loadImageWithMultiThread{
- int count=ROW_COUNT*COLUMN_COUNT;
- /*取得全局队列
- 第一个参数:线程优先级
- 第二个参数:标记参数,目前没有用,一般传入0
- */
- dispatch_queue_t globalQueue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
- //创建多个线程用于填充图片
- for (int i=0; i<count; ++i) {
- //异步执行队列任务
- dispatch_async(globalQueue, ^{
- [self loadImage:[NSNumber numberWithInt:i]];
- });
- }
- }
GCD执行任务的方法并非只有简单的同步调用方法和异步调用方法,还有其他一些常用方法:
- dispatch_apply():重复执行某个任务,但是注意这个方法没有办法异步执行(为了不阻塞线程可以使用dispatch_async()包装一下再执行)。
- dispatch_once():单次执行一个任务,此方法中的任务只会执行一次,重复调用也没办法重复执行(单例模式中常用此方法)。
- dispatch_time():延迟一定的时间后执行。
- dispatch_barrier_async():
使用此方法创建的任务首先会查看队列中有没有别的任务要执行,如果有,则会等待已有任务执行完毕再执行;同时在此方法后添加的任务必须等待此方法中任务执
行后才能执行。(利用这个方法可以控制执行顺序,例如前面先加载最后一张图片的需求就可以先使用这个方法将最后一张图片加载的操作添加到队列,然后调用
dispatch_async()添加其他图片加载任务) - dispatch_group_async():实现对任务分组管理,如果一组任务全部完成可以通过dispatch_group_notify()方法获得完成通知(需要定义dispatch_group_t作为分组标识)。