020*:多线程基础知识:(进程、线程、线程池)(线程生命周期【新建、就绪、运行、阻塞、销毁】)(锁【自旋锁 原子锁、互斥锁】)(线程、runloop、NSPort)(线程通讯)
问题
(进程、线程、线程池)
(线程生命周期【新建、就绪、运行、阻塞、销毁】)
(锁【自旋锁 原子锁、互斥锁】)
(线程、runloop、NSPort)
(线程通讯)
目录
1:进程与线程
2: 多线程的优缺点
3:多线程的内存消耗
4:多线程处理方案
5:线程生命周期
6:线程池的原理
7:线程优先级
8:锁
9:线程和Runloop的关系
10:线程通讯
11:NSPort通讯
预备
正文
1:进程与线程
1:进程
- 1.进程是指在
系统中正在运⾏的⼀个应⽤程序
- 2.
每个进程之间是独⽴的
,每个进程均运⾏在其专⽤的且受保护的内存空间内,是分配资源的最小单位 - 3.通过“活动监视器”可以查看 Mac 系统中所开启的进程
2:线程
- 1.
线程是进程的基本执⾏单元
,⼀个进程的所有任务都在线程中执⾏
- 2.
进程要想执⾏任务,必须得有线程
,进程⾄少要有⼀条线程
- 3.程序启动会
默认开启⼀条线程
,这条线程被称为主线程
或 UI 线程
3:线程和进程的联系
- 1.
地址空间
:同⼀进程的线程共享本进程的地址空间
,⽽进程之间则是独⽴的地址空间
。 - 2.
资源拥有
:同⼀进程内的线程共享本进程的资源
如内存、I/O、cpu等,但是进程之间的资源是独⽴的
。- 1:
⼀个进程崩溃后,在保护模式下不会对其他进程产⽣影响,但是⼀个线程崩溃整个进程都死掉。所以多进程要⽐多线程健壮
。 - 2:
进程切换时,消耗的资源⼤,效率⾼。所以涉及到频繁的切换时,使⽤线程要好于进程
。同样如果要求同时进⾏并且⼜要共享某些变量的并发操作,只能⽤线程不能⽤进程
- 3: 执⾏过程:
每个独⽴的进程有⼀个程序运⾏的⼊⼝、顺序执⾏序列和程序⼊⼝
。但是线程不能独⽴执⾏,必须依存在应⽤程序中,由应⽤程序提供多个线程执⾏控制
。 - 4:
线程是处理器调度的基本单位,但是进程不是
。 - 5:
线程没有地址空间,线程包含在进程地址空间中
。
- 1:
通信方式之间的差异
因为那个根本原因,实际上只有进程间需要通信,同一进程的线程共享地址空间,没有通信的必要,但要做好同步/互斥,保护共享的全局变量。
而进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用。
管道(pipe)
管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
有名管道 (namedpipe)
有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
信号量(semaphore)
信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
信号 (sinal)
信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
共享内存(shared memory)
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
套接字(socket)
套接口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同设备及其间的进程通信。
线程间的通信方式
信号机制(Signal)和加锁
类似进程间的信号处理。
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
2. 多线程的优缺点
优点
- 能适当
提⾼程序的执⾏效率
- 能适当
提⾼资源的利⽤率
(CPU,内存) - 线程上的任务执⾏完成后,
线程会⾃动销毁
缺点
- 开启线程需要
占⽤⼀定的内存空间
(默认情况下,每⼀个线程都占 512 KB) - 如果
开启⼤量的线程,会占⽤⼤量的内存空间
,降低程序的性能 线程越多,CPU 在调⽤线程上的开销就越⼤
程序设计更加复杂
,⽐如线程间的通信、多线程的数据共享
多线程的实际实现
1.单核:同一时间,CPU只能处理一个线程
,也就是说同一时间只有一个线程在执行。
2.多线程同时执行:CPU快速的在多个线程之间切换
。CPU调度线程的时间足够快,就造成了多线程'同时'执行的效果
3.线程非常多:CPU会在N个线程之间切换,消耗大量的CPU资源
,每个线程的被调度次数会降低,线程的执行效率降低
3:多线程的内存消耗
官方文档对多线程的内存消耗有个说明官方文档传送门
1.线程内核数据结构大约占1KB
,主要用于存储线程数据结构和属性
,其中大部分是作为连接内存分配
的,因此不能分页到磁盘
。
2.栈的控件大小:子线程512kb,OS X的主线程是8MB,iOS的主线程是1MB
。子线程允许的最小堆栈大小是16 KB
,堆栈大小必须是4 KB的倍数
。该内存的空间是在线程创建时在进程空间中留出的,但是只有在需要时才创建与该内存关联的实际页面。
3.创建时间大概90微秒
,这个值反映了从创建线程的初始调用到线程入口点例程开始执行的时间之间的时间
。这些数字是通过分析在运行OS X v10.5、配置2 GHz双核处理器和1 GB RAM、基于intel的iMac上创建线程时生成的平均值和中值得出的。
4:多线程处理方案
3.1 Pthread__bridge
只做类型转换,但是不修改对象(内存)管理权;__bridge_retained
(也可以使用CFBridgingRetain
)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象;__bridge_transfer
(也可以使用CFBridgingRelease
)将Core Foundation
的对象 转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。
5:线程生命周期
新建、就绪、运行、阻塞、销毁
-
新建
:主要是实例化线程对象 -
就绪
:线程对象调用start方法
,将线程对象加入可调度线程池
,等待CPU的调用
,即调用start方法,并不会立即执行
,进入就绪状态
,需要等待一段时间,经CPU调度后才执行
,也就是从就绪状态进入运行状态
-
运行
:CPU负责调度可调度线城市中线程的执行
,在线程执行完成之前,其状态可能会在就绪和运行之间来回切换,这个变化是由CPU负责
,开发人员不能干预。 -
阻塞
:当满足某个预定条件时,可以使用休眠,即sleep,或者同步锁
,阻塞线程执行。当进入sleep时,会重新将线程加入就绪
中。下面关于休眠的时间设置,都是NSThread
的-
sleepUntilDate:
阻塞当前线程,直到指定的时间为止,即休眠到指定时间
-
sleepForTimeInterval:
在给定的时间间隔内休眠线程,即指定休眠时长
-
同步锁:
@synchronized(self):
-
-
死亡
:分为两种情况-
正常死亡
,即线程执行完毕 -
非正常死亡
,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)
-
简要说明,就是处于运行中的线程
拥有一段可以执行的时间(称为时间片
),
-
如果
时间片用尽
,线程就会进入就绪状态队列
-
如果
时间片没有用尽
,且需要开始等待某事件
,就会进入阻塞状态队列
-
等待事件发生后,线程又会重新进入
就绪状态队列
-
每当一个
线程离开运行
,即执行完毕或者强制退出后,会重新从就绪状态队列
中选择一个线程继续执行
线程的exit
和cancel
说明
- exit
:一旦强行终止线程,后续的所有代码都不会执行
注意: `cancel`:取消当前线程,但是不能取消正在执行的线程
6:线程池的原理
- 【第一步】判断核心线程池是否都正在执行任务
-
返回NO,创建新的工作线程去执行
-
返回YES,进入【第二步】
-
- 【第二步】判断线程池工作队列是否已经饱满
-
返回NO,将任务存储到工作队列,等待CPU调度
-
返回YES,进入【第三步】
-
- 【第三步】判断线程池中的线程是否都处于执行状态
-
返回NO,安排可调度线程池中空闲的线程去执行任务
-
返回YES,进入【第四步】
-
- 【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)
-
AbortPolicy
:直接抛出RejectedExecutionExeception异常来阻止系统正常运行 -
CallerRunsPolicy
:将任务回退到调用者 -
DisOldestPolicy
:丢掉等待最久的任务 -
DisCardPolicy
:直接丢弃任务
-
这四种拒绝策略均实现了RejectedExecutionHandler接⼝
7:线程优先级
地址越高,其优先级越高,也就是说用户操作行为的优先级是最高的。但注意:
- 1.
优先级越高,执行速度不一定越快
,跟资源大小
(任务复杂度)和CPU的调度
(多任务) - 2.
多任务就会出现资源抢夺问题
(会导致数据出错),此时需要锁来防止这种情况出现
8:锁
1:锁作用
- 1.
保证锁内的代码,同⼀时间,只有⼀条线程能够执⾏
! - 2.
锁的锁定范围,应该尽量⼩,锁定范围越⼤,效率越差
!
2:锁使用的注意点
- 1.能够加锁的
任意 NSObject 对象
- 2.注意:锁对象⼀定要保证
所有的线程都能够访问
- 3.如果代码中只有⼀个地⽅需要加锁,⼤多都使⽤ self,这样可以避免单独再创建⼀个锁对象
3:互斥锁和自旋锁
3.1:自旋锁
3.1.1:自旋锁成员
自旋锁包含:atomic, OSSpinLock, dispatch_semaphore_t
等
3.1.2:定义:是一种用于保护多线程共享资源的锁
,与一般互斥锁(mutex)不同之处在于当自旋锁尝试获取锁时以忙等待(busy waiting)的形式不断地循环检查锁是否可用
。当上一个线程的任务没有执行完毕的时候(被锁住
),那么下一个线程会一直等待(不会睡眠)
,当上一个线程的任务执行完毕,下一个线程会立即执行
。
自旋锁会忙等: 所谓忙等,即在访问被锁资源时,调用者线程不会休眠
,而是不停循环在那里
,直到被锁资源释放锁。
3.1.3:使用场景
- 1.
对持有锁较短的程序来说,使用自旋锁代替一般的互斥锁往往能够提高程序的性能
。 - 2.
加锁代码(临界区)经常被调用,但竞争情况很少发生
- 3.
CPU资源不紧张
- 4.
多核处理器
3.2:互斥锁
3.2.1:互斥锁成员
互斥锁包含:@synchronized,NSLock, pthread_mutex, NSConditionLock, NSCondition, NSRecursiveLock
等
3.2.2:定义
定义:互斥锁当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程会进入睡眠状态等待任务执行完毕
,当上一个线程的任务执行完毕,下一个线程会自动唤醒然后执行任务。
互斥锁会休眠: 所谓休眠,即在访问被锁资源时,调用者线程会休眠
,此时cpu可以调度其他线程工作
。直到被锁资源释放锁。此时会唤醒休眠线程。
3.2.3:使用场景
- 1.
预计线程等待锁的时间较长
- 2.
单核处理器
- 3.
临界区有IO操作
- 4.
临界区代码复杂或者循环量大
- 5.
临界区竞争非常激烈
3.3:原子锁
atomic: 原子属性,本身有一把自旋锁,可以保证单个线程写入,多个线程同时读写。对于属性指针来讲是线程安全的。
- 1.读取值线程安全
- 2..atomic:
读写安全,需要消耗⼤量的资源
(至少是nonatomic的10倍)
说明:使用atomic修饰属性时,编译器在生成getter和setter方法时,在getter和setter方法内部实现进行加锁操作
,这么做的目的是为了保证属性读写的安全性和完整性
,也就是说对于属性值得存取是线程安全
的。但这个不能保证操作这个属性的时候是线程安全的
。
注意:由于被锁住的是属性的指针,对于指针指向的地址内的数据是没做线程安全处理的。所以当属性的数据类型是可变的(数组、字符串、字典)时,指针指向的地址不变,而只是改变地址内的数据时,这个并不是线程安全的。
nonatomic: 非原子属性。不是线程安全的。
- 1.nonatomic:
⾮线程安全
,适合内存⼩
的移动设备
iOS 开发的建议
1.所有属性都声明为 nonatomic
2.尽量避免多线程抢夺同⼀块资源
,尽量将加锁
、资源抢夺的业务逻辑交给服务器端处理,减⼩移动客户端的压⼒
9:线程和Runloop的关系
- 1.
runloop与线程是⼀⼀对应的
,⼀个runloop对应⼀个核⼼的线程
,为什么说是核⼼的,是因为runloop是可以嵌套的
,但是核⼼的只能有⼀个,他们的关系保存在⼀个全局的字典⾥
。 - 2.
runloop是来管理线程的
,当线程的runloop被开启后,线程会在执⾏完任务后进⼊休眠状态
,有了任务就会被唤醒去执⾏任务
。 - 3.
runloop在第⼀次获取时被创建,在线程结束时被销毁
。 - 4.对于
主线程
来说,runloop在程序⼀启动就默认创建好了
。 - 5.对于
⼦线程来
说,runloop是懒加载的
,只有当我们使⽤的时候才会创建
,所以在⼦线程⽤定时器
要注意:确保⼦线程的runloop被创建
,不然定时器不会回调。、
10:线程间通讯
在Threading Programming Guide文档中,提及,线程间的通讯有以下几种方式
-
直接消息传递
: 通过performSelector
的一系列方法,可以实现由某一线程指定在另外的线程上执行任务。因为任务的执行上下文是目标线程,这种方式发送的消息将会自动的被序列化 -
全局变量、共享内存块和对象
: 在两个线程之间传递信息的另一种简单方法是使用全局变量,共享对象或共享内存块。尽管共享变量既快速又简单,但是它们比直接消息传递更脆弱。必须使用锁或其他同步机制仔细保护共享变量,以确保代码的正确性
。 否则可能会导致竞争状况,数据损坏或崩溃。 -
条件执行
: 条件是一种同步工具
,可用于控制线程何时执行代码的特定部分。您可以将条件视为关守,让线程仅在满足指定条件时运行。 -
Runloop sources
: 一个自定义的 Runloop source 配置可以让一个线程上收到特定的应用程序消息。由于Runloop source 是事件驱动
的,因此在无事可做时,线程会自动进入睡眠状态
,从而提高了线程的效率 -
Ports and sockets
:基于端口的通信
是在两个线程之间进行通信的一种更为复杂的方法,但它也是一种非常可靠的技术
。更重要的是,端口和套接字可用于与外部实体(例如其他进程和服务)进行通信。为了提高效率,使用 Runloop source 来实现端口,因此当端口上没有数据等待时,线程将进入睡眠状态。需要注意
的是,端口通讯需要将端口加入到主线程的Runloop中
,否则不会走到端口回调方法 -
消息队列
: 传统的多处理服务定义了先进先出(FIFO)队列抽象,用于管理传入和传出数据。尽管消息队列既简单又方便,但是它们不如其他一些通信技术高效 -
Cocoa 分布式对象
: 分布式对象是一种 Cocoa 技术,可提供基于端口的通信的高级实现。尽管可以将这种技术用于线程间通信,但是强烈建议不要这样做,因为它会产生大量开销。分布式对象更适合与其他进程进行通信,尽管在这些进程之间进行事务的开销也很高
11:端口通讯:NSPort
下面我们举例子来展示多线程的端口通讯,通过KCPerson对象跟ViewController进行发送消息的例子
// ViewController.m //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]); 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); } // Person.m - (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 = [@"AAAA" dataUsingEncoding:NSUTF8StringEncoding]; NSData *data2 = [@"BBBB" 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]; } - (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"]); }
运行打印:
注意:
- 1.NSPort对象
必须添加到要接受的线程的runLoop中
- 2.
接收消息的对象实现NSPortDelegate协议的-handlePortMessage:方法来获取消息内容
注意