iOS 多线程详解

阅读 2017
收藏 59
2017-02-20
原文链接:www.imlifengfeng.com

一、概述

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

多线程的实现有以下几种方式:

 

1593252-1ae56de0c12d5936

NSThread:

(1)使用NSThread对象建立一个线程非常方便

(2)但是!要使用NSThread管理多个线程非常困难,不推荐使用

(3)技巧!使用[NSThread currentThread]获得任务所在线程,适用于这三种技术

(4)使线程休眠3秒:[NSThread sleepForTimeInterval:0.3f];

 

GCD —— Grand Central Dispatch:

(1)是基于C语言的底层API

(2)用Block定义任务,使用起来非常灵活便捷

(3)提供了更多的控制能力以及操作队列中所不能使用的底层函数

 

NSOperation/NSOperationQueue:

(1)是使用GCD实现的一套Objective-C的API

(2)是面向对象的线程技术

(3)提供了一些在GCD中不容易实现的特性,如:限制最大并发数量、操作之间的依赖关系

 

 

 

二、线程与进程

1、进程

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,每一个进程都有自己独立的虚拟内存空间。

简单来说,进程是指在系统中正在运行的一个应用程序,每一个程序都是一个进程,并且进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内。

 

2、线程

线程,是程序执行流的最小单元线程是程序中一个单一的顺序控制流程。是进程内一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指运行中的程序的调度单位。

简单来说,1个进程要想执行任务,必须得有线程。

线程中任务的执行是串行的,要在1个线程中执行多个任务,那么只能一个一个地按顺序执行这些任务,也就是说,在同一时间内,1个线程只能执行1个任务,由此可以理解线程是进程中的1条执行路径。

一个进程中至少包含一条线程,即主线程,创建线程的目的就是为了开启一条新的执行路径,运行指定的代码,与主线程中的代码实现同时运行。

 

3、多线程

多线程(multithreading),是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

原理:

  • 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)
  • 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
  • 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
    注意:多线程并发,并不是cpu在同一时刻同时执行多个任务,只是CPU调度足够快,造成的假象。

优点:

  • 能适当提高程序的执行效率
  • 能适当提高资源利用率(CPU、内存利用率)

缺点:

  • 开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
  • 线程越多,CPU在调度线程上的开销就越大

图片 1

主线程栈区的1M,非常非常宝贵。一个进程,至少有一个线程(主线程),不能杀掉一个线程!但是可以暂停、休眠。

 

三、NSThread的使用

1、线程的创建

NSThread创建线程有如下三种方法:

//参数1:要执行的方法,参数2:提供selector的对象,通常是self,参数3传递给selector的参数
[NSThread detachNewThreadSelector:(nonnull SEL)> toTarget:(nonnull id) withObject:(nullable id)]
//参数一:提供selector的对象,通常是self,参数2:要执行的方法,参数3:传递给selector的参数(如果selector方法不带参数,就使用nil)
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(doSomething) object:nil];

 

//隐式创建并启动线程,第一个参数为调用的方法,第二个参数为传给selector方法的参数
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

NSThread对象的常见属性:

    //只读属性,线程是否在执行
    thread.isExecuting;
    //只读属性,线程是否被取消
    thread.isCancelled;
    //只读属性,线程是否完成
    thread.isFinished;
    //是否是主线程
    thread.isMainThread;

    //线程的优先级,取值范围0.0到1.0,默认优先级0.5,1.0表示最高优     //先级,优先级高,CPU调度的频率高
    thread.threadPriority;

    //线程的堆栈大小,线程执行前堆栈大小为512K,线程完成后堆栈大小       为0K
    //注意:线程执行完毕后,由于内存空间被释放,不能再次启动
    thread.stackSize;

NSThread对象的方法:

    //线程开始,线程加入线程池等待CPU调度(并非真正开始执行,只是通常等待时间都非常短,看不出效果)
    [thread start];
    if(!thread.isCancelled){//在执行之前需要先确认线程状态,如果已经取消就直接返回
        [thread cancel]; //通知线程取消,可以在外不终止线程执行
    }else{
        return;
    }

NSThread的类方法:

类方法都用在线程内部,也就是说类方法作用于相关类方法的线程。

(1)当前线程,在开发中常用于调试,适用于所有多线程计数,返回一个线程号码

//number == 1 表示主线程,number != 1表示后台线程
int number = [NSThread currentThread];

(2)阻塞方法

//休眠到指定时间
[NSThread sleepUntilDate:[NSDate date]];
//休眠指定时长
[NSThread sleepForTimeInterval:4.5];

(3)其他类方法

//退出线程
[NSThread exit];
//当前线程是否为主线程
[NSThread isMainThread];
//是否多线程
[NSThread isMultiThreaded];
//返回主线程的对象
NSThread *mainThread = [NSThread mainThread];

2、线程的状态

线程的状态如下图:

Snip20170209_1

(1)新建:实例化对象

(2)就绪:向线程对象发送 start 消息,线程对象被加入“可调度线程池”等待 CPU 调度;detach 方法和 performSelectorInBackground 方法会直接实例化一个线程对象并加入“可调度线程池”

(3)运行:CPU 负责调度“可调度线程池”中线程的执行,线程执行完成之前,状态可能会在“就绪”和“运行”之间来回切换,“就绪”和“运行”之间的状态变化由 CPU 负责,程序员不能干预

(4)阻塞:当满足某个预定条件时,可以使用休眠或锁阻塞线程执行,影响的方法有:sleepForTimeInterval,sleepUntilDate,@synchronized(self)x线程锁;
线程对象进入阻塞状态后,会被从“可调度线程池”中移出,CPU 不再调度

(5)死亡
死亡方式:

正常死亡:线程执行完毕
非正常死亡:线程内死亡--->[NSThread exit]:强行中止后,后续代码都不会在执行
线程外死亡:[threadObj cancel]--->通知线程对象取消,在线程执行方法中需要增加 isCancelled 判断,如果 isCancelled == YES,直接返回

死亡后线程对象的 isFinished 属性为 YES;如果是发送 calcel 消息,线程对象的 isCancelled 属性为YES;死亡后 stackSize == 0,内存空间被释放。

 

3、多线程的安全问题

多个线程访问同一块资源进行读写,如果不加控制随意访问容易产生数据错乱从而引发数据安全问题。为了解决这一问题,就有了加锁的概念。加锁的原理就是当有一个线程正在访问资源进行写的时候,不允许其他线程再访问该资源,只有当该线程访问结束后,其他线程才能按顺序进行访问。对于读取数据,有些程序设计是允许多线程同时读的,有些不允许。UIKit中几乎所有控件都不是线程安全的,因此需要在主线程上更新UI。

解决多线程安全问题:

(1)互斥锁

// 注意:锁定1份代码只用1把锁,用多把锁是无效的
@synchronized(锁对象) { // 需要锁定的代码  }

使用互斥锁,在同一个时间,只允许一条线程执行锁中的代码。因为互斥锁的代价非常昂贵,所以锁定的代码范围应该尽可能小,只要锁住资源读写部分的代码即可。使用互斥锁也会影响并发的目的。

 

(2)使用NSLock对象

_lock = [[NSLock alloc] init];
 - (void)synchronizedMethod {
    [_lock lock];
    //safe
    [_lock unlock];

 }

(3)atomic加锁

OC在定义属性时有nonatomic和atomic两种选择。

atomic:原子属性,为setter方法加锁(默认就是atomic)。

nonatomic:非原子属性,不会为setter方法加锁。

atomic加锁原理:

 @property (assign, atomic) int age;

 - (void)setAge:(int)age
 { 

     @synchronized(self) { 
        _age = age;
     }
 }

atomic:线程安全,需要消耗大量的资源

nonatomic:非线程安全,适合内存小的移动设备

iOS开发的建议:

(1)所有属性都声明为nonatomic

(2)尽量避免多线程抢夺同一块资源

(3)尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力

 

4、线程间通信

//在主线程上执行操作,例如给UIImageVIew设置图片
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait
//在指定线程上执行操作
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thread withObject:(id)arg waitUntilDone:(BOOL)wai

四、GCD的使用

GCD(Grand Central Dispatch) 伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。

GCD会自动利用更多的CPU内核;
会自动管理线程的生命周期(创建线程,调度任务,销毁线程等);
程序员只需要告诉 GCD 想要如何执行什么任务,不需要编写任何线程管理代码。

一些专业术语:

dispatch :派遣/调度

queue:队列
    用来存放任务的先进先出(FIFO)的容器
sync:同步
    只是在当前线程中执行任务,不具备开启新线程的能力
async:异步
    可以在新的线程中执行任务,具备开启新线程的能力
concurrent:并发
    多个任务并发(同时)执行
串行:
    一个任务执行完毕后,再执行下一个任务

 

1、GCD中的核心概念

(1)任务

任务就是要在线程中执行的操作。我们需要将要执行的代码用block封装好,然后将任务添加到队列并指定任务的执行方式,等待CPU从队列中取出任务放到对应的线程中执行。

 - queue:队列
 - block:任务
// 1.用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);

// 2.用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

// 3.GCD中还有个用来执行任务的函数
// 在前面的任务执行结束后它才执行,而且它后面的任务等它执行完成之后才会执行
dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

(2)队列

串行队列:串行队列一次只调度一个任务,一个任务完成后再调度下一个任务。

// 1.使用dispatch_queue_create函数创建串行队列
// 创建串行队列(队列类型传递NULL或者DISPATCH_QUEUE_SERIAL)
dispatch_queue_t queue = dispatch_queue_create("queue", NULL);

// 2.使用dispatch_get_main_queue()获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
注意:主队列是GCD自带的一种特殊的串行队列,放在主队列中的任务,都会放到主线程中执行。

并发队列:并发队列可以同时调度多个任务,调度任务的方式,取决于执行任务的函数;并发功能只有在异步的(dispatch_async)函数下才有效;异步状态下,开启的线程上线由GCD底层决定。

/ 1.使用dispatch_queue_create函数创建队列
dispatch_queue_t
dispatch_queue_create(const char *label, // 队列名称,该名称可以协助开发调试以及崩溃分析报告 
dispatch_queue_attr_t attr); // 队列的类型

// 2.创建并发队列
dispatch_queue_t queue = dispatch_queue_create("queue", DISPATCH_QUEUE_CONCURRENT);

以上自定义队列在MRC开发时需要使用dispatch_release释放队列:

#if !__has_feature(objc_arc)
    dispatch_release(queue);
#endif

主队列:主队列负责在主线程上调度任务,如果在主线程上有任务执行,会等待主线程空闲后再调度任务执行。主队列用于UI以及触摸事件等的操作,我们在进行线程间通信,通常是返回主线程更新UI的时候使用到。

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // 耗时操作
    // ...
    //放回主线程的函数
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主线程更新 UI
    });
});

全局并发队列:全局并发队列是由苹果API提供的,方便程序员使用多线程。

//使用dispatch_get_global_queue函数获得全局的并发队列
dispatch_queue_t dispatch_get_global_queue(dispatch_queue_priority_t priority, unsigned long flags);
// dispatch_queue_priority_t priority(队列的优先级 )
// unsigned long flags( 此参数暂时无用,用0即可 )

//获得全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

全局并发队列有优先级:

//全局并发队列的优先级
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高优先级
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默认(中)优先级
//注意,自定义队列的优先级都是默认优先级
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低优先级
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后台优先级

然而,iOS8 开始使用 QOS(服务质量) 替代了原有的优先级。获取全局并发队列时,直接传递 0,可以实现 iOS 7 & iOS 8 later 的适配。

//像这样
dispatch_get_global_queue(0, 0);

 

全局并发队列与并发队列的区别:

(1)全局并发队列与并发队列的调度方法相同
(2)全局并发队列没有队列名称
(3)在MRC开发中,全局并发队列不需要手动释放

 

2、执行任务的函数

(1)同步(dispatch_sync)

任务被添加到队列后,会当前线程被调度;队列中的任务同步执行完成后,才会调度后续任务。

在主线程中,向主队列添加同步任务,会造成死锁。
在其他线程中,向主队列向主队列添加同步任务,则会在主线程中同步执行。

我们可以利用同步的机制,建立任务之间的依赖关系。如用户登录后,才能够并发下载多部小说等情况。
例如:

dispatch_queue_t q = dispatch_queue_create("com.imlifengfeng.ios", DISPATCH_QUEUE_SERIAL);
// [NSThread currentThread]获得当前线程
    for (int i = 0; i < 10; ++i) {
        // 异步任务顺序执行,但是如果用在串行队列中,仍然会依次顺序执行
        dispatch_async(q, ^{
            NSLog(@"%@%d ", [NSThread currentThread],i);
     });
  }

(2)异步(dispatch_async)

异步是多线程的代名词,当任务被添加到主队列后,会等待主线程空闲时才会调度该任务;添加到其他线程时,会开启新的线程调度任务。

例如:

dispatch_queue_t q = dispatch_queue_create("com.imlifengfeng.ios", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i < 10; ++i) {
        // 异步任务顺序执行
        dispatch_async(q, ^{
            NSLog(@"%@ %d", [NSThread currentThread], i);
        });
    }

(3)以函数指针的方式调度任务

函数指针的调用方式有两种,同样是同步和异步;函数指针的传递类似于 pthread。

dispatch_sync_f
dispatch_async_f

函数指针调用在实际开发中几乎不用,只是有些面试中会问到,dispatch + block 才是 gcd 的主流!

 

3、开发中如何选择队列

选择队列当然是要先了解队列的特点:
串行队列:对执行效率要求不高,对执行顺序要求高,性能消耗小
并发队列:对执行效率要求高,对执行顺序要求不高,性能消耗大
如果不想兼顾 MRC 中队列的释放,建议选择使用全局队列 + 异步任务。

 

4、GCD的其他用法

(1)延时执行

//参数1:从现在开始经过多少纳秒,参数2:调度任务的队列,参数3:异步执行的任务
dispatch_after(when, queue, block)

例如:

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒后异步执行这里的代码...
});

(2)一次性执行

应用场景:保证某段代码在程序运行过程中只被执行一次,在单例设计模式中被广泛使用。

// 使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});

(3)调度组(队列组)

应用场景:需要在多个耗时操作执行完毕之后,再统一做后续处理。

//创建调度组
dispatch_group_t group = dispatch_group_create();
//将调度组添加到队列,执行 block 任务
dispatch_group_async(group, queue, block);
//当调度组中的所有任务执行结束后,获得通知,统一做后续操作
dispatch_group_notify(group, dispatch_get_main_queue(), block);

例如:

// 分别异步执行2个耗时的操作、2个异步操作都执行完毕后,再回到主线程执行操作
dispatch_group_t group =  dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行1个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    // 执行1个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 等前面的异步操作都执行完毕后,回到主线程...
});

5、应用案例:单例模式

// 1.在.m中保留一个全局的static的实例
static id _instance;

// 2.重写allocWithZone:方法,在这里创建唯一的实例(注意线程安全)
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [super allocWithZone:zone];
    });
    return _instance;
}

// 3.提供1个类方法让外界访问唯一的实例
+ (instancetype)sharedInstance
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _instance = [[self alloc] init];
    });
    return _instance;
}

// 4.实现copyWithZone:方法
- (id)copyWithZone:(struct _NSZone *)zone
{
    return _instance;
}

五、NSOperation

NSOperation是苹果推荐使用的并发技术,它提供了一些用GCD不是很好实现的功能。NSOperation是基于GCD的面向对象的使用OC语言的封装。相比GCD,NSOperation的使用更加简单。NSOperation是一个抽象类,也就是说它并不能直接使用,而是应该使用它的子类。使用它的子类的方法有三种,使用苹果为我们提供的两个子类 NSInvocationOperationNSBlockOperation和自定义继承自NSOperation的子类。

NSOperation的使用常常是配合NSOperationQueue来进行的。只要是使用NSOperation的子类创建的实例就能添加到NSOperationQueue操作队列之中,一旦添加到队列,操作就会自动异步执行(注意是异步)。如果没有添加到队列,而是使用start方法,则会在当前线程执行

我们知道,线程间的通信主要是主线程与分线程之间进行的。主线程到分线程,NSOperation子类也有相应带参数的方法;而分线程到主线程,比如更新UI,它也有很方便的获取主队列(被添加到主队列的操作默认会在主线程执行)的方法:[NSOperationQueue mainQueue]。

 

1、NSInvocationOperation

(1)单个NSInvocationOperation

直接创建一个NSInvocationOperation的对象,然后调用start方法会直接在主线程执行:

//1.创建
  NSOperation *op = [[NSInvocationOperation alloc]initWithTarget:self 
selector:@selector(downloadImage:) object:@"Invocation"];
 //2.start方法,直接在当前线程执行
    [op start];

#pragma mark - 调用的耗时操作,后面调用的耗时操作都是这个
- (void)downloadImage:(id)obj{
  NSLog(@"%@-----%@",[NSThread currentThread],obj);
}

运行结果:

[1151:50868] <NSThread: 0x7fae624047b0>{number = 1, name = main}-----Invocation

添加到NSOperationQueue:

//1.创建
  NSOperation *op = [[NSInvocationOperation alloc]initWithTarget:self 
selector:@selector(downloadImage:) object:@"Invocation"];
 //2.放到队列里面去
    NSOperationQueue *q = [[NSOperationQueue alloc]init];
//只要把操作放到队列,会自动异步执行调度方法
    [q addOperation:op];

运行结果:

[1192:55469] <NSThread: 0x7fbe59e45c30>{number = 3, name = (null)}-----Invocation

在number为3,name为空的子线程执行。

 

(2)多个NSInvocationOperation

  //队列,GCD里面的并发队列使用最多,所以NSOperation技术直接把GCD里面的并发队列封装起来
  //NSOperationQueue本质就是GCD里面的并发队列
  //操作就是GCD里面异步执行的任务
  NSOperationQueue *q = [[NSOperationQueue alloc]init];

  //把多个操作放到队列里面
  for (int i = 0; i < 100; i++) {
    NSOperation *op = [[NSInvocationOperation alloc]initWithTarget:self 
         selector:@selector(downloadImage:) object:[NSString stringWithFormat:@"Invocation%d",i]];
    [q addOperation:op];
  }

运行结果:

**[1222:58476] <NSThread: 0x7fdc14b0cd20>{number = 7, name = (null)}-----Invocation5
**[1222:58478] <NSThread: 0x7fdc1357e5f0>{number = 9, name = (null)}-----Invocation7
**[1222:58307] <NSThread: 0x7fdc14a06ad0>{number = 3, name = (null)}-----Invocation1
**[1222:58477] <NSThread: 0x7fdc134916e0>{number = 8, name = (null)}-----Invocation6
**[1222:58481] <NSThread: 0x7fdc1357e120>{number = 12, name = (null)}-----Invocation10
**[1222:58475] <NSThread: 0x7fdc14801710>{number = 6, name = (null)}-----Invocation4
**[1222:58480] <NSThread: 0x7fdc13415630>{number = 11, name = (null)}-----Invocation9
**[1222:58306] <NSThread: 0x7fdc13512e20>{number = 4, name = (null)}-----Invocation3
··· ···

线程名与输出均没有规律,很明显就是并发队列。

 

2、NSBlockOperation

NSBlockOperation的用法与NSInvocationOperation相同,只是创建的方式不同,它不需要去调用方法,而是直接使用代码块,显得更方便。这也使得NSBlockOperation比NSInvocationOperation更加流行。

  //跟GCD中的并发队列一样
  NSOperationQueue *q = [[NSOperationQueue alloc]init];
  //跟GCD中的主队列一样
  // NSOperationQueue *q = [NSOperationQueue mainQueue];
  //把多个操作放到队列里面
  for (int i = 0; i < 100; i++) {
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
      NSLog(@"%@------%d",[NSThread currentThread],i);

   }];
    //把Block操作放到队列
    [q addOperation:op];
  }
  NSLog(@"完成");

并发队列输出结果:
**[1378:72440] <NSThread: 0x7f9cb2603460>{number = 6, name = (null)}------5**
**[1378:72442] <NSThread: 0x7f9cb48106a0>{number = 5, name = (null)}------7**
**[1378:72441] <NSThread: 0x7f9cb242b3e0>{number = 7, name = (null)}------6**
**[1378:72325] <NSThread: 0x7f9cb4851550>{number = 9, name = (null)}------2**
**[1378:72320] <NSThread: 0x7f9cb492be70>{number = 4, name = (null)}------3**
**[1378:72313] <NSThread: 0x7f9cb24077b0>{number = 2, name = (null)}------1**
**[1378:72276] 完成
**[1378:72444] <NSThread: 0x7f9cb481cc40>{number = 11, name = (null)}------9**
**[1378:72326] <NSThread: 0x7f9cb4923fe0>{number = 3, name = (null)}------0**
**[1378:72440] <NSThread: 0x7f9cb2603460>{number = 6, name = (null)}------12**
... ...

主队列输出结果:
**[1417:76086] 完成
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------0**
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------1**
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------2**
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------3**
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------4**
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------5**
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------6**
**[1417:76086] <NSThread: 0x7fa452e04360>{number = 1, name = main}------7**
... ...

 

事实上NSBlockOperation有更简单的使用方法:

NSOperationQueue *q = [[NSOperationQueue alloc]init];

  for (int i = 0; i < 10; i++) {

    [q addOperationWithBlock:^{
      NSLog(@"%@------%d",[NSThread currentThread],i);
    }];
  }

3、线程间通信

主线程到子线程传对象,前面的例子里面已经有了,不再缀述。下面的例子就是回到主线程更新UI。

p.p1 {margin: 0.0px 0.0px 0.0px 0.0px; font: 14.0px Menlo; c

NSOperationQueue *q = [[NSOperationQueue alloc]init];

  [q addOperationWithBlock:^{
    NSLog(@"耗时操作--%@",[NSThread currentThread]);
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
      NSLog(@"更新UI-----%@",[NSThread currentThread]);
    }];
  }];

4、NSOperationQueue的一些高级操作

NSOperationQueue支持的高级操作有:队列的挂起,队列的取消,添加操作的依赖关系和设置最大并发数。

(1)最大并发数

@property (nonatomic,strong)NSOperationQueue *opQueue;

//重写getter方法实现懒加载
- (NSOperationQueue*)opQueue{
  if (_opQueue == nil) {
    _opQueue = [[NSOperationQueue alloc]init]; 
  }
  return _opQueue;
}


#pragma mark - 高级操作:最大并发数

  //设置最大的并发数量(并非线程的数量)
  self.opQueue.maxConcurrentOperationCount = 2;
  //把多个操作放到队列里面
  for (int i = 0; i < 10; i++) {
    NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
      [NSThread sleepForTimeInterval:3.0];
      NSLog(@"%@------%d",[NSThread currentThread],i);

    }];
    //把Block操作放到队列
    [self.opQueue addOperation:op];
  }

(2)线程的挂起

#pragma mark - 高级操作:线程的挂起
//暂停继续(对队列的暂停和继续),挂起的是队列,不会影响已经在执行的操作
- (IBAction)pause:(UIButton *)sender {
  //判断操作的数量,当前队列里面是不是有操作?
  if (self.opQueue.operationCount == 0) {
    NSLog(@"当前队列没有操作");
    return;
  }

  self.opQueue.suspended = !self.opQueue.isSuspended;
  if (self.opQueue.suspended) {
    NSLog(@"暂停");

  }else{
    NSLog(@"继续");
  }
}

(3)取消队列里的所有操作

#pragma mark - 高级操作:取消队列里的所有操作
- (IBAction)cancelAll:(UIButton *)sender {
  //只能取消所有队列的里面的操作,正在执行的无法取消
  //取消操作并不会影响队列的挂起状态
  [self.opQueue cancelAllOperations];
  NSLog(@"取消队列里所有的操作");
  //取消队列的挂起状态
  //(只要是取消了队列的操作,我们就把队列处于不挂起状态,以便于后续的开始)
  self.opQueue.suspended = NO;

}

(4)依赖关系

 /*
  * 例子
  *
  * 1.下载一个小说压缩包
  *  2.解压缩,删除压缩包
  * 3.更新UI
  */

  NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"1.下载一个小说压缩包,%@",[NSThread currentThread]);

  }];

  NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"2.解压缩,删除压缩包,%@",[NSThread currentThread]);

  }];

  NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{
    NSLog(@"3.更新UI,%@",[NSThread currentThread]);

  }];

  //指定任务之间的依赖关系 --依赖关系可以跨队列(可以再子线程下载,在主线程更新UI)

  [op2 addDependency:op1];
  [op3 addDependency:op2];
//  [op1 addDependency:op3];  一定不能出现循环依赖

  //waitUntilFinished  类似GCD中的调度组的通知
  //NO不等待,直接执行输出come here
  //YES等待任务执行完再执行输出come here
  [self.opQueue addOperations:@[op1,op2] waitUntilFinished:YES];


  //在主线程更新UI
  [[NSOperationQueue mainQueue] addOperation:op3];
  [op3 addDependency:op2];
  NSLog(@"come here");

还有一个NSOperationQueuePriority,队列优先级的概念,因为用的极少,所以这里不做介绍。

 

六、三种多线程技术比较

1、NSThread

  • 优点:NSThread 比其他两个轻量级,使用简单
  • 缺点:需要自己管理线程的生命周期、线程同步、加锁、睡眠以及唤醒等。线程同步对数据的加锁会有一定的系统开销

2、GCD
GCD 是iOS 4.0以后才出现的并发技术

  • 使用方式:将任务添加到队列(串行/并行(全局)),指定执行任务的方法,(同步(阻塞)/异步 )
  • 拿到主队列:dispatch_get_main_queu()
  • NSOperation无法做到的:1.一次性执行,2.延迟执行,3.调度组(op实现要复杂的多 )

 

3、NSOperation
NSOperation iOS2.0的时候就出现了(当时不好用,后来苹果对其进行改造)

  • 使用方式:将操作(异步执行)添加到队列(并发/全局)
  • 拿到主队列:[NSOperationQueue mainQueue] 主队列,任务添加到主队列就会在主线程执行
  • 提供了GCD不好实现的:1.最大并发数,2.暂停和继续,3.取消所有任务,4.依赖关系

 

GCD是比较底层的封装,我们知道较低层的代码一般性能都是比较高的,相对于NSOperationQueue。所以追求性能,而功能够用的话就可以考虑使用GCD。如果异步操作的过程需要更多的用户交互和被UI显示出来,NSOperationQueue会是一个好选择。如果任务之间没有什么依赖关系,而是需要更高的并发能力,GCD则更有优势。
高德纳的教诲:“在大概97%的时间里,我们应该忘记微小的性能提升。过早优化是万恶之源。”只有Instruments显示有真正的性能提升时才有必要用低级的GCD。

原创文章,转载请注明: 转载自李峰峰博客

本文链接地址: iOS多线程详解

评论