iOS 开发 多线程详解

Posted 许小罗

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS 开发 多线程详解相关的知识,希望对你有一定的参考价值。

常用的多线程开发有三种方式
1.NSThread
2.NSOperation
3.GCD

线程状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经取消)三种。其中取消状态程序可以干预设置,只要调用线程的cancel方法即可。但是需要注意在主线程中仅仅能设置线程状态,并不能真正停止当前线程,如果要终止线程必须在线程中调用exist方法,这是一个静态方法,调用该方法可以退出当前线程。


NSThread


NSThread是轻量级的多线程开发,使用起来也并不复杂,但是使用NSThread需要自己管理线程生命周期。

NSThread有两种方法创建线程:
1、使用类方法:
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument
直接将操作添加到线程中并启动。

2、使用对象方法
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument
创建一个线程对象,然后调用start方法启动线程。

通过NSThread的currentThread可以取得当前操作的线程,其中会记录线程名称name和编号number,需要注意主线程编号永远为1。多个线程虽然按顺序启动,但是实际执行未必按照顺序加载照片(loadImage:方法未必依次创建,可以通过在loadImage:中打印索引查看),因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度。

为了简化多线程开发过程,苹果官方对NSObject进行分类扩展(本质还是创建NSThread),对于简单的多线程操作可以直接使用这些扩展方法。

在后台执行一个操作,本质就是重新创建一个线程执行当前方法。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

在指定的线程上执行一个方法,需要用户创建一个线程对象。
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait

在主线程上执行一个方法(前面已经使用过)。
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait


NSOperation


使用NSOperation和NSOperationQueue进行多线程开发类似于C#中的线程池,只要将一个NSOperation(实际开中需要使用其子类NSInvocationOperation、NSBlockOperation)放到NSOperationQueue这个队列中线程就会依次启动。NSOperationQueue负责管理、执行所有的NSOperation,在这个过程中可以更加容易的管理线程总数和控制线程之间的依赖关系。

NSOperation有两个常用子类用于创建线程操作:NSInvocationOperation和NSBlockOperation,两种方式本质没有区别,但是是后者使用Block形式进行代码组织,使用相对方便。

开一个线程的方法:

-(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];
}



开多个线程下载图片
#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]];
        }];
       
    }
}
#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{
    int i=[index integerValue];

    //请求数据
   
NSData *data= [self requestData:i];
    NSLog(@"%@",[NSThread currentThread]);
    //更新UI界面,此处调用了主线程队列的方法(mainQueue是UI主线程)
   
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self updateImageWithData:data andIndex:i];
    }];
}

1、使用NSBlockOperation方法,所有的操作不必单独定义方法,同时解决了只能传递一个参数的问题。
2、调用主线程队列的addOperationWithBlock:方法进行UI更新,不用再定义一个参数实体(之前必须定义一个KCImageData解决只能传递一个参数的问题)。
3、使用NSOperation进行多线程开发可以设置最大并发线程,有效的对线程进行了控制(上面的代码运行起来你会发现打印当前进程时只有有限的线程被创建,如上面的代码设置最大线程数为5,则图片基本上是五个一次加载的)。

线程执行顺序

前面使用NSThread很难控制线程的执行顺序,但是使用NSOperation就容易多了,每个NSOperation可以设置依赖线程。假设操作A依赖于操作B,线程操作队列在启动线程时就会首先执行B操作,然后执行A。对于前面优先加载最后一张图的需求,只要设置前面的线程操作的依赖线程为最后一个操作即可。修改图片加载方法如下:

-(void)loadImageWithMultiThread{
    int count=ROW_COUNT*COLUMN_COUNT;
    //创建操作队列
    NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;//设置最大并发线程数
    
    NSBlockOperation *lastBlockOperation=[NSBlockOperation blockOperationWithBlock:^{
        [self loadImage:[NSNumber numberWithInt:(count-1)]];
    }];
    //创建多个线程用于填充图片
    for (int i=0; i<count-1; ++i) {
        //方法1:创建操作块添加到队列
        //创建多线程操作
        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        //设置依赖操作为最后一张图片加载操作
        [blockOperation addDependency:lastBlockOperation];
        
        [operationQueue addOperation:blockOperation];
        
    }
    //将最后一个图片的加载操作加入线程队列
    [operationQueue addOperation:lastBlockOperation];
}


GCD


GCD(Grand Central Dispatch)是基于C语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法。前面也说过三种开发中GCD抽象层次最高,当然是用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而是完全面向过程的这种机制相比较于前面两种多线程开发方式最显著的优点就是它对于多核运算更加有效

GCD中也有一个类似于NSOperationQueue的队列,GCD统一管理整个队列中的任务。但是GCD中的队列分为并行队列和串行队列两类:

  • 串行队列:只有一个线程,加入到队列中的操作按添加顺序依次执行。
  • 并发队列:有多个线程,操作进来之后它会将这些队列安排在可用的处理器上,同时保证先进来的任务优先处理。

其实在GCD中还有一个特殊队列就是主队列,用来执行主线程上的操作任务(从前面的演示中可以看到其实在NSOperation中也有一个主队列)。



串行队列

因为当前队列中只有一个线程,所以串行队列会按顺序执行。
使用串行队列时首先要创建一个串行队列,然后调用异步调用方法,在此方法中传入串行队列和线程操作即可自动执行。

#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);
}


并行队列

并发队列同样是使用dispatch_queue_create()方法创建,只是最后一个参数指定为DISPATCH_QUEUE_CONCURRENT进行创建,但是在实际开发中我们通常不会重新创建一个并发队列而是使用dispatch_get_global_queue()方法取得一个全局的并发队列(当然如果有多个并发队列可以使用前者创建)。下面通过并行队列演示一下多个图片的加载。代码与上面串行队列加载类似,只需要修改照片加载方法如下:

-(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执行任务的方法并非只有简单的同步调用方法和异步调用方法,还有其他一些常用方法:
1、dispatch_apply():重复执行某个任务,但是注意这个方法没有办法异步执行(为了不阻塞线程可以使用dispatch_async()包装一下再执行)。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
       
       
dispatch_apply(100,dispatch_queue_create("myThread", DISPATCH_QUEUE_PRIORITY_DEFAULT), ^(size_t index) {
          
           
NSLog(@"index == %zu and thread == %@",index,[NSThread currentThread]);
        });
       
    });

单次执行一个任务,此方法中的任务只会执行一次,重复调用也没办法重复执行(单例模式中常用此方法)。
2、dispatch_once():

static dispatch_once_t __singletonToken;
   
static id __singleton__;
   
dispatch_once( &__singletonToken, ^{
        __singleton__ = [[
self alloc] init];
    } );
   
    return __singleton__;



延迟一定的时间后执行。
3、dispatch_time():

   double delayInSeconds = 1.0;
   
__block SeanGCDViewController* bself = self;
   
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        // code
    });




  使用此方法创建的任务首先会查看队列中有没有别的任务要执行,如果有,则会等待已有任务执行完毕再执行;
  同时在此方法后添加的任务必须等待此方法中任务执行后才能执行。
(利用这个方法可以控制执行顺序,例如前面先加载最后一张图片的需求就可以先使用这个方法将最后一张图片加载的操作添加到队列,然后调用dispatch_async()添加其他图片加载任务)
4、dispatch_barrier_async():
dispatch_queue_t concurrentQueue = dispatch_queue_create("my.concurrent.queue", DISPATCH_QUEUE_CONCURRENT);
   
   
dispatch_async(concurrentQueue, ^(){
       
NSLog(@"dispatch-1");
    });
   
dispatch_async(concurrentQueue, ^(){
       
NSLog(@"dispatch-2");
    });
   
   
dispatch_barrier_async(concurrentQueue, ^(){
       
NSLog(@"dispatch-barrier");
    });
   
   
   
dispatch_async(concurrentQueue, ^(){
       
NSLog(@"dispatch-3");
    });
   
dispatch_async(concurrentQueue, ^(){
       
NSLog(@"dispatch-4");
    });

上面代码的执行步骤:
//    dispatch_barrier_async 作用是在并行队列中,等待前面两个操作并行操作完成,这里是并行输出
//   
//    dispatch-1dispatch-2
//   
//   
//    然后执行
//   
//    dispatch_barrier_async中的操作,(现在就只会执行这一个操作)执行完成后,即输出
//   
//    "dispatch-barrier
//    最后该并行队列恢复原有执行状态,继续并行执行
//   
//    dispatch-3,dispatch-4



实现对任务分组管理,如果一组任务全部完成可以通过dispatch_group_notify()方法获得完成通知(需要定义dispatch_group_t作为分组标识)
5、dispatch_group_async():

    dispatch_queue_t dispatchQueue = dispatch_queue_create("ted.queue.next", DISPATCH_QUEUE_CONCURRENT);
   
dispatch_group_t dispatchGroup = dispatch_group_create();
   
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
       
NSLog(@"dispatch-1");
    });
   
dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
       
NSLog(@"dspatch-2");
    });
   
dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
       
NSLog(@"end");
    });
//    上面的 log1 log2输出顺序不定,因为是在并行队列上执行,当并行队列全部执行完成后,最后到main队列上执行一个操作,保证“end”是最后输出。
   

线程同步


说到多线程就不得不提多线程中的锁机制,多线程操作过程中往往多个线程是并发执行的,同一个资源可能被多个线程同时访问,造成资源抢夺,这个过程中如果没有锁机制往往会造成重大问题。

要解决资源抢夺问题在ios中有常用的有两种方法:一种是使用NSLock同步锁,另一种是使用@synchronized代码块。两种方法实现原理是类似的,只是在处理上代码块使用起来更加简单。


总结

1>无论使用哪种方法进行多线程开发,每个线程启动后并不一定立即执行相应的操作,具体什么时候由系统调度(CPU空闲时就会执行)。

2>更新UI应该在主线程(UI线程)中进行,并且推荐使用同步调用,常用的方法如下:

  • - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
  • (或者-(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL) wait;方法传递主线程[NSThread mainThread])
  • [NSOperationQueue mainQueue] addOperationWithBlock:
  • dispatch_sync(dispatch_get_main_queue(), ^{})

3>NSThread适合轻量级多线程开发,控制线程顺序比较难,同时线程总数无法控制(每次创建并不能重用之前的线程,只能创建一个新的线程)。

4>对于简单的多线程开发建议使用NSObject的扩展方法完成,而不必使用NSThread。

5>可以使用NSThread的currentThread方法取得当前线程,使用 sleepForTimeInterval:方法让当前线程休眠。

6>NSOperation进行多线程开发可以控制线程总数及线程依赖关系。

7>创建一个NSOperation不应该直接调用start方法(如果直接start则会在主线程中调用)而是应该放到NSOperationQueue中启动。

8>相比NSInvocationOperation推荐使用NSBlockOperation,代码简单,同时由于闭包性使它没有传参问题。

9>NSOperation是对GCD面向对象的ObjC封装,但是相比GCD基于C语言开发,效率却更高,建议如果任务之间有依赖关系或者想要监听任务完成状态的情况下优先选择NSOperation否则使用GCD。

10>在GCD中串行队列中的任务被安排到一个单一线程执行(不是主线程),可以方便地控制执行顺序;并发队列在多个线程中执行(前提是使用异步方法),顺序控制相对复杂,但是更高效。


11>GCD中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行(如果是并行队列使用同步方法调用则会在主线程中执行)。

12>相比使用NSLock,@synchronized更加简单,推荐使用后者。


以上是关于iOS 开发 多线程详解的主要内容,如果未能解决你的问题,请参考以下文章

iOS 开发 多线程详解

iOS 开发 多线程详解

20160226.CCPP体系详解(0036天)

iOS开发之再探多线程编程:Grand Central Dispatch详解

iOS多线程详解

iOS多线程NSThread,NSOperation和GCD详解