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

1,215 阅读19分钟

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


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


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

###2.进程

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

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

###3.线程

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

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

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

###4.多线程

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

原理:

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

优点:

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

缺点:

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

##二、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方法运行在它自己独立的线程中,可以使用这个类。当你想执行一个比较耗时(冗长)的操作而又不想阻塞程序其他部分的运行状态时,线程是特别有用的。尤其是你可以使用线程来避免阻塞主线程处理用户界面以及和事件相关的活动。线程可以将待处理任务分割成小任务以提高多核计算机的性能。 ##一、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 ##一、GCD简介 GCD(Grand Central Dispatch) 伟大的中央调度系统,是苹果为多核并行运算提出的C语言并发技术框架。

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

一些专业术语

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

##二、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 中队列的释放,建议选择使用全局队列 + 异步任务。 ##三、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(), ^{
    // 等前面的异步操作都执行完毕后,回到主线程...
});

##四、单例模式

作用: 可以保证在程序运行过程,一个类只有一个实例,而且该实例易于供外界访问。从而方便地控制了实例个数,并节约系统资源 使用场合: 在整个应用程序中,共享一份资源(这份资源只需要创建初始化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