iOS多线程基础(想不会都难)

标签(空格分隔): iOS多线程 NSThread NSOpearation GCDios


本文是在简述做者:开发者zuoios多线程系列文章基础上我的的补充,感谢原做!程序员


图片来自互联网
#第一部分 多线程基础 ##1、线程的基本概念 ###1.多线程出现的背景 在计算机编程中,一个基本的概念就是同时对多个任务加以控制。许多程序设计问题都要求程序可以停下手头的工做,改成处理其余一些问题,再返回主进程。能够经过多种途径达到这个目的。多线程是为了同步完成多项任务,不是为了提升运行效率,而是为了经过提升资源使用效率来提升系统整体的效率。线程是在同一时间须要完成多项任务的时候执行的。

###2.进程面试

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操做系统结构的基础。编程

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

###3.线程安全

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

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

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

###4.多线程多线程

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

原理:

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

优势:

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

缺点:

  • 开启线程须要占用必定的内存空间(默认状况下,主线程占用1M,子线程占用512KB),若是开启大量的线程,会占用大量的内存空间,下降程序的性能
  • 线程越多,CPU在调度线程上的开销就越大

##2、iOS开发中的应用 ###1.主线程 一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”。

做用: 显示\刷新UI界面 处理UI事件(好比点击事件、滚动事件、拖拽事件等) 注意: 刷新UI必须放在主线程 别将比较耗时的操做放到主线程中 耗时操做会卡住主线程,严重影响UI的流畅度 ###2.实现方案

实现方案


#第二部分 NSThread 先看一段API文档的描述 An NSThread object controls a thread of execution. Use this class when you want to have an Objective-C method run in its own thread of execution. Threads are especially useful when you need to perform a lengthy task, but don’t want it to block the execution of the rest of the application. In particular, you can use threads to avoid blocking the main thread of the application, which handles user interface and event-related actions. Threads can also be used to divide a large job into several smaller jobs, which can lead to performance increases on multi-core computers.

大概的意思是:一个NSThread对象管理一个线程的执行。当你想要将一个Objective-C方法运行在它本身独立的线程中,可使用这个类。当你想执行一个比较耗时(冗长)的操做而又不想阻塞程序其余部分的运行状态时,线程是特别有用的。尤为是你可使用线程来避免阻塞主线程处理用户界面以及和事件相关的活动。线程能够将待处理任务分割成小任务以提升多核计算机的性能。 ##1、NSThread的使用

###1.线程的建立

方式一:

/ *  建立并启动线程
     *
     *  参数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;
    }
复制代码

###2.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];
复制代码

###3.线程的状态

线程的状态
] <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,内存空间被释放。

###4.多线程的安全问题 多个线程访问同一块资源进行读写,若是不加控制随意访问容易产生数据错乱从而引起数据安全问题。为了解决这一问题,就有了加锁的概念。加锁的原理就是当有一个线程正在访问资源进行写的时候,不容许其余线程再访问该资源,只有当该线程访问结束后,其余线程才能按顺序进行访问。对于读取数据,有些程序设计是容许多线程同时读的,有些不容许。

解决多线程安全问题 <1>互斥锁

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

使用互斥锁,在同一个时间,只容许一条线程执行锁中的代码.由于互斥锁的代价很是昂贵,因此锁定的代码范围应该尽量小,只要锁住资源读写部分的代码便可。使用互斥锁也会影响并发的目的。

<2>原子属性

@property (strong, nonatomic) UIWindow *window;
复制代码

atomic:可以实现“单写多读”的数据保护,同一时间只容许一个线程修改属性值,可是容许多个线程同时读取属性值,在多线程读取数据时,有可能出现“脏”数据 - 读取的数据可能会不正确。原子属性是默认属性,若是不须要考虑线程安全,要指定 nonatomic。

atomic(原子属性)在setter方法内部加了一把自旋锁 nonatomic(非原子属性)下,set和get方法都不会加锁,消耗资源小适合内存小的移动设备

UIKit中几乎全部控件都不是线程安全的,所以须要在主线程上更新UI

原子属性内部使用的 自旋锁 自旋锁和互斥锁的区别

共同点: 均可以锁定一段代码。 同一时间, 只有线程可以执行这段锁定的代码

区别:互斥锁,在锁定的时候,其余线程会睡眠,等待条件知足,再唤醒
自旋锁,在锁定的时候, 其余的线程会作死循环,一直等待这条件知足,一旦条件知足,立马去执行,少了一个唤醒过程
复制代码

// 在主线程更新UI,有什么好处?

  1. 只在主线程更新UI,就不会出现多个线程同时改变 同一个UI控件
  2. 主线程的优先级最高。也就意味UI的更新优先级高。 会让用户感受很流畅

开发建议

1.全部属性都声明为nonatomic 2.尽可能避免多线程抢夺同一块资源 3.尽可能将加锁、资源抢夺的业务逻辑交给服务器端处理,减少移动客户端的压力

###5.自动释放池和运行循环 <1>运行循环 做用:保证程序不退出,坚挺全部事件,例如:手势触摸,网络加载等 特性:没有事件时,会休眠(省电),一旦监听到事件,会当即响应,每个线程都有一个 runloop,可是只有主线程的 runloop 会默认启动。

<2>自动释放池 工做原理:自动释放池被销毁或耗尽时会向池中全部对象发送 release 消息,释放全部 autorelease 的对象! 建立和销毁:每一次运行循环启动后会建立自动释放池;程序执行过程当中,全部 autorelease 对象在出了做用域以后,会被添加到最近建立的自动释放池中;运行循环结束前,会释放自动释放池。 自动释放池在ARC中一样须要。 工做原理图:

Screen Shot 2016-05-03 at 20.54.21.png

常见面试题:

int largeNumber = 2 * 1024 * 1024; // 问题:(1)如下代码是否存在问题?(2)若是有,怎么修改?

for (int i = 0; i < largeNumber; i++) {
    @autoreleasepool {
        NSString *str = [NSString stringWithFormat:@"Hello "];
        str = [str uppercaseString];
        str = [str stringByAppendingString:@" - World"];
    }
}
复制代码

网上的解决办法: 1)@autoreleasepool 放在外面,保证循环以后释放循环中的自动释放对象 2)@autoreleasepool 放在内部,每一次循环以后,都倾倒一次自动释放池,内存管理是最好的,可是性能很差!

###6.线程通讯(方法继承自NSObject)

Screen Shot 2016-05-04 at 00.09.18.png

//在主线程上执行操做,例如给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 ##1、GCD简介 GCD(Grand Central Dispatch) 伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。

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

一些专业术语

dispatch :派遣/调度
    
queue:队列
    用来存听任务的先进先出(FIFO)的容器
sync:同步
    只是在当前线程中执行任务,不具有开启新线程的能力
async:异步
    能够在新的线程中执行任务,具有开启新线程的能力
concurrent:并发
    多个任务并发(同时)执行
串行:
    一个任务执行完毕后,再执行下一个任务
复制代码

##2、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>自定义队列 自定义队列又分为:串行队列和并发队列 串行队列 串行队列一次只调度一个任务,一个任务完成后再调度下一个任务

// 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
复制代码

<2>主队列 主队列负责在主线程上调度任务,若是在主线程上有任务执行,会等待主线程空闲后再调度任务执行。 主队列用于UI以及触摸事件等的操做,咱们在进行线程间通讯,一般是返回主线程更新UI的时候使用到

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

<3>全局并发队列

全局并发队列是由苹果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);
复制代码

<4>全局并发队列与并发队列的区别

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

<5>QOS (服务质量) iOS 8.0 推出

QOS_CLASS_USER_INTERACTIVE:用户交互,会要求 CPU 尽量地调度此任务,耗时操做不该该使用此服务质量
QOS_CLASS_USER_INITIATED:用户发起,比 QOS_CLASS_USER_INTERACTIVE 的调度级别低,可是比默认级别高;耗时操做一样不该该使用此服务质量;若是用户但愿任务尽快执行完毕返回结果,能够选择此服务质量;
QOS_CLASS_DEFAULT:默认,此 QOS 不是为添加任务准备的,主要用于传送或恢复由系统提供的 QOS 数值时使用
QOS_CLASS_UTILITY:实用,耗时操做可使用此服务质量;
QOS_CLASS_BACKGROUND:后台,指定任务以最节能的方式运行
QOS_CLASS_UNSPECIFIED:没有指定 QOS
复制代码

###3.执行任务的函数 <1>同步(dispatch_sync)

执行完这一句代码,再执行后续的代码就是同步

任务被添加到队列后,会当前线程被调度;队列中的任务同步执行完成后,才会调度后续任务。-在主线程中,向主队列添加同步任务,会形成死锁 -在其余线程中,向主队列向主队列添加同步任务,则会在主线程中同步执行。 具体是否会形成死锁,以及死锁的缘由,还须要针对具体的状况分析,理解队列和执行任务的函数才是关键。实际开发中通常只要记住经常使用的组合就能够了。 咱们能够利用同步的机制,创建任务之间的依赖关系 例如:

用户登陆后,才可以并发下载多部小说 只有“用户登陆”任务执行完成以后,多个下载小说的任务才可以“异步”执行 全部下载任务都依赖“用户登陆”

<2>异步(dispatch_async)

没必要等待这一句代码执行完,就执行下一句代码就是异步

异步是多线程的代名词,当任务被添加到主队列后,会等待主线程空闲时才会调度该任务;添加到其余线程时,会开启新的线程调度任务。 <3>以函数指针的方式调度任务 函数指针的调用方式有两种,一样是同步和异步;函数指针的传递相似于 pthread。

dispatch_sync_f
dispatch_async_f
复制代码

函数指针调用在实际开发中几乎不用,只是有些面试中会问到,dispatch + block 才是 gcd 的主流! ###4.开发中如何选择队列 选择队列固然是要先了解队列的特色 串行队列:对执行效率要求不高,对执行顺序要求高,性能消耗小 并发队列:对执行效率要求高,对执行顺序要求不高,性能消耗大 若是不想兼顾 MRC 中队列的释放,建议选择使用全局队列 + 异步任务。 ##3、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(), ^{
    // 等前面的异步操做都执行完毕后,回到主线程...
});
复制代码

##4、单例模式

做用: 能够保证在程序运行过程,一个类只有一个实例,并且该实例易于供外界访问。从而方便地控制了实例个数,并节约系统资源 使用场合: 在整个应用程序中,共享一份资源(这份资源只须要建立初始化1次)

实现方法 重写实现

// 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;
}
复制代码

宏实现

// .h文件
#define SingletonH(name) + (instancetype)shared##name;

// .m文件
#define SingletonM(name) 
static id _instance; 
 
+ (instancetype)allocWithZone:(struct _NSZone *)zone 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [super allocWithZone:zone]; 
    }); 
    return _instance; 
} 
 
+ (instancetype)shared##name 
{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        _instance = [[self alloc] init]; 
    }); 
    return _instance; 
} 
 
- (id)copyWithZone:(NSZone *)zone 
{ 
    return _instance; 
}
复制代码

#第四部分 NSOperation 文章太长了,最后一部分独立出来吧。 [NSOperation以及多线程技术比较]3

相关文章
相关标签/搜索