iOS 多线程
1.线程与进程
1.1 线程的定义
- 线程是进程的基本执行单元,一个进程的所有任务大偶在线程中执行
- 进程要想执行任务,必须得有线程,进程至少要有一条线程
- 程序启动会默认开启一条线程,这条线程被称为主线程或UI线程
1.2进程
- 进程是系统进行资源和调度的基本单位
- 在移动端进程是指在系统中正在运行的一个应用程序(注:iOS是单进程,Android可以实现多进程)
- 每个进程之间是独立的,每个进程均运行在其专用的切受保护的内存
1.3进程与线程的关系
- 地址空间:统一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
- 资源拥有:同一进程内的线程共享本进程的资源内存、I/O、CPU等,但是进程之间的资源是独立的。
- 相互影响:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃后整个进程就死掉了,所以多进程要比多线程健壮
- 资源占用:进程切换时资源消耗大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
- 执行过程:每个独立的进程有一个程序的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须存在应用程序中(进程),有应用程序提供多个线程执行控制(多线程开发)。
- 线程是处理器调度的基本单位,但进程不是,线程也是进程执行的基本单位
1.4线程与队列的关系
多线程中的队列有:串行队列,并发队列,全局队列,主队列。
队可以理解为一种数据结构,以某种方式,等待线程去执行。
1.5 iOS线程与Runloop的关系
- 线程与Runloop是一一对应的,一个Runloop对应一个核心线程,为什么说是核心线程,因为Runloop是可以嵌套的,但是核心只有一个,他们的对应关系保存在一个全局字典里
- Runloop是用来管理线程的,线程执行完任务时会进入休眠状态,有任务进来时会被唤醒,开始执行任务。所以说Runloop是事件驱动的。
- Runloop在第一次获取时被创建,线程结束时被销毁。主线程的Runloop在程序启动的时候就会被创建。
- 子线程的Runloop是懒加载的,只有在使用的时候才被创建。
注:在子线程使用NSTimer时要注意确保子线程的Runloop已经创建,否则NSTimer不会生效。
2.多线程
2.1 多线程的定义
多线程是一个进程中并发多个线程同时执行各自的任务。就是由单核CPU通过时间片不断的切换执行程序。
分时操作系统会把CPU的时间划分为长短基本相同的时间区间(时间片),在一个时间片内,CPU只能处理一个线程中的一个任务,对于一个单核CPU来说,在不同的时间片来执行不同线程中的任务,就形成了多个任务在同时执行的“假象”。
2.2多线程的优缺点
1.优点
*减少应用程序的堵塞,增加程序的执行效率
*适当提高CPU和内存的利用率
*线程上的任务执行完成后,线程自动销毁(部分API可实现)
3.缺点
*线程的开启需要占用一定的内存空间,默认是512KB/线程
*线程开启的越多内存占用越大,会降低程序的性能
*线程越多CPU在调用线程上的开销就越大
*程序设计更加复杂,需要考虑线程间通信,多线程的数据共享等问题
2.3 多线程的生命周期
可能造成阻塞的条件:
锁(lock),循环引用,sleep()函数,循环执行
2.4 可调度线程池
什么是线程池:
提供一组线程资源用来复用线程资源的一个池子
线程池中常用参数释义:
- corePoolSize 线程池的基本大小(核心线程池大小)
- maximumPool 线程池最大大小
-
keepAliveTime 线程池中超过
corePoolSize
数目的空闲线程的最大存活时间 -
unit
keepAliveTime
参数的时间单位 - workQueue 任务阻塞队列
- threadFactory 新建线程的工厂
-
handler 当提交任务数超过
maximumPoolSize
与workQueue
之和时,任务会交给RejectedExecutionHandler
来处理
线程池调度原理:
饱和策略:
- AboutPolicy 直接抛出RejectedExecutionExeception异常,阻止系统的正常运行
- CallerRunsPolicy 将任务回退到调用者
- DisOldestPolicy 丢掉等待最久的任务
- DisCardPolicy 直接丢弃任务
注: 以上四种拒绝策略均实现的RejectedExencutionHandler
注:iOS开发中不会直接接触到线程池,这是因为GCD已经包含了线程池的管理,我们常用的就是GCD(NSOperation底层也是GCD),所以只需要通过GCD获取线程来执行任务即可。
继续探索多线程更底层的知识可以尝试搜索Java Posix
2.6 iOS的几种多线程技术方案
-
pthread
:即POSIX Thread
,是线程的POSIX
标准,是一套通用的多线程API,可以在Unix/Linux/Windows
等平台跨平台使用。iOS
中基本不使用。 -
NSThread
:苹果封装的面向对象的线程类,可以直接操作线程,比起GCD
,NSThread
效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。 -
GCD
:全称Grand Central Dispatch
,由C
语言实现,是苹果为多核的并行运算提出的解决方案,GCD
会自动利用更多的CPU
内核,自动管理线程的声明周期,程序员只需要告诉GCD
需要执行的任务,无需编写任何管理线程的代码。GCD
也是iOS
使用频率最高的多线程技术。 -
NSOperation
:基于GCD
封装的面向对象的多线程类,相较于GCD
提供了很多方便的API,使用频率较高。
3. 线程间的通讯
3.1 几种线程间的通讯方式
通过上面的介绍我们对线程有了基本的了解,那么线程之间是如何通讯的呢?其实我们都知道的就是在子线程获取数据,然后切换到主线程进行刷新UI
,其实这只是表象,并不是真正的线程通讯方式,如果在面试中这样回答基本就是回家等消息了。那么线程之间倒地是如何通讯的呢?在苹果官方文档中给我们列出了线程间通讯的几种方式:
线程之间有许多通信方式,每种方式都有其优缺点。配置线程本地存储列出了您可以在OS x中使用的最常见的通信机制(除了消息队列和Cocoa分布式对象,这些技术在iOS中也可用)。这个表中的技术是按照复杂度递增的顺序列出的。其中后两种只能在OS X中使用。
-
Direct messaging
:这个就是我们常用的selector
系列,例如-performSelector:
。 -
Global variables, shared memory, and objects
: 直接通过全局变量、共享内存等方式,但这种方式会造成资源抢夺,涉及到线程安全问题。 -
Conditions
:条件锁,一种特殊的锁,我们可以使用它来控制线程何时执行特定部分的代码。 -
Run loop sources
: 自定义Runloop
来设置用于在线程上接收应用程序特定消息的循环源,因为它们是事件驱动的,所以运行循环源会在无事可做时将线程自动休眠,从而提高线程的效率。 -
Ports and sockets
: 通过端口和套接字来实现线程间通讯。
4. iOS中使用线程间通讯示例(使用NSPort)
此处的例子是通过一个PortPerson
对象跟ViewController
进行发消息的实例。
代码中首先将VC
的self.myPort
添加到主线程的Runloop
中,然后起新线程让Person
像VC
发送消息,VC
在收到消息后也像Person
发送一条消息,在此过程中并没有切换线程的代码,通过打印线程可以看到VC
的操作是在主线程中完成的,Person
是在子线程(7)中完成的,通过这些代码就完成了线程间的通讯。
PortPerson 代码:
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface PortPerson : NSObject
- (void)personLaunchThreadWithPort:(NSPort *)port;
@end
NS_ASSUME_NONNULL_END
#import "PortPerson.h"
@interface PortPerson()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end
@implementation PortPerson
- (void)personLaunchThreadWithPort:(NSPort *)port {
@autoreleasepool {
NSLog(@"Person Thread %@", [NSThread currentThread]);
//1. 保存主线程传入的port
self.vcPort = port;
//2. 设置子线程名字
[[NSThread currentThread] setName:@"PortPersonThread"];
//3. 开启runloop
[[NSRunLoop currentRunLoop] run];
//4. 创建自己port
self.myPort = [NSMachPort port];
//5. 设置port的代理回调对象
self.myPort.delegate = self;
//6. 完成向主线程port发送消息
[self sendPortMessage];
}
}
- (void)sendPortMessage {
NSData *data1 = [@"这是一条port信息" dataUsingEncoding:NSUTF8StringEncoding];
// NSData *data2 = [@"data2" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data1,self.myPort]];
// 发送消息到VC的主线程
// 第一个参数:发送时间。
// msgid 消息标识。
// components,发送消息附带参数。
// reserved:为头部预留的字节数
[self.vcPort sendBeforeDate:[NSDate date]
msgid:10086
components:array
from:self.myPort
reserved:0];
}
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"person:handlePortMessage == %@",[NSThread currentThread]);
NSLog(@"从VC 传过来一些信息:");
NSLog(@"components == %@",[(id)message valueForKey:@"components"]);
NSLog(@"receivePort == %@",[(id)message valueForKey:@"receivePort"]);
NSLog(@"sendPort == %@",[(id)message valueForKey:@"sendPort"]);
NSLog(@"msgid == %@",[(id)message valueForKey:@"msgid"]);
}
@end
PortViewController 代码:
#import "PortViewController.h"
#import <objc/runtime.h>
#import "PortPerson.h"
@interface PortViewController ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *myPort;
@property (nonatomic, strong) PortPerson *person;
@end
@implementation PortViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// 创建主线程的port,子线程通过此端口发送消息给主线程
self.myPort = [NSMachPort port];
// 设置port的代理回调对象
self.myPort.delegate = self;
// 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.person = [[PortPerson alloc] init];
[NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
toTarget:self.person
withObject:self.myPort];
}
- (void)handlePortMessage:(NSPortMessage *)message {
NSLog(@"VC Thread --- %@", [NSThread currentThread]);
NSLog(@"从 person 传过来一些信息");
NSArray *messageArr = [(id)message valueForKey:@"components"];
NSString * dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"通过port传过来一些信息:%@", dataStr);
NSPort *destinPort = [(id)message valueForKey:@"remotePort"];
if(!destinPort || ![destinPort isKindOfClass:[NSPort class]]){
NSLog(@"传过来的数据有误");
return;
}
NSData *data = [@"VC收到!!!" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *array =[[NSMutableArray alloc]initWithArray:@[data,self.myPort]];
// 非常重要,如果你想在Person的port接收信息,必须加入到当前主线程的runloop
[[NSRunLoop currentRunLoop] addPort:destinPort forMode:NSDefaultRunLoopMode];
NSLog(@"VC == %@",[NSThread currentThread]);
BOOL success = [destinPort sendBeforeDate:[NSDate date]
msgid:10010
components:array
from:self.myPort
reserved:0];
NSLog(@"%d",success);
}
@end
打印结果:
注意!!!
-
NSPort
对象必须添加到要接收消息的线程的Runloop
中。 - 接收消息的对象实现
NSPortDelegate
协议的-handlePortMessage:
方法来获取消息内容。!