iOS开发-多线程学习

iOS开发中常用的几种多线程方案,简单做个小结,方便日后查阅。

  • Pthreads
  • NSThead
  • GCD
  • NSOperation & NSOpeartionQueue

Pthreads#

这种方式不用介绍(我也不太会使用),一般ios开发里也用不上,这是在很多操作系统中都通用的。使用方法大概如下:
#import
创建线程并执行任务:
void *run(void *data){
for (int i = 0; i<1000; i++) {
NSLog(@"touchesBegan:%d-----%@", i, [NSThread currentThread]);
}
return NULL;
}

  -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
  {
  // 创建线程
    pthread_t myRestrict;
    pthread_create(&myRestrict, NULL, run, NULL);
  }

通过log可以看到:

2016-03-08 18:27:04.936 testPthread[4859:1637201] touchesBegan:0-----{number = 3, name = (null)}
2016-03-08 18:27:04.937 testPthread[4859:1637201] touchesBegan:1-----{number = 3, name = (null)}
2016-03-08 18:27:04.937 testPthread[4859:1637201] touchesBegan:2-----{number = 3, name = (null)}
2016-03-08 18:27:04.937 testPthread[4859:1637201] touchesBegan:3-----{number = 3, name = (null)}
2016-03-08 18:27:04.937 testPthread[4859:1637201] touchesBegan:4-----{number = 3, name = (null)}

这种方式虽然创建了一个线程,但并没有销毁,我们需要手动管理线程的生命周期。随意感受一下就好了。。。

NSThead#

优点:NSThread 比其他几个轻量级。
缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。

创建和启动线程的3种方式:###

1.先创建后启动:####

// 创建线程
NSThread *thread = [[NSThread alloc] initWithTarget:self     selector:@selector(download:) object:@"http://b.png"];
thread.name = @"下载线程";
// 启动线程(调用self的download方法)
[thread start];

selector :线程执行的方法,这个selector只能有一个参数,而且不能有返回值。
target :selector消息发送的对象
argument:传输给target的唯一参数,也可以是nil

2.创建并自动启动线程:####

 [NSThread detachNewThreadSelector:@selector(download:) toTarget:self withObject:@"http://a.jpg"];

3.使用NSObject方法创建线程并自动启动:####

[self performSelectorInBackground:@selector(download:) withObject:@"http://c.gif"];

其他常见方法:

//获得当前线程
+(NSThread *)currentThread;

 //获得主线程   
 +(NSThread *)mainThread;

 //睡眠(暂停)线程  
 +(void)sleepUntilDate:(NSDate *)date;    
 +(void)sleepForTimeInterval:(NSTimeInterval)ti;

//设置和获取线程名称    
 -(void)setName:(NSString *)n;   
 -(NSString *)name;

GCD#

Grand Central Dispatch 简称(GCD)是苹果公司开发的技术,以优化的应用程序支持多核心处理器和其他的对称多处理系统的系统。这建立在任务并行执行的线程池模式的基础上的。它首次发布在Mac OS X 10.6 ,iOS 4及以上也可用。GCD的工作原理是:让程序平行排队的特定任务,根据可用的处理资源,安排他们在任何可用的处理器核心上执行任务。它是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的CPU内核(比如双核、四核),最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理,我们只需要告诉干什么就行。

任务和队列###

任务:

需要执行什么操作,一个任务可以是一个函数(function)或者是一个block。任务有两种执行方式:同步执行异步执行,它们区别在于是否会阻塞当前线程,直到任务执行完。如果是 同步(sync) 操作,它会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续往下运行。如果是 异步(async)操作,当前线程会直接往下执行,它不会阻塞当前线程。

创建任务方式:

同步任务:会阻塞当前线程,并且不具备开启新线程的能力。

  dispatch_sync(<#queue#>, ^{  
    //code here  
  });

异步任务:不会阻塞当前线程,并且具有开启新线程的能力。

dispatch_async(<#queue#>, ^{ 
 //code here
});

队列:

存放任务。有串行队列和并行队列两种。
串行队列:GCD中的FIFO队列称为dispatch queue,它可以保证先进来的任务先得到执行,然后再取下一个,一个接着一个执行。
并行队列:放到并行队列的任务可以并发执行。不过需要注意,GCD 会根据系统资源控制并行的数量,所以如果任务很多,它并不会让所有任务同时执行。

创建队列方式

主队列:这是一个特殊的串行队列,主要用于刷新UI界面,处理UI控件的事件。如下:
dispatch_queue_t queue = dispatch_get_main_queue();

自己创建的队列: 自己可以创建串行队列,也可以创建 并行队列。它有两个参数:
第一个参数:标识符,用于debug的时候标识唯一的队列。
第二个参数:用来表示创建的队列是串行的还是并行的,传入 DISPATCH_QUEUE_SERIALNULL 表示创建串行队列。传入 DISPATCH_QUEUE_CONCURRENT 表示创建并行队列。如下:

 //串行队列   
 dispatch_queue_t queue =dispatch_queue_create("serial.testQueue", NULL);

 dispatch_queue_t queue = dispatch_queue_create("serial.testQueue", DISPATCH_QUEUE_SERIAL); 

//并行队列    
dispatch_queue_t queue = dispatch_queue_create("concurrent.testQueue", DISPATCH_QUEUE_CONCURRENT);

全局并发队列: 可以让任务并发执行,只要是并行任务一般都加入到这个队列中。

 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

下面的例子能更好的理解同步异步和各种队列的使用:
例1:

  //sync -- 主队列(不能用---会卡死) 
-(void)testSyncMainQueue{
    NSLog(@"download之前----%@",[NSThread currentThread]);    
  // 1.主队列(添加到主队列中的任务,都会自动放到主线程中去执行)  
    dispatch_queue_t queue = dispatch_get_main_queue();        
  // 2.异步执行    
    dispatch_sync(queue, ^{        
    NSLog(@"-----download1---%@", [NSThread currentThread]);    
    });    
    dispatch_sync(queue, ^{        
    NSLog(@"-----download2---%@", [NSThread currentThread]);    
    });    
  NSLog(@"download之后------");
} 

打印结果:

2016-03-09 10:37:47.450 testGCD[5725:2260551] download之前----{number = 1, name = main}

只打印了第一句后主线程就被卡死了,什么界面操作都做不了。这是因为dispatch_sync同步任务会阻塞当前的主线程,然后把block中的任务放到主队列queue当中,可是添加到主队列中的任务,都会自动放到主线程中去执行,但是主线程已被阻塞,所以block中download无法执行,dispatch_sync就会一直阻塞主线程,造成死锁。

例2:
- (void)testAsyncSerialQueue{
// 1.创建一个串行队列
dispatch_queue_t queue = dispatch_queue_create("testAsync.SerialQueue", NULL);

  NSLog(@"download之前----%@",[NSThread currentThread]);   
 // 2.异步执行    
  dispatch_async(queue, ^{                
    NSLog(@"async download之前----%@",[NSThread currentThread]); 
    dispatch_sync(queue, ^{            
    NSLog(@"sync download ----%@",[NSThread currentThread]);       
 });               
   NSLog(@"async download 之后----%@",[NSThread currentThread]);      
});   

  dispatch_async(queue, ^{         
    NSLog(@"async download2----%@",[NSThread currentThread]);   
  });   
 dispatch_async(queue, ^{       
   NSLog(@"async download3----%@",[NSThread currentThread]);    
});    
  NSLog(@"async download 之后----%@",[NSThread currentThread]);
}

打印结果:

2016-03-09 11:00:27.419 testGCD[5876:2284715] download之前----{number = 1, name = main}
2016-03-09 11:00:27.420 testGCD[5876:2284748] sync download之前----{number = 2, name = (null)}
2016-03-09 11:00:27.420 testGCD[5876:2284715] async download 之后----{number = 1, name = main}

可以看到sync download----async download之后----还有async download2----async download3----这几个没有被打印出来。这是为啥?

分析:

  1. 首先必须明白,dispatch_queue_create创建一个新队列queue, 参数DISPATCH_QUEUE_SERIALNULL 表示创建的这是一个串行队列。
  2. 接着打印download之前----正常;
  3. dispatch_async异步执行,当前线程不会被阻塞,且具备开启新线程能力,所以同时有了两条线程。一条当前线程打印出了async download 之后----,正常。另外一条线程执行block里的任务,打印sync download之前----,也是正常。因为这两条线程是并行,所以并不会相互影响,顺序前后也不一定。
  4. 之后dispatch_sync同步执行,同步任务会阻塞当前所在的线程,直到sync里的任务执行完毕后才会继续往下。然后与例1相似,sync把block中的任务放到队列queue当中,可是queue是一个串行队列,串行队列是任务一个接着一个执行,一次只能执行一个任务,所以sycn的block里的任务就必须等到前一个任务执行完毕,可是,前一个正在执行的任务正是被sync阻塞的那个。于是这里又出现了死锁现象。sycn所在的线程就被卡死了。sync download----sync download之后----这两句代码也就不会被打印出来了。明白不?后面的两个dispatch_async任务同样道理。

例3:

  -(void)testAsyncGlobalQueue{    
    // 并发队列    
    dispatch_queue_t queue       =dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);        
    //异步 执行    
    dispatch_async(queue, ^{        
      NSLog(@"-----download1---%@", [NSThread currentThread]);    
    });   
    dispatch_async(queue, ^{        
      NSLog(@"-----download2---%@", [NSThread currentThread]);   
   });    
    dispatch_async(queue, ^{        
    NSLog(@"-----download3---%@", [NSThread currentThread]);   
   });    
    dispatch_async(queue, ^{        
    NSLog(@"-----download4---%@", [NSThread currentThread]);   
   });   
    dispatch_async(queue, ^{       
     NSLog(@"-----download5---%@", [NSThread currentThread]);
    });
}

打印结果:

2016-03-09 11:49:44.439 testGCD[6039:2341953] -----download5---{number = 6, name = (null)}
2016-03-09 11:49:44.439 testGCD[6039:2341800] -----download4---{number = 5, name = (null)}

2016-03-09 11:49:44.439 testGCD[6039:2341755] -----download2---{number = 2, name = (null)}
2016-03-09 11:49:44.439 testGCD[6039:2341756] -----download1---{number = 3, name = (null)}
2016-03-09 11:49:44.439 testGCD[6039:2341799] -----download3---{number = 4, name = (null)}

从这个结果可以看出,异步执行任务,一般同时开启多条线程,且彼此之间不会相互影响,并发执行,哪条线程先后执行都不一定。

例4:

 -(void)testSyncGlobalQueue{    
  // 全局并发队列    
   dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); 
 // 同步执行    
   dispatch_sync(queue, ^{        
    NSLog(@"-----download1---%@", [NSThread currentThread]);    
  });    
   dispatch_sync(queue, ^{        
      NSLog(@"-----download2---%@", [NSThread currentThread]);    
   });    
  dispatch_sync(queue, ^{        
      NSLog(@"-----download3---%@", [NSThread currentThread]);    
  });    
  dispatch_sync(queue, ^{        
      NSLog(@"-----download4---%@", [NSThread currentThread]);   
 });    
  dispatch_sync(queue, ^{        
    NSLog(@"-----download5---%@", [NSThread currentThread]);    
  });
}

打印结果:

2016-03-09 12:07:51.493 testGCD[6150:2359827] -----download1---{number = 1, name = main}
2016-03-09 12:07:51.494 testGCD[6150:2359827] -----download2---{number = 1, name = main}
2016-03-09 12:07:51.494 testGCD[6150:2359827] -----download3---{number = 1, name = main}
2016-03-09 12:07:51.494 testGCD[6150:2359827] -----download4---{number = 1, name = main}
2016-03-09 12:07:51.494 testGCD[6150:2359827] -----download5---{number = 1, name = main}

可以看出,同步任务不会创建新的线程,串行执行一次只能执行一个任务,一个接着一个,按顺序执行。并且这个全局并发队列失去了并发功能。

例5:

-(void)testSyncSerialQueue{    
  //串行队列    
    dispatch_queue_t queue = dispatch_queue_create("testSync.SerialQueue", NULL);        
  //同步执行    
    dispatch_sync(queue, ^{       
         NSLog(@"-----download1---%@", [NSThread currentThread]); 
     });  
    dispatch_sync(queue, ^{      
        NSLog(@"-----download2---%@", [NSThread currentThread]);  
    });  
    dispatch_sync(queue, ^{       
       NSLog(@"-----download3---%@", [NSThread currentThread]);  
    });    
    dispatch_sync(queue, ^{      
      NSLog(@"-----download4---%@", [NSThread currentThread]);    
    });    
    dispatch_sync(queue, ^{        
      NSLog(@"-----download5---%@", [NSThread currentThread]);          
    });
 }

打印结果:

2016-03-09 12:20:13.188 testGCD[6210:2373033] -----download1---{number = 1, name = main}
2016-03-09 12:20:13.189 testGCD[6210:2373033] -----download2---{number = 1, name = main}
2016-03-09 12:20:13.189 testGCD[6210:2373033] -----download3---{number = 1, name = main}
2016-03-09 12:20:13.189 testGCD[6210:2373033] -----download4---{number = 1, name = main}
2016-03-09 12:20:13.189 testGCD[6210:2373033] -----download5---{number = 1, name = main}

可以看出,同步任务不会创建新的线程,串行队列执行一次只能执行一个任务,一个接着一个,按顺序执行。

队列组#####

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

  -(void)testGCDQueueGroup{    
  {        
    // 1.队列组        
    dispatch_group_t group = dispatch_group_create();        
    // 创建队列        
  dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);                
   
    // 2.使用队列组的异步方法,下载库里的图片        
   __block UIImage *image1 = nil;   

   dispatch_group_async(group, queue, ^{            
       NSURL *url1 = [NSURL URLWithString:@"http://i2.hoopchina.com.cn/u/1306/04/318/17056318/62affa38_530x.jpg"];           
       NSData *data1 = [NSData dataWithContentsOfURL:url1];                    
      image1 = [UIImage imageWithData:data1];       
  });  
         
   // 3.使用队列组的异步方法,下载百度的logo       
   __block UIImage *image2 = nil;        
    dispatch_group_async(group, queue, ^{            
        NSURL *url2 = [NSURL URLWithString:@"http://su.bdimg.com/static/superplus/img/logo_white_ee663702.png"];            
        NSData *data2 = [NSData dataWithContentsOfURL:url2];        
        image2 = [UIImage imageWithData:data2];        
    });    
        
  // 4.合并图片      
  dispatch_group_notify(group, queue, ^{   
     
  // 开启一个位图上下文     
   UIGraphicsBeginImageContextWithOptions(image1.size, NO, 0.0);                       
   // 绘制第1张图片            
  CGFloat image1W = image1.size.width;           
  CGFloat image1H = image1.size.height;           
  [image1 drawInRect:CGRectMake(0, 0, image1W, image1H)];                                  

  // 绘制第2张图片           
 CGFloat image2W = image2.size.width * 0.5;           
 CGFloat image2H = image2.size.height * 0.5;          
 CGFloat image2Y = image1H - image2H;            
  [image2 drawInRect:CGRectMake(0, image2Y, image2W, image2H)];                        

    // 得到上下文中的图片            
    UIImage *fullImage = UIGraphicsGetImageFromCurrentImageContext();                       
 // 结束上下文            
  UIGraphicsEndImageContext();                        
// 5.回到主线程显示图片      
    dispatch_async(dispatch_get_main_queue(), ^{ 
      self.imageView.image = fullImage;            
    });       
   });    
  }
}

这个例子就是一个简单的使用方法,在队列里分别下载两张图片,等两张图片下载完成后,合并成一张图片。保证执行完组里面的所有任务之后,再执行notify函数里面的block。最后再回到主线程,更新界面并显示出来。

还有几点从其他文章看到,贴一下:

dispatch_barrier_async(queue, ^{ });这个方法重点是你传入的 queue,当你传入的 queue 是通过 DISPATCH_QUEUE_CONCURRENT 参数自己创建的 queue 时,这个方法会阻塞这个 queue(注意是阻塞 queue ,而不是阻塞当前线程),一直等到这个 queue 中排在它前面的任务都执行完成后才会开始执行自己,自己执行完毕后,再会取消阻塞,使这个 queue 中排在它后面的任务继续执行。如果你传入的是其他的 queue, 那么它就和 dispatch_async 一样了。 例子代码如下:

-(void)testGCDBarrierAsync{
NSLog(@"begin ---%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("testGCD.BarrierAsync", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"dispatch_async1");
});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:5];
    NSLog(@"dispatch_async2");
});
dispatch_barrier_async(queue, ^{
    NSLog(@"dispatch_barrier_async");
    [NSThread sleepForTimeInterval:5];
    
});
dispatch_async(queue, ^{
    [NSThread sleepForTimeInterval:1];
    NSLog(@"dispatch_async3");   
});
}

打印结果:

2016-03-09 14:42:14.239 testGCD[7320:2534975] begin ---{number = 1, name = main}
2016-03-09 14:42:16.244 testGCD[7320:2535015] dispatch_async1
2016-03-09 14:42:19.241 testGCD[7320:2535016] dispatch_async2
2016-03-09 14:42:19.241 testGCD[7320:2535016] dispatch_barrier_async
2016-03-09 14:42:25.247 testGCD[7320:2535016] dispatch_async3

请注意执行的时间,可以看到dispatch_barrier_async是在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行。

dispatch_barrier_sync(queue, ^{ });这个方法的使用和上一个一样,传入 自定义的并发队列(DISPATCH_QUEUE_CONCURRENT),它和上一个方法一样的阻塞 queue,不同的是 这个方法还会 阻塞当前线程。如果你传入的是其他的 queue, 那么它就和 dispatch_sync 一样了。例子如下:

-(void)testGCDBarrierSync{
NSLog(@"begin ---%@",[NSThread currentThread]);
dispatch_queue_t queue = dispatch_queue_create("testGCD.BarrierSync", DISPATCH_QUEUE_CONCURRENT);
dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:2];
    NSLog(@"dispatch_Sync1");
});
dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:5];
    NSLog(@"dispatch_Sync2");
});
dispatch_barrier_sync(queue, ^{
    NSLog(@"dispatch_barrier_Sync");
    [NSThread sleepForTimeInterval:5];
    
});
dispatch_sync(queue, ^{
    [NSThread sleepForTimeInterval:3];
    NSLog(@"dispatch_Sync3");
});
}

打印结果:

2016-03-09 14:58:10.146 testGCD[7449:2557946] begin ---{number = 1, name = main}
2016-03-09 14:58:12.151 testGCD[7449:2557946] dispatch_Sync1
2016-03-09 14:58:17.153 testGCD[7449:2557946] dispatch_Sync2
2016-03-09 14:58:17.154 testGCD[7449:2557946] dispatch_barrier_Sync
2016-03-09 14:58:25.157 testGCD[7449:2557946] dispatch_Sync3

从这个打印结果看不出什么,囧。。。但是此时如果UI上有什么交互的话,就不行了。自己可以另行实验。

dispatch_apply(times, queue, ^(size_t index) { }); 表现得就像一个 for 循环,但它能并发地执行不同的迭代。这个函数是同步的,所以和普通的 for 循环一样,它只会在所有工作都完成后才会返回。 简单例子如下:

    -(void)testGCDForApply{
        dispatch_queue_t queue =   dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

        dispatch_apply(5, queue, ^(size_t index) {
        // 执行5次
        NSLog(@"testGCDForApply");
      });
    }

打印出来的结果如下:

2016-03-09 15:15:01.340 testGCD[7593:2586389] testGCDForApply
2016-03-09 15:15:01.340 testGCD[7593:2586391] testGCDForApply
2016-03-09 15:15:01.340 testGCD[7593:2584865] testGCDForApply
2016-03-09 15:15:01.340 testGCD[7593:2586252] testGCDForApply
2016-03-09 15:15:01.340 testGCD[7593:2586390] testGCDForApply

其他用法#####

延迟执行:所谓延迟执行就是延时一段时间再执行某段代码。下面是一些常用方法:
1.[NSThread sleepForTimeInterval:3];
虽然这种方法能起到延迟执行的作用,但别用,因为会卡住当前线程。
2.[self performSelector:@selector(download:) withObject:@"http://abc.jpg" afterDelay:5];
5秒后自动调用download:方法,并且传递参数。
3.NSTimer: 也能延迟执行。
[NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(download:) userInfo:@"http://abc.jpg" repeats:NO];
4.GCD中的dispatch_after

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);        

double delay = 3;        

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), queue, ^{        
NSLog(@"------download------%@", [NSThread currentThread]);      
});

线程安全:为了防止多个线程抢夺同一个资源造成的数据安全问题。线程安全的代码能在多线程或并发任务中被安全的调用,而不会导致任何问题(数据损坏,崩溃,等)。线程不安全的代码在某个时刻只能在一个上下文中运行。也有两种实现方法:

  1. 互斥锁:给需要同步的代码块加一个互斥锁,就可以保证每次只有一个线程访问此代码块。
//()小括号里面放的是锁对象 
@synchronized(self) {   
 //code here
}

2.同步执行:把多个线程需要执行的代码添加到同一个串行队列中,由于串行队列一次只能执行一个任务,所以也能实现线程同步的作用。
- (void)saleTicket2
{
while (1) {
dispatch_sync(ticketQueue, ^{
NSInteger count = self.leftTicketCount;
if (count > 0) {

            [NSThread sleepForTimeInterval:0.05];
            
            self.leftTicketCount = self.leftTicketCount = count - 1;;
            NSLog(@"线程名:%@,售出1,剩余票数是:%ld,",[[NSThread currentThread] name],(long)self.leftTicketCount);
        }else{
            return ;
        }
    });
  }
}

GCD还有一些其他用法,比如创建单例:

单例模式:关于单例,有三件事是你必须要记住的:

  1. 单例必须是唯一的,所以它才被称为单例。在一个应用程序的生命周期里,有且只有一个实例存在。单例的存在给我们提供了一个唯一的全局状态。比如我们熟悉的NSNotification,UIApplication和NSUserDefaults都是单例。

  2. 为了保持一个单例的唯一性,单例的构造器必须是私有的。这防止其他对象也能创建出单例类的实例。

  3. 为了确保单例在应用程序的整个生命周期是唯一的,它就必须是线程安全的。简单来说,如果你写单例的方式是错误的,就有可能会有两个线程尝试在同一时间初始化同一个单例,这样你就有潜在的风险得到两个不同的单例。这就意味着我们需要用GCD的dispatch_once来确保初始化单例的代码在运行时只执行一次。

     @interface Login : NSObject 
    
     +(instancetype)sharedLogin;
    
     @end
    

    @implementation Login

    static id _instance;
    

    +(instancetype)sharedLogin {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    _instance = [[Login alloc] init];
    });

        return _instance;
     }
    

    @end
    关于GCD目前就想到这些,写到这。

NSOperation 和 NSOperationQueue#

NSOperation##

iOS并发编程中,把每个并发任务定义为一个Operation,对应的类名是NSOperation。对应GCD中的任务。NSOperation是一个抽象类,无法直接使用,它只定义了Operation的一些基本方法。我们需要创建一个继承于它的子类或者使用系统预定义的子类。目前系统预定义了两个子类:NSInvocationOperationNSBlockOperation。创建一个Operation后需要调用start来启动任务,它会默认在当前队列中同步执行

  • NSInvocationOperation
    NSInvocationOperation 是一个基于对象和selector的operation,使用这个只需要指定对象以及任务的selector,还可以设定传递的参数对象。

       // 创建操作
      NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
      // 开始执行,operation直接调用start,是同步执行(在当前线程执行操作)
      [operation start];
    

同时当这个operation执行完成后,还可以获取operation中Invocation执行后返回的结果对象:

    id result = [operation result];
  • NSBlockOperation

运行一个Operation
在一个Block中执行一个任务,这时我们就需要用到NSBlockOperation。可以通过blockOperationWithBlock:方法来方便地创建一个NSBlockOperation:

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

start方法用来启动一个Operation任务,这个operation默认会在当前线程中执行。

NSBlockOperation还有一个方法:addExecutionBlock,通过这个方法可以给operation添加多个执行block。这样 Operation 中的任务会 并发执行,它会在主线程和其它的多个线程执行这些任务:

    //创建NSBlockOperation对象
  NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"operation---%@", [NSThread currentThread]);
  }];

  //添加多个Block
  for (NSInteger i = 0; i < 6; i++) {
      [operation addExecutionBlock:^{
          NSLog(@"---download%ld:%@", i, [NSThread currentThread]);
      }];
  }

  //开始执行任务
  [operation start];

注意这打印结果:

2016-03-10 12:13:17.269 TestOperation[9900:3589128] ---download1:{number = 5, name = (null)}
2016-03-10 12:13:17.269 TestOperation[9900:3589136] ---download4:{number = 7, name = (null)}
2016-03-10 12:13:17.269 TestOperation[9900:3589091] operation---{number = 1, name = main}
2016-03-10 12:13:17.269 TestOperation[9900:3589129] ---download0:{number = 2, name = (null)}
2016-03-10 12:13:17.269 TestOperation[9900:3589137] ---download3:{number = 4, name = (null)}
2016-03-10 12:13:17.269 TestOperation[9900:3589135] ---download5:{number = 6, name = (null)}
2016-03-10 12:13:17.269 TestOperation[9900:3589132] ---download2:{number = 3, name = (null)}

取消Operation
要取消一个Operation,要向Operation对象发送cancel消息:
[operation cancel];
当向一个Operation对象发送cancel消息后,并不保证这个Operation对象一定能立刻取消,这取决于你的main中对cancel的处理。如果你在main方法中没有对cancel进行任何处理的话,发送cancel消息是没有任何效果的。为了让Operation响应cancel消息,那么你就要在main方法中一些适当的地方手动的判断isCancelled属性,如果返回YES的话,应释放相关资源并立刻停止继续执行。

  • 自定义Operation
    自定义Operation 需要继承 NSOperation,并实现Operation提供的main方法,你的所有任务都应该在main中进行处理。默认的start方法中会先做出一些异常判断然后直接调用main方法。如果需要自定义一个NSOperation必须重载main方法来执行你所想要执行的任务。
 @implementation CustomOperation 

  -(void)main { 
     @try { 
        // Do some work. 
     } 
     @catch(...) { 
        // Exception handle. 
     } 
  } 
  @end 

NSOperationQueue##

NSOperationQueue是一个Operation执行队列,你可以将任何你想要执行的Operation添加到Operation Queue中,以在队列中执行。Operation Queue一共有两种类型:主队列其他队列,只要添加到队列中,会自动调用start()方法执行任务,无需再手动添加。

  • 主队列
    创建主队列:
    // 主队列
    NSOperationQueue *queue = [NSOperationQueue mainQueue];

  • 其他队列
    其他队列的任务会在其他线程中并行执行。

    //创建NSBlockOperation对象
    NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"operation---%@", [NSThread currentThread]);
    }];
    
    //添加多个Block
    for (NSInteger i = 0; i < 6; i++) {
      [operation addExecutionBlock:^{
          NSLog(@"---download%ld:%@", i, [NSThread currentThread]);
      }];
    }
    
    //开始任务,用NSOperationQueue无需手动start方法
    // [operation start];
    
     // 创建队列
     NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    // 添加任务到队列中(自动异步执行)
    [queue addOperation:operation];
    

打印结果:

2016-03-10 14:06:03.208 TestOperation[10144:3718109] ---download1:{number = 8, name = (null)}
2016-03-10 14:06:03.208 TestOperation[10144:3718203] ---download0:{number = 7, name = (null)}
2016-03-10 14:06:03.208 TestOperation[10144:3718107] operation---{number = 2, name = (null)}
2016-03-10 14:06:03.208 TestOperation[10144:3718240] ---download4:{number = 5, name = (null)}
2016-03-10 14:06:03.208 TestOperation[10144:3718239] ---download3:{number = 4, name = (null)}
2016-03-10 14:06:03.208 TestOperation[10144:3718241] ---download5:{number = 6, name = (null)}
2016-03-10 14:06:03.208 TestOperation[10144:3718204] ---download2:{number = 3, name = (null)}

NSOperationQueue还有一个添加任务的方法addOperationWithBlock

 NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    // 异步下载图片
    NSURL *url = [NSURL URLWithString:@"http://d.hiphotos.baidu.com/image/pic/item/37d3d539b6003af3290eaf5d362ac65c1038b652.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];
    // 回到主线程,显示图片
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        self.imageView.image = image;
    }];
}];

最大并发Operation数目
在一个Operation Queue中是可以同时执行多个Operation的,Operation Queue会动态的创建多个线程来完成相应Operation。具体的线程数是由Operation Queue来优化配置的,这一般取决与系统CPU的性能,比如CPU的核心数,和CPU的负载。但我们还是可以设置一个最大并发数的,那么Operation Queue就不会创建超过最大并发数量的线程。当设置最大并发数位1时,队列中每次只能执行一个任务,这就是一个串行执行队列了。

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1; 

Operation的依赖关系
有时候我们对任务的执行顺序有要求,一个任务必须在另一个任务执行之前完成,这就需要用到Operation的依赖(Dependency)属性。我们可以为每个Operation设定一些依赖的另外一些Operation,那么如果依赖的Operation没有全部执行完毕,这个Operation就不会被执行。比如有个操作C依赖操作B,操作b又依赖操作A:

 // 1.创建一个队列(非主队列)
NSOperationQueue *queue = [[NSOperationQueue alloc] init];

// 2.创建3个操作
NSBlockOperation *operationC = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"---操作C---%@", [NSThread currentThread]);
}];

NSBlockOperation *operationA = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"---操作A---%@", [NSThread currentThread]);
}];
NSBlockOperation *operationB = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"---操作B---%@", [NSThread currentThread]);
}];

// 设置依赖
[operationB addDependency:operationA];
[operationC addDependency:operationB];

// 3.添加操作到队列中(自动异步执行任务)
[queue addOperation:operationA];
[queue addOperation:operationB];
[queue addOperation:operationC];

打印输出:

2016-03-10 14:52:01.518 TestOperation[10812:3782938] ---操作A---{number = 2, name = (null)}
2016-03-10 14:52:01.519 TestOperation[10812:3782945] ---操作B---{number = 3, name = (null)}
2016-03-10 14:52:01.519 TestOperation[10812:3782945] ---操作C---{number = 3, name = (null)}

如果将这些operation和它所依赖的operation加入某个队列中,那么这些operation只有在它所依赖的operation都执行完毕后才可以被执行。注意:

  • 不能相互添加依赖,比如operationA依赖operationB,operationB 又依赖operationA,这会造成死锁。
  • 可以在不同队列之间添加依赖。
  • 可以使用removeDependency解除依赖。

Operation在队列中执行的优先级
Operation在队列中默认是按FIFO(First In First Out)顺序执行的。同时我们可以为单个的Operation设置一个执行的优先级queuePriority,打乱这个顺序。当Queue有空闲资源执行新的Operation时,会优先执行当前队列中优先级最高的待执行Operation。
我从网上copy了一小段代码来展示一下队列优先级效果:

NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"------operation1 begin------");
    sleep(5);
    NSLog(@"------operation1 end------");
}];
operation1.queuePriority = NSOperationQueuePriorityHigh;

NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"------operation2 begin------");
    sleep(1);
    NSLog(@"------operation2 end------");
}];

NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"------operation3 begin------");
    sleep(2);
    NSLog(@"------operation3 end------");
}];

operation2.completionBlock = ^{
    NSLog(@"------operation2 finished in completionBlock------");
};

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 1;
[queue addOperation:operation2];
[queue addOperation:operation3];
[queue addOperation:operation1];
[queue waitUntilAllOperationsAreFinished];

设置最大并发数为1,以便任务能一个一个的执行。打印输出:

2016-03-10 15:26:11.790 TestOperation[11068:3829921] -----test-----{number = 1, name = main}
2016-03-10 15:26:11.791 TestOperation[11068:3829995] ------operation2 begin------
2016-03-10 15:26:12.864 TestOperation[11068:3829995] ------operation2 end------
2016-03-10 15:26:12.865 TestOperation[11068:3830132] ------operation2 finished in completionBlock------
2016-03-10 15:26:12.865 TestOperation[11068:3829996] ------operation1 begin------
2016-03-10 15:26:17.934 TestOperation[11068:3829996] ------operation1 end------
2016-03-10 15:26:17.934 TestOperation[11068:3830132] ------operation3 begin------
2016-03-10 15:26:20.006 TestOperation[11068:3830132] ------operation3 end------

从这个输出结果可以看出,operation2执行完毕后会执行operation1,而不是operation3,这是因为operation1的优先级是NSOperationQueuePriorityHigh。这里还要注意点一就是,第一个加入到Operation Queue中的Operation,无论它的优先级有多么低,总是会第一个执行

设置Operation的completionBlock
每个Operation都可以设置一个completionBlock,在Operation执行完成时自动执行这个Block。我们可以在此进行一些完成的处理。completionBlock实现原理是对Operation的isFinnshed字段进行KVO(Key-Value Observing),当监听到isFinnished变成YES时,就执行completionBlock。上面的小段代码可以看出operation2的block执行完毕后立刻执行completionBlock

其他方法####

设置Operation的线程优先级:我们可以为Operation设置一个线程优先级,即threadPriority。那么执行main的时候,线程优先级就会调整到所设置的线程优先级。这个默认值是0.5,我们可以在Operation执行前修改它。
operation.threadPriority = 0.1;
注意:如果你重载的start方法,那么你需要自己来配置main执行时的线程优先级和threadPriority字段保持一致。

  • NSOperation

    //判断任务是否正在执行
    BOOL executing;
    
    //判断任务是否完成
    BOOL finished;
    
    //取消任务
    -(void)cancel;
    
    //阻塞当前线程直到此任务执行结束
    -(void)waitUntilFinished
    
    //是否异步执行任务
     BOOL asynchronous 
    
  • NSOperationQueue

    //获取队列任务数
    NSUInteger operationCount
    
    //暂停或是取消任务
    BOOL suspended;
    
    //取消所有任务
    - (void)cancelAllOperations;
    
    //阻塞当前线程直到队列中的所有任务执行结束
    - (void)waitUntilAllOperationsAreFinished;
    

在最后再看看有哪些方式可以从其他线程回到主线程:

NSThead#####
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
GCD#####
dispatch_async(dispatch_get_main_queue(), ^{

});
NSOperationQueue#####
[[NSOperationQueue mainQueue] addOperationWithBlock:^{

}];

关于多线程编程,还有其他很多功能,这里所写的只是自己平时比较常用的。具体可以查看官方文档,或是其他资料。本篇GCD的代码例子可以到github上查看,NSOperation的代码在这里

最后声明:#

本文所记载的东西都是来自网上资料或是官方文档,如有侵权,请及时告诉我,但所有代码我都亲自测试了一遍,知识水平有限,如果有错可随时指证,谢谢!!!希望站在巨人的肩膀上看得更远一些。

你可能感兴趣的:(iOS开发-多线程学习)