线程和进程
进程:
- 进程是指系统中正在运行的一个应用程序
- 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内。
- 通过
活动监视器可以查看Mac系统中所开启的进程
线程
- 线程是进程的基本执行单元,一个进程的所有任务都是在线程中执行的。
- 进程想要执行任务,必须得有线程,进程至少要有一条线程。
- 程序启动会默认开启一条线程,被称为
主线程或者`UI线程·。
线程和进程的关系
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
资源拥有:同一进程内的线程共享本进程的资源,如内存、I/O、cpu等,但是进程之间的资源是独立的。
一个
进程奔溃后,在保护模式下不会对其他进程产生影响。但是一个线程崩溃,整个进程都会死掉。进程切换时,消耗的资源大,效率高。所以涉及到频繁的的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作时,只能用线程不能用进程。执行过程:每个独立的
进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存于应用程序中,由应用程序提供多个线程执行控制。线程是处理器调度的基本单元,但进程不是。线程没有地址空间,线程包含在进程地址空间中。
线程和runloop的关系
-
runloop和线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,因为runloop是可以嵌套的,但是核心职能有一个,他们的关系保存在一个全局的字典里。 -
runloop是用来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。 -
runloop在第一次获取时被创建,在线程结束时被销毁 - 对于
主线程来说,runloop在程序一启动就默认创建好了。 - 对于
子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器时要注意:确保子线程的runloop被创建,不然定时器不会回调
多线程的意义
优点:
- 能适当提高程序的执行效率
- 能适当提高资源利用率(cpu、内存)
- 线程上的任务执行完成后,线程会自动销毁
缺点:
- 开启线程需要占用一定的内存空间(默认情况下,一个线程
512KB) - 如果开启
大量的线程,会占用大量的内存,降低程序的性能。 - 线程越多,
CPU在调用线程上的开销就越大 - 程序设计更加复杂,比如线程间的通信、多线程间的数据共享。
时间片
时间片的概念:CPU在多个任务之间进行快速的切换,这个时间间隔就是时间片
-
单核CPU:同一时间,CPU只能处理1个线程- 换言之,同一时间只有1个线程在执行
- 多线程同时执行:
- 实际上是
CPU快速的在多个线程之间切换 -
CPU调度线程的时间足够快,就造成了多线程“同时执行”的效果
- 实际上是
- 如果线程非常多
-
CPU会在N个线程中切换,消耗大量的CPU资源 - 每个线程被调度的次数会降低,线程的执行效率低。
-
多线程技术方案
iOS中多线程方案,主要有pthread、NSThread、GCD、NSOperation

简单例子
下面演示这4种方式开启线程的简单实例。
//0: pthread
/**
pthread_create 创建线程
参数:
1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
同时不需要 `*`
2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
3. 线程要执行的`函数地址`
void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*): 函数名
(void *): 参数类型,void *
4. 传递给第三个参数(函数)的`参数`
返回值:C 语言框架中非常常见
int
0 创建线程成功!成功只有一种可能
非 0 创建线程失败的错误码,失败有多种可能!
*/
// 1: pthread
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
// NSString *ocString = @"Gavin";
//延伸到: OC--C的混编 尤其在智能家居,SDK封装
//抛出一个问题: 在ARC需要这样操作,在MRC不需要
// OC prethread -- 跨平台
// 锁
int result = pthread_create(&threadId, NULL, pthreadTest, cString);
if (result == 0) {
NSLog(@"成功");
} else {
NSLog(@"失败");
}
// 2: NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
// 3: GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
// 4: NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];
C与OC的桥接
-
__bridge只做类型转换,不修改对象(内存)管理权 -
__bridge_retained(也可以使用CFBridgingRetain),将Objective-C对象转换成Core Foundation的对象,同时将内存管理权也交给了Core Foundation,后续需要使用CFRelease或相关方法来释放对象 -
__bridge_transfer(也可以使用CFBridgingRelease),将Core Foundation对象转换成Objective-C的对象,同时将内存管理权交给ARC
线程的生命周期

- 创建一个新线程,调用
start方法时,并没有立即执行,只是进入了就绪状态。而是经过短暂的等待,等到CPU调度才真正执行 - 直接调用
exit、cancle等可以直接终止线程,执行完毕也会终止。
cancle不能取消正在执行的任务。
线程池
线程池是为了管理线程而提出的一个概念。

核心线程在没有任务的时候也不会被回收,而非核心线程是有存活时间的。
那么线程池的执行流程是怎么样的呢?

饱和策略
-
AbortPolicy:直接抛出RejectedExecutionExeception异常,阻止系统正常运行 -
CallerRunsPolicy:将任务回退给主线程 -
DisCardOldSetPolicy:丢弃等待最久的任务,替换为当前进来的任务执行 -
DisCardPolicy:丢弃进来的任务,不执行也不抛出异常
任务执行速度分析
线程优先级threadPriority越高是否任务执行速度越快?
threadPriority是一个double类型,取值(0~1),在iOS8.0之后被qualityOfService替代

typedef NS_ENUM(NSInteger, NSQualityOfService) {
NSQualityOfServiceUserInteractive = 0x21,
NSQualityOfServiceUserInitiated = 0x19,
NSQualityOfServiceUtility = 0x11,
NSQualityOfServiceBackground = 0x09,
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
NSQualityOfServiceUserInteractive用户交互优先级最高。
答案:任务的执行速度,除了看优先级,还要看任务的难易程度,消耗资源的大小,以及cpu的调度。就算优先级高,一个难得任务也会比简单任务执行的慢。
资源抢夺
多线程同时访问一块资源时,可能会引发数据错乱和数据安全问题。一般需要加锁来保证每个线程看到的数据是一致的,同时保证每个线程对数据的修改一致。
使用较多的是下面两种锁:
- 自旋锁
- 互斥锁
自旋锁
一个执行单元想要访问被自旋锁保护的共享资源,必须先得到锁,在访问完共享资源后,必须释放锁。如果在获取自旋锁时,没有任何执行单元持有该锁,那么将立即得到锁;如果在获取时,锁已经有了持有者,那么获取锁的操作将自旋在那里,直到自选锁的持有者释放了锁。
线程会反复检查锁变量是否可用,由于线程在这一过程中一直保持执行,因此是一种忙等(忙碌等待)。一旦获取了自旋锁,线程会一直持有该锁,直至显式释放自旋锁。
自选锁可能会存在两个问题:
-
死锁:
试图递归的获得自旋锁必然会造成死锁:递归程序的持有实例在第二实例循环,试图获得相同的自旋锁,不会释放此自旋锁。- 递归程序中使用自旋锁应遵循以下策略:
递归程序不能在持有自旋锁时调用它自己,也不能在递归时试图获得相同的自旋锁。
- 递归程序中使用自旋锁应遵循以下策略:
过多的占用系统资源:
如果不加限制,由于申请者一直在循环等待,不成功也不会睡眠,会持续的尝试。单cpu是自旋锁会让其他进程动不了。
由此可见自旋锁比较适用于锁的保持者保持时间比较短的情况。正是由于锁的持有者保存锁的时间非常短,所以才旋转自旋而不是睡眠。自旋锁的效率是远高于互斥锁。
互斥锁
互斥锁和自旋锁一样,都是用于线程同步的锁。保护一个对象,使其同时只能被一个线程保护。同一线程多次加锁会造成死锁。
- 与自旋锁不同的是,当线程访问的变量正在被其线程访问时,互斥锁不会反复询问,而是进入休眠(
挂起状态不占用cpu资源),直到前一个线程解除对这个变量的锁定,第二个线程被`唤醒并继续执行,同时锁定这个变量。
互斥锁还需要注意一下几点:
- 互斥锁的锁定范围应该尽量小,范围越大,效率越差。
- 能够加锁任意
NSObject对象
3.锁对象一定要保证所有的线程都能访问
4.如果代码中只有一个地方需要加锁,大多使用self,这样可以避免单独再创建一个锁对象
atomic和nonatomic
atomic和nonatomic都是用来修饰属性的,以下是他们之间的区别:
atomic:
- 是
原子属性,是为多线程开发准备的,是默认属性 - 仅在
setter方法中增加了自旋锁,保证了同一时间只有一条线程可以对属性进行写操作。多读单写 - 多用于
Mac开发 - 线程安全,需要
消耗大量的资源,而且不是绝对意义上的线程安全。
nonatomic:
- 是
非原子属性的,没有锁,效率高 - 移动开发中常用。
iOS开发的建议:所有属性都声明nonatomic,尽量避免多线程抢夺同一资源,尽量将加锁、抢夺资源的业务逻辑交给服务端处理,减少客户端的压力
线程通讯
NSThread可以使用performSelectorInBackground
performSelector: onThread:和performSelectorOnMainThread在主线程和子线程中切换。
GCD可以使用dispatch_async开启子线程,也可以使用dispatch_get_main_queue()回到主线程。
端口通讯
线程之间的通讯还可以使用NSPort。
了解NSPort
-NSPort是描述通信通道的抽象类。在两个NSPort对象之间通过- (BOOL)sendBeforeDate:(NSDate *)limitDate msgid:(NSUInteger)msgID components:(nullable NSMutableArray *)components from:(nullable NSPort *)receivePort reserved:(NSUInteger)headerSpaceReserved;发送消息和- (void)handlePortMessage:(NSPortMessage *)message;接收消息,来实现线程(或进程或应用)之间的通信。
-
NSPort对象必须作为输出源添加到NSRunLoop对象中,来实现线程不退出。 -
NSPort有3个子类NSMachPort、NSMessagePort和NSSocketPort。
线程间通信的思路
- 在主线程创建一个
NSPort的实例portA,并添加到主线程runloop中 - 创建一个子线程
S,将portA作为参数发送到线程S中 - 在线程
S中再创建一个本地的NSPort的实例portB,也添加到runloop中 -
portB向portA发送消息,将线程S的runloop执行 - 主线程收到线程
S的消息 - 主线程向线程
S发送消息 - 通过
handlePortMessage:代理方法接收消息。
实例代码
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Port线程通讯";
self.view.backgroundColor = [UIColor whiteColor];
//1. 创建主线程的port
// 子线程通过此端口发送消息给主线程
self.myPort = [NSMachPort port];
//2. 设置port的代理回调对象
self.myPort.delegate = self;
//3. 把port加入runloop,接收port消息
[[NSRunLoop currentRunLoop] addPort:self.myPort forMode:NSDefaultRunLoopMode];
self.person = [[KCPerson alloc] init];
[NSThread detachNewThreadSelector:@selector(personLaunchThreadWithPort:)
toTarget:self.person
withObject:self.myPort];
}
- (void)handlePortMessage:(NSPortMessage *)message{
NSLog(@"VC == %@",[NSThread currentThread]);
NSLog(@"从person 传过来一些信息:");
// NSLog(@"localPort == %@",[message valueForKey:@"localPort"]);
// NSLog(@"remotePort == %@",[message valueForKey:@"remotePort"]);
// NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
// NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
// NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
// NSLog(@"components == %@",[message valueForKey:@"components"]);
//会报错,没有这个隐藏属性
//NSLog(@"from == %@",[message valueForKey:@"from"]);
NSArray *messageArr = [message valueForKey:@"components"];
NSString *dataStr = [[NSString alloc] initWithData:messageArr.firstObject encoding:NSUTF8StringEncoding];
NSLog(@"传过来一些信息 :%@",dataStr);
NSPort *destinPort = [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);
}
@interface KCPerson()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *vcPort;
@property (nonatomic, strong) NSPort *myPort;
@end
@implementation KCPerson
- (void)personLaunchThreadWithPort:(NSPort *)port{
NSLog(@"VC 响应了Person里面");
@autoreleasepool {
//1. 保存主线程传入的port
self.vcPort = port;
//2. 设置子线程名字
[[NSThread currentThread] setName:@"KCPersonThread"];
//3. 开启runloop
[[NSRunLoop currentRunLoop] run];
//4. 创建自己port
self.myPort = [NSMachPort port];
//5. 设置port的代理回调对象
self.myPort.delegate = self;
//6. 完成向主线程port发送消息
[self sendPortMessage];
}
}
/**
* 完成向主线程发送port消息
*/
- (void)sendPortMessage {
NSData *data1 = [@"Gavin" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data2 = [@"Cooci" 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 == %@",[message valueForKey:@"components"]);
NSLog(@"receivePort == %@",[message valueForKey:@"receivePort"]);
NSLog(@"sendPort == %@",[message valueForKey:@"sendPort"]);
NSLog(@"msgid == %@",[message valueForKey:@"msgid"]);
}
打印结果如下:
VC == <NSThread: 0x600000684900>{number = 1, name = main}
从person 传过来一些信息:
传过来一些信息 :Gavin
VC == <NSThread: 0x600000684900>{number = 1, name = main}
1
person:handlePortMessage == <NSThread: 0x600000684900>{number = 1, name = main}
从VC 传过来一些信息:
components == (
{length = 11, bytes = 0x5643e694b6e588b0212121},
"<NSMachPort: 0x6000024884d0>"
)
receivePort == <NSMachPort: 0x60000249c000>
sendPort == <NSMachPort: 0x6000024884d0>
msgid == 10010
注意点
handlePortMessage方法
查看NSMachPort的delegate属性为NSMachPortDelegate,而NSPort的delegate属性为NSPortDelegate。NSMachPortDelegate是继承于NSPortDelegate子线程运行RunLoop
调用[[NSRunLoop currentRunLoop] run];的时机不同,就决定是否子线程能够通过NSPort接受到主线程的消息。也就是必须将[[NSRunLoop currentRunLoop] run];一定要放到[self sendPortMessage];之后。
- components的类型
components数组里的内容必须是NSPort或者是NSData。
