多线程

概览

  在开发过程中,为了减少用户等待时间,提高程序效率,可以从两个角度出发:

  对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作;

  对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续执行,用户同样可以继续进行其他UI操作,与此同时前一个操作的后续任务可以分散到多个空闲CPU中继续执行(当然具体调度顺序要根据程序设计而定),及解决了线程阻塞又提高了运行效率。

多线程

  简介

在单线程中一个线程只能做一件事情,一件事情处理不完另一件事就不能开始,这样势必影响用户体验。早在单核处理器时期就有多线程,这个时候多线程更多的用于解决线程阻塞造成的用户等待(通常是操作完UI后用户不再干涉,其他线程在等待队列中,CPU一旦空闲就继续执行,不影响用户其他UI操作),其处理能力并没有明显的变化。如今有了多核处理器,“并行运算”就更多的被提及。一件事情我们可以分成多个步骤,在没有顺序要求的情况下使用多线程既能解决线程阻塞又能充分利用多核处理器运行能力

单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别当然,不管是多核还是单核,开发人员不用担心任务的具体分配(给几个CPU运算是由系统调度的),需要关心的是线程之间的依赖关系,因为有些操作必须在某个操作完成完才能执行,如果不能保证这个顺序势必会造成程序问题。

iOS多线程

在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程。由于在iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面(新版iOS中,使用其他线程更新UI可能也能成功,但是不推荐)。iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。常用的多线程开发有三种方式:

1.NSThread 

2.NSOperation 

3.GCD

三种方式是随着iOS的发展逐渐引入的,所以相比而言后者比前者更加简单易用,并且GCD也是目前苹果官方比较推荐的方式(它充分利用了多核处理器的运算性能)。

 

NSThread

–优点:NSThread是轻量级、使用简单的多线程开发。

–缺点:需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等;不易控制线程执行顺序(可用休眠方法);线程同步对数据的加锁会有一定的系统开销。

 

两种创建方法

自动启动:直接将操作添加到线程中并启动:

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

手动启动:创建一个线程对象,然后调用start方法启动线程:

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument ;

不管是使用什么方法创建新线程,都只能传一个参数,由若需要传递不只一个数据,则需要定义一个类来保存数据模型,再传入。

 

多个线程并发(多个子线程操作之间的关系)

  • 每个线程的实际执行顺序并不一定按顺序执行(虽然是按顺序启动):因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度;每个线程执行时实际网络状况不一致。
  • 可以通过改变线程的优先级来改变线程执行的先后:线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。
  • 提高优先级只是提高被优先执行的几率:线程的启动顺序不受优先级影响;其他因素如网速也会影响实际执行
  • 多个线程同时进行,会涉及到安全问题(如第一个线程未下载完,第二个线程就像读取数据),因此需要使用线程锁

线程锁

  同个全局变量,用于两个线程内时,为了安全问题,需要用到线程锁。

  • setter方法对某个对象加锁:设置atomic属性,一时间内只允许一个线程使用setter方法

  但是这个方法效率低性能差,不能用于UI,因为系统规定UI本身就不能在分线程中使用,根本不需要线程锁;而且设定UI有关的类都是nonatomic的

//setter方法加锁
@property (atomic, copy) NSString *str;

@property (nonatomic, weak) UIImageView *imageView;

- (void)setStr:(NSString *)str;

@end

@implementation ViewController

- (void)setStr:(NSString *)str
{
    //加锁
    
    _str = str;
    
    //解锁
}

 

  • NSLock互斥锁:让线程不同时走

  [_lock lock]; [_lock unlock]; 成对存在,中间是被加锁的代码块。

  • 对象锁 @synchronized(对象);

  括号内是同个对象,则是同个对象锁,程序会按顺序依次执行同个线程锁的行为;对象不同,顺序就未知。

    _obj = [[NSObject alloc] init];
    NSObject *o1 = [[NSObject alloc] init];
#if 0
    //括号里面对象一样,顺序就确定是按顺序执行,因为是同一个锁
    @synchronized(_obj)
    {
        //my code1
    }
    
    @synchronized(_obj)
    {
        //my code2
    }
#endif

 

#if 0
    //括号里面对象不一样,不是同一个对象锁,顺序不确定
    @synchronized(_o1)
    {
        //my code1
    }
    
    @synchronized(_obj)
    {
        //my code2
    }
#endif

 

 

 

 

线程状态

在线程操作过程中可以让某个线程休眠等待,优先执行其他线程操作,而且在这个过程中还可以修改某个线程的状态或者终止某个指定线程。为了解决上面优先加载最后一张图片的问题,不妨让其他线程先休眠一会等待最后一个线程执行。

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

使用NSThread在进行多线程开发过程中操作比较简单,但是要控制线程执行顺序并不容易(前面万不得已采用了休眠的方法),另外在这个过程中如果打印线程会发现循环几次就创建了几个线程,这在实际开发过程中是不得不考虑的问题,因为每个线程的创建也是相当占用系统开销的。

扩展--NSObject分类扩展方法(self来掉用这些方法)

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

后台执行一个操作,本质就是重新创建一个线程执行当前方法:

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

指定的线程上执行一个方法,需要用户创建一个线程对象:

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait

主线程上执行一个方法(多数用于请求完数据后刷新UI界面):

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait

 


NSOperation

优点:

- 不需要关心线程管理,数据同步的事情,可以把精力放在自己需要执行的操作上:只要把操作(NSOperation)放在队列(NSOperationQueue)中,线程就会依次启动

- NSOperation是面向对象的

- 容易的管理线程总数(最大并发线程数)、控制线程之间的依赖关系和执行顺序:有NSOperationQueue负责执行和管理所有的NSOperation

- 方便设置在同一时间任务最大并发个数

- 可以创建操作,支持在第一时间被取消

 

使用过程

  • NSOperationQueue 创建队列 queue

  • NSInvocationOperation / NSBlockOperation / 自定义Operation 创建线程操作 operation

  • [queue addOperation:operation]; 把操作添加到队列中,交由队列管理

  • 设置NSOperation的依赖关系,从而影响线程的执行顺序

 

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

子类一:NSInvocationOperation

  • 创建一个操作,在这个操作中指定调用方法和参数,然后加入到操作队列。
  • [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(方法) object:(nullable id)arg参数];  注意到这个方法每次只能传入一个参数,这使得信息传递不够简便。 

#pragma mark - 1.NSInvocationOperation
//创建队列-创建NSInvocationOperation-添加operation到队列中
    
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    NSInvocationOperation *oper1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(download) object:nil];
    
    //添加到队列里面,队列会管理operation
    [queue addOperation:oper1];
    
    //下面任务数由0 → 1 证明:当任务执行完成后,会从队列里面自动移除
    NSLog(@"111111:%ld",(unsigned long)queue.operations.count);
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"222222:%ld",(unsigned long)queue.operations.count);
    });

 

子类二:NSBlockOperation

  • 基本用法:类似子类一,但不用单独定义方法,同时解决传参难的问题(可以不需要定义一个实体参数进行传参)

 

  • 直接用法:可以直接调用目标队列(自创的NSOperationQueue或者主线程)用addOperationWithBlock:方法来直接添加操作
#pragma mark - 2.NSBlockOperation
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    
    NSBlockOperation *oper2 = [NSBlockOperation blockOperationWithBlock:^{
        //耗时操作
        NSLog(@"NSBlockOperation请求数据--%d",[NSThread isMainThread]);
    }];
    //添加队列
    [queue addOperation:oper2];

 

基本用法,多线程加载图片

 

 

 

- (void)downloadImages
{
    //创建队列
    NSOperationQueue *quque = [[NSOperationQueue alloc] init];
    NSArray *array = @[
    @"...",
    @"...",
    @"..."];
    
    for (int i = 0; i < array.count; i++)
    {
        UIImageView *imageView = [[UIImageView alloc] init];
        //imageView.frame
        [self.view addSubview:imageView];
        
        //创建操作
        NSBlockOperation *oper2 = [NSBlockOperation blockOperationWithBlock:^{
            //耗时操作:下载图片
            NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:array[i]]];
            //主线程刷新:加载数据
            dispatch_async(dispatch_get_main_queue(), ^{
                imageView.image = [UIImage imageWithData:data];
            });
        }];
        
        //添加到队列,任务开始执行
        [quque addOperation:oper2];
    }
}

 

 

 

 

直接用法,创建多个线程加载图片

#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];
    }];
}
#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) {        
        //方法2:直接使用操队列添加操作
        [operationQueue addOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
    }
} 

 

自定义Operation

  •  原理:Operation的生命周期:当任务添加到队列里面,会自动触发main(),当main执行完成就会从队列里面移除。
  •  所以设计自定义Operation的关键在于,保证main方法不在操作完成前走完(RunLoop)。
  •  分线程,异步请求必须在main方法内执行
  • 自定义的operation:

         1.创建一个NSOperation的子类

         2.然后重写main()方法

         3.在main()方法发送异步请求

  "ViewController.h"

#import "ViewController.h"
#import "RequestOperation.h"

#pragma mark - 3.自定的Operation
    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    //自定义的Operation
    RequestOperation *oper3 = [[RequestOperation alloc] initWithUrl:[NSURL URLWithString:@"xxxxx"]];;
    [queue addOperation:oper3];

 

  RequestOperation.h

#import <Foundation/Foundation.h>
@interface RequestOperation : NSOperation

- (instancetype)initWithUrl:(NSURL *)url;
//取消
- (void)cancel;

@end

 

  RequestOperation.m

#import "RequestOperation.h"

@interface RequestOperation ()
{
    //是否停止请求
    BOOL _isEndRequest;
}

@property (nonatomic, strong) NSURL *url;

@end

@implementation RequestOperation

- (instancetype)initWithUrl:(NSURL *)url
{
    if (self = [super init])
    {
        self.url = url;
    }
    
    return self;
}

//取消
- (void)cancel
{
    [super cancel];
    
    _isEndRequest = YES;
}

//分线程,异步请求必须在main方法执行
- (void)main
{
    
    NSLog(@"--%d",[NSThread isMainThread]);
    
    //发送请求
    NSURLSession *session = [NSURLSession sharedSession];
    [session dataTaskWithRequest:[NSURLRequest requestWithURL:_url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        
       //回到主线程
        dispatch_async(dispatch_get_main_queue(), ^{
            if (error)
            {
                //失败回调
            }
            else
            {
                //成功回调
            }
        });
    
        //下载完成
        _isEndRequest = YES;
        
    }];
    
    //如果没有下载完成,会执行while里面的代码
    while (!_isEndRequest)
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode  beforeDate:[NSDate distantFuture]];
    }
}
@end

 

线程执行顺序:NSOperation的依赖关系

  • 添加依赖关系:[o1 addDependency:o2];   //o1依赖o2

  • 1.在调用addOperation:之前必须添加好依赖关系。在addOperation:之后添加的依赖关系都是失效的。(原因:加入队列后指不定几时被执行)
  • 2.不能出现循环依赖:操作依赖关系可以设置多个,例如A依赖于B、B依赖于C…但是千万不要设置为循环依赖关系(例如A依赖于B,B依赖于C,C又依赖于A),否则是不会被执行的。

 

NSOperationQueue队列管理

 

GCD

优点

- GCD(Grand Central Dispatch)是由苹果开发的一个多核编程的解决方案。iOS4.0+才能使用,是替代NSThread, NSOperation的高效和强大的技术。

- 前面也说过三种开发中GCD抽象层次最高,当然是用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而是完全面向过程的。

- 使用Block后代码比使用NSOperation,NSThread更简洁。

 

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

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

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

 

串行队列

使用串行队列时首先要创建一个串行队列,然后调用异步调用方法,在此方法中传入串行队列和线程操作即可自动执行。

更新UI必须使用GCD方法的主线程队列dispatch_get_main_queue(),其实这与前面两种主线程更新UI没有本质的区别。

 

并发队列

并发队列同样是使用dispatch_queue_create()方法创建,只是最后一个参数指定为DISPATCH_QUEUE_CONCURRENT进行创建,但是在实际开发中我们通常不会重新创建一个并发队列而是使用dispatch_get_global_queue()方法取得一个全局的并发队列(当然如果有多个并发队列可以使用前者创建)

细心的朋友肯定会思考,既然可以使用dispatch_async()异步调用方法,是不是还有同步方法,确实如此,在GCD中还有一个dispatch_sync()方法。

  • 在GDC中一个操作是多线程执行还是单线程执行取决于当前队列类型和执行方法,只有队列类型为并行队列并且使用异步方法执行时才能在多个线程中执行。 
  • 串行队列可以按顺序执行,并行队列的异步方法无法确定执行顺序。 
  • UI界面的更新最好采用同步方法,其他操作采用异步方法。 

 

其他任务执行方法

GCD执行任务的方法并非只有简单的同步调用方法和异步调用方法,还有其他一些常用方法:

  1. dispatch_apply():重复执行某个任务,但是注意这个方法没有办法异步执行(为了不阻塞线程可以使用dispatch_async()包装一下再执行)。 
  2. dispatch_once():单次执行一个任务,此方法中的任务只会执行一次,重复调用也没办法重复执行(单例模式中常用此方法)。 
  3. dispatch_time():延迟一定的时间后执行。 
  4. dispatch_barrier_async():使用此方法创建的任务首先会查看队列中有没有别的任务要执行,如果有,则会等待已有任务执行完毕再执行;同时在此方法后添加的任务必须等待此方法中任务执行后才能执行。(利用这个方法可以控制执行顺序,例如前面先加载最后一张图片的需求就可以先使用这个方法将最后一张图片加载的操作添加到队列,然后调用dispatch_async()添加其他图片加载任务) 
  5. dispatch_group_async():实现对任务分组管理,如果一组任务全部完成可以通过dispatch_group_notify()方法获得完成通知(需要定义dispatch_group_t作为分组标识)。

 

线程同步

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

举例来说,每年春节都是一票难求,在12306买票的过程中,成百上千的票瞬间就消失了。不妨假设某辆车有1千张票,同时有几万人在抢这列车的车票,顺利的话前面的人都能买到票。但是如果现在只剩下一张票了,而同时还有几千人在购买这张票,虽然在进入购票环节的时候会判断当前票数,但是当前已经有100个线程进入购票的环节,每个线程处理完票数都会减1,100个线程执行完当前票数为-99,遇到这种情况很明显是不允许的。

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

这里不妨还拿图片加载来举例,假设现在有9张图片,但是有15个线程都准备加载这9张图片,约定不能重复加载同一张图片,这样就形成了一个资源抢夺的情况。在下面的程序中将创建9张图片,每次读取照片链接时首先判断当前链接数是否大于1,用完一个则立即移除,最多只有9个。在使用同步方法之前先来看一下错误的写法:


首先在_imageNames中存储了9个链接用于下载图片,然后在requestData:方法中每次只需先判断_imageNames的个数,如果大于一就读取一个链接加载图片,随即把用过的链接删除,一切貌似都没有问题。此时运行程序:

LockEffect1

上面这个结果不一定每次都出现,关键要看从_imageNames读取链接、删除链接的速度,如果足够快可能不会有任何问题,但是如果速度稍慢就会出现上面的情况,很明显上面情况并不满足前面的需求。

分析这个问题造成的原因主:当一个线程A已经开始获取图片链接,获取完之后还没有来得及从_imageNames中删除,另一个线程B已经进入相应代码中,由于每次读取的都是_imageNames的最后一个元素,因此后面的线程其实和前面线程取得的是同一个图片链接这样就造成图中看到的情况。要解决这个问题,只要保证线程A进入相应代码之后B无法进入,只有等待A完成相关操作之后B才能进入即可。下面分别使用NSLock和@synchronized对代码进行修改。

NSLock

iOS中对于资源抢占的问题可以使用同步锁NSLock来解决,使用时把需要加锁的代码(以后暂时称这段代码为”加锁代码“)放到NSLock的lock和unlock之间,一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码需要注意的是lock和unlock之间的”加锁代码“应该是抢占资源的读取和修改代码,不要将过多的其他操作代码放到里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。

另外,在上面的代码中”抢占资源“_imageNames定义成了成员变量,这么做是不明智的,应该定义为“原子属性”。对于被抢占资源来说将其定义为原子属性是一个很好的习惯,因为有时候很难保证同一个资源不在别处读取和修改。nonatomic属性读取的是内存数据(寄存器计算好的结果),而atomic就保证直接读取寄存器的数据,这样一来就不会出现一个线程正在修改数据,而另一个线程读取了修改之前(存储在内存中)的数据,永远保证同时只有一个线程在访问一个属性。

下面的代码演示了如何使用NSLock进行线程同步:

KCMainViewController.h

//
//  KCMainViewController.h
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

#import <UIKit/UIKit.h>

@interface KCMainViewController : UIViewController

@property (atomic,strong) NSMutableArray *imageNames;
@end

KCMainViewController.m


 

前面也说过使用同步锁时如果一个线程A已经加锁,线程B就无法进入。那么B怎么知道是否资源已经被其他线程锁住呢?可以通过tryLock方法,此方法会返回一个BOOL型的值,如果为YES说明获取锁成功,否则失败。另外还有一个lockBeforeData:方法指定在某个时间内获取锁,同样返回一个BOOL值,如果在这个时间内加锁成功则返回YES,失败则返回NO。

@synchronized代码块

使用@synchronized解决线程同步问题相比较NSLock要简单一些,日常开发中也更推荐使用此方法。首先选择一个对象作为同步对象(一般使用self),然后将”加锁代码”(争夺资源的读取、修改代码)放到代码块中。@synchronized中的代码执行时先检查同步对象是否被另一个线程占用,如果占用该线程就会处于等待状态,直到同步对象被释放。下面的代码演示了如何使用@synchronized进行线程同步:


扩展--使用GCD解决资源抢占问题

在GCD中提供了一种信号机制,也可以解决资源抢占问题(和同步锁的机制并不一样)。GCD中信号量是dispatch_semaphore_t类型,支持信号通知和信号等待。每当发送一个信号通知,则信号量+1;每当发送一个等待信号时信号量-1,;如果信号量为0则信号会处于等待状态,直到信号量大于0开始执行。根据这个原理我们可以初始化一个信号量变量,默认信号量设置为1,每当有线程进入“加锁代码”之后就调用信号等待命令(此时信号量为0)开始等待,此时其他线程无法进入,执行完后发送信号通知(此时信号量为1),其他线程开始进入执行,如此一来就达到了线程同步目的。


运行效果与前面使用同步锁是一样的。

扩展--控制线程通信

由于线程的调度是透明的,程序有时候很难对它进行有效的控制,为了解决这个问题iOS提供了NSCondition来控制线程通信(同前面GCD的信号机制类似)。NSCondition实现了NSLocking协议,所以它本身也有lock和unlock方法,因此也可以将它作为NSLock解决线程同步问题,此时使用方法跟NSLock没有区别,只要在线程开始时加锁,取得资源后释放锁即可,这部分内容比较简单在此不再演示。当然,单纯解决线程同步问题不是NSCondition设计的主要目的,NSCondition更重要的是解决线程之间的调度关系(当然,这个过程中也必须先加锁、解锁)。NSCondition可以调用wati方法控制某个线程处于等待状态,直到其他线程调用signal(此方法唤醒一个线程,如果有多个线程在等待则任意唤醒一个)或者broadcast(此方法会唤醒所有等待线程)方法唤醒该线程才能继续。

假设当前imageNames没有任何图片,而整个界面能够加载15张图片(每张都不能重复),现在创建15个线程分别从imageNames中取图片加载到界面中。由于imageNames中没有任何图片,那么15个线程都处于等待状态,只有当调用图片创建方法往imageNames中添加图片后(每次创建一个)并且唤醒其他线程(这里只唤醒一个线程)才能继续执行加载图片。如此,每次创建一个图片就会唤醒一个线程去加载,这个过程其实就是一个典型的生产者-消费者模式。下面通过NSCondition实现这个流程的控制:

 


 

在上面的代码中loadImage:方法是消费者,当在界面中点击“加载图片”后就创建了15个消费者线程。在这个过程中每个线程进入图片加载方法之后都会先加锁,加锁之后其他进程是无法进入“加锁代码”的。但是第一个线程进入“加锁代码”后去加载图片却发现当前并没有任何图片,因此它只能等待。一旦调用了NSCondition的wait方法后其他线程就可以继续进入“加锁代码”(注意,这一点和前面说的NSLock、@synchronized等是不同的,使用NSLock、@synchronized等进行加锁后无论什么情况下,只要没有解锁其他线程就无法进入“加锁代码”),同时第一个线程处于等待队列中(此时并未解锁)。第二个线程进来之后同第一线程一样,发现没有图片就进入等待状态,然后第三个线程进入。。。如此反复,直到第十五个线程也处于等待。此时点击“创建图片”后会执行createImageName方法,这是一个生产者,它会创建一个图片链接放到imageNames中,然后通过调用NSCondition的signal方法就会在条件等待队列中选择一个线程(该线程会任意选取,假设为线程A)开启,那么此时这个线程就会继续执行。在上面代码中,wati方法之后会继续执行图片加载方法,那么此时线程A启动之后继续执行图片加载方法,当然此时可以成功加载图片。加载完图片之后线程A就会释放锁,整个线程任务完成。此时再次点击”创建图片“按钮重复前面的步骤加载其他图片。

为了说明上面的过程,这里以一个流程图的进行说明,流程图蓝色部分代表15个加载图片的线程,绿色部分表示创建图片资源线程。

 

iOS中的其他锁

在iOS开发中,除了同步锁有时候还会用到一些其他锁类型,在此简单介绍一下:

NSRecursiveLock :递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。

NSDistributedLock:分布锁,它本身是一个互斥锁,基于文件方式实现锁机制,可以跨进程访问。

pthread_mutex_t:同步锁,基于C语言的同步锁机制,使用方法与其他同步锁机制类似。

提示:在开发过程中除非必须用锁,否则应该尽可能不使用锁,因为多线程开发本身就是为了提高程序执行顺序,而同步锁本身就只能一个进程执行,这样不免降低执行效率。

总结

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

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

 

参考:iOS开发系列--并行开发其实很容易

posted on 2016-03-29 13:45  Wilson_CYS  阅读(293)  评论(0编辑  收藏  举报

导航