线程和进程
线程
- 线程是进程的
基本执行单元
,一个进程的所有任务都在线程中
执行 - 进程要想执行任务,必须得有线程,
进程至少要有一条线程
- 程序启动会默认开启一条线程,这条线程被称为
主线程
或者UI线程
进程
-
进程
是指在系统中正在运行的一个应用程序 - 每个进程之间是独立的,每个进程均运行在其
专用的且受保护
的内存空间内 - 通过
“活动监视器”
可以查看mac系统中所开启的线程 -
MAC
是多进程的,iOS
是单进程的。
进程中包含多个线程,进程负责任务的调度
,线程负责任务的执行
。在iOS中并不支持多进程
,所有程序都是单一进程运行,进程之间相互独立
。
线程与进程的关系
-
地址空间
:同一进程的线程共享本进程的地址空间,而进程与进程之间是独立的地址空间; -
资源拥有
:同一进程的线程共享本进程的资源,如:内存、IO、CPU
等,而进程与进程之间的资源是独立的。
两者的使用特点:
- 一个进程崩溃后,在
保护模式下
不会对其他进程产生影响,但一个线程崩溃会导致整个进程都死掉。所以多进程要比多线程健壮
; - 进程切换时
消耗的资源大
,效率高
。所以涉及到频繁的切换时,使用线程要好于进程。如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程; - 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是
线程不能独立执行
,必须依存在应用程序中,由应用程序提供多个线程执行控制; - 线程是处理器调度的
基本单位
,但是进程不是; - 线程没有地址空间,线程包含在进程地址空间中;
线程局部存储TLS
线程局部存储
全称:Thread Local Storage:线程是没有地址空间的,但是存在线程局部存储
。线程局部存储是某些操作系统
为线程单独提供的私有空间,但通常只具有有限的容量
。
类的加载篇章
中分析在objc
源码中,_objc_init
方法中包含了对tls
的初始化操作,源码如下
void _objc_init(void)
{
static bool initialized = false;
if (initialized) return;
initialized = true;
// fixme defer initialization until an objc-using image is found?
environ_init();
// 线程绑定,例如线程数据的析构函数
tls_init();
static_init();
runtime_init();
exception_init();
#if __OBJC2__
cache_t::init();
#endif
_imp_implementationWithBlock_init();
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
#if __OBJC2__
didCallDyldNotifyRegister = true;
#endif
}
//tls_init源码实现
void tls_init(void)
{
#if SUPPORT_DIRECT_THREAD_KEYS
pthread_key_init_np(TLS_DIRECT_KEY, &_objc_pthread_destroyspecific);
#else
_objc_pthread_key = tls_create(&_objc_pthread_destroyspecific);
#endif
}
多线程原理
iOS中多线程同时执行的本质是CPU在多个任务之间快速的切换
,由于CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果。其中切换的时间间隔就是时间片
。所以多线程并不是真正的并发
,而真正的并发
必须建立在多核CPU
的基础上。
多线程意义
- 优点
- 能适当提高程序的
执行效率
- 能适当提高资源的
利用率
,如CPU、内存
- 线程上的任务执行完成后,线程会
自动销毁
- 缺点
- 开启线程需要
占用一定的内存空间
,默认情况下,每一个线程占用512KB
- 如果开启大量线程,会占用大量的
内存空间
,降低程序的性能 - 线程越多,
CPU在调用线程上的开销就越大
- 程序设计更加复杂,比如
线程间的通信
,多线程的数据共享
时间片
时间片
的概念:CPU在多个任务之间进行快速的切换
,这个时间间隔
就是时间片
-
单核CPU
同一时间,CPU只能处理一个线程
换言之,同一时间只有一个线程在执行
- 多线程同时执行:
是CPU快速的在多个线程之间的切换
CPU调度线程的时间足够快,就造成了多线程的“同时”执行的效果 - 如果线程数非常多
CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源
每个线程被调度的次数会降低,线程的执行效率降低
-
线程创建成本
多线程技术方案
以上四种方案的简单示例
// *********1: pthread*********
pthread_t threadId = NULL;
//c字符串
char *cString = "HelloCode";
/**
pthread_create 创建线程
参数:
1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
同时不需要 `*`
2. 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
3. 线程要执行的`函数地址`
void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*): 函数名
(void *): 参数类型,void *
4. 传递给第三个参数(函数)的`参数`
*/
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];
}];
/**
1. 循环的执行速度很快
2. 栈区/常量区的内存操作也挺快
3. 堆区的内存操作有点慢
4. I(Input输入) / O(Output 输出) 操作的速度是最慢的!
* 会严重的造成界面的卡顿,影响用户体验!
* 多线程:开启一条线程,将耗时的操作放在新的线程中执行
*/
- (void)threadTest{
NSLog(@"begin");
NSInteger count = 1000 * 100;
for (NSInteger i = 0; i < count; i++) {
// 栈区
NSInteger num = i;
// 常量区
NSString *name = @"zhang";
// 堆区
NSString *myName = [NSString stringWithFormat:@"%@ - %zd", name, num];
NSLog(@"%@", myName);
}
NSLog(@"over");
}
void *pthreadTest(void *para){
// 接 C 语言的字符串
// NSLog(@"===> %@ %s", [NSThread currentThread], para);
// __bridge 将 C 语言的类型桥接到 OC 的类型
NSString *name = (__bridge NSString *)(para);
NSLog(@"===>%@ %@", [NSThread currentThread], name);
return NULL;
}
C与OC的桥接
-
__bridge
只做类型转换,但是不修改对象(内存)管理权; -
__bridge_retained
(也可以使用CFBridgingRetain
)将Objective-C的对象转换为 Core Foundation的对象,同时将对象(内存)的管理权交给我们,后续需要使用 CFRelease或者相关方法来释放对象; -
__bridge_transfer
(也可以使用CFBridgingRelease
)将Core Foundation
的对象 转换为Objective-C的对象,同时将对象(内存)的管理权交给ARC。
线程生命周期
线程的生命周期
主要分为五部分:新建 - 就绪 - 运行 - 阻塞 - 死亡
-
新建
:主要是实例化线程对象 -
就绪
:线程对象调用start
方法,将线程对象加入可调度线程池
,等待CPU的调用,即调用start方法并不会立即执行,进入就绪状态
,需要等待一段时间经CPU调度后
才执行,也就是从就绪状态进入运行状态
-
运行
:CPU负责调度可调度线程池中线程
的执行,在线程执行完成之前,其状态可能会在就绪
和运行
之间来回切换,这个变化是由CPU负责,开发人员不能干预。 -
阻塞
:当满足某个预定条件时,可以使用休眠
即sleep,或者同步锁
阻塞线程执行。当进入sleep时,会重新将线程加入就绪
中。下面关于休眠的时间设置,都是NSThread
的API
sleepUntilDate
: 阻塞当前线程,直到指定的时间为止,即休眠到指定时间
sleepForTimeInterval
: 在给定的时间间隔内休眠线程,即指定休眠时长
同步锁:@synchronized(self)
-
死亡
:分为两种情况
正常死亡
,即线程执行完毕
非正常死亡
,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit方法等退出)
线程池原理
- 判断
核心线程池
是否都正在执行任务:
如果返回NO
,创建新的工作线程去执行;
如果返回YES
,进入下一步; - 判断
线程池工作队列是否饱满
:
如果返回NO
,将任务存储到工作队列,等待CPU调度
如果返回YES
,进入下一步; - 判断线程池中的线程
是否都处于执行状态
:
如果返回NO
,安排可调度线程池中空闲的线程去执行任务
如果返回YES
,进入下一步; - 交给
饱和策略
去执行,分为以下四种拒绝策略
:
AbortPolicy
:抛出RejectedExecutionExeception异常,阻止系统正常运行
CallerRunsPolicy
:将任务回退到调用者
DisOldestPolicy
:丢掉等待最久的任务
DisCardPolicy
:直接丢弃任务
这四种拒绝策略均实现的RejectedExecutionHandler
接口。
多线程面试题
任务执行速度的影响因素有哪些?
这个问题从四个角度分析:CPU的调度情况
、任务的复杂度
、任务的优先级
、线程状态
。
目前iOS中,线程优先级的threadPriority
属性已经弃用,被NSQualityOfService
类型的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));
开发者自己指定
:NSQualityOfService
服务质量,用于表示工作的性质和对系统的重要性。当存在资源竞争
时,使用高质量的服务类比使用低质量的服务类获得更多的资源
-
NSQualityOfServiceUserInteractive
:用于直接涉及提供交互式UI的工作。例如:处理控制事件或在屏幕上绘图; -
NSQualityOfServiceUserInitiated
:用于执行用户明确要求的工作,并且为了允许进一步的用户交互,必须立即显示这些工作的结果。例如:在用户邮件列表中选择邮件后加载邮件; -
NSQualityOfServiceUtility
:用于执行用户不太可能立即等待结果的工作。这项工作可能是由用户请求的,也可能是自动启动的,并且通常使用非模式进度指示器在用户可见的时间尺度上操作。例如:定期内容更新或批量文件操作,如媒体导入; -
NSQualityOfServiceBackground
:用于非用户发起或不可见的工作。通常,用户甚至不知道正在进行这项工作。例如:预抓取内容、搜索索引、备份或与外部系统同步数据; -
NSQualityOfServiceDefault
:表示没有明确的服务质量信息。只要可能,适当的服务质量是根据可用的资源确定的。否则,使用NSQualityOfServiceUserInteractive和NSQualityOfServiceUtility之间的服务质量级别。
优先级反转
- 线程分为以下两种:
IO密集型
,频繁等待的线程;
CPU密集型
,很少等待的线程; - IO密集型比CPU密集型更容易得到
线程优先级的提升
。
I(Input输入) / O(Output输出)
操作的速度是最慢的,并且等待频繁,如果它的优先级又低,很容易被饱和策略所淘汰;
为了避免这种情况,当CPU发现一个频繁等待的线程,会将其优先级提升
,从而提升线程被执行的可能性。
优先级的影响因素
- 用户指定线程的服务质量;
- 根据线程等待的频繁程度提高或降低;
- 长时间不执行的线程,提升它的优先级。
自旋锁和互斥锁
当多个线程同时访问同一块资源时,很容易引发资源抢夺
,造成数据错乱
和数据安全
问题,有以下两种解决方案:
-
互斥锁
(同步锁):@synchronized
自旋锁
例如多窗口卖票时,会产生资源的抢夺(如下图)。这时我们的常规操作就是加锁
。
互斥锁
- 用于
保护临界区
,确保同一时间,只有一条线程能够执行; - 如果代码中只有一个地方需要加锁,大多都使用
self
,这样可以避免单独再创建一个锁对象
; - 加了互斥锁的代码,当新线程访问时,如果发现
其他线程正在执行锁定的代码
,新线程就会进入休眠。
使用互斥锁的注意事项:
- 互斥锁的锁定范围,应该尽量小,
锁定范围越大,效率越差
; - 能够加锁的任意
NSObject
对象; - 锁对象一定要保证所有的线程都能够访问。
自旋锁
- 自旋锁与互斥锁类似,但它不是通过休眠使线程阻塞,而是在获取锁之前一直处于
忙等
(即原地打转,称为自旋)阻塞状态; - 使用场景:锁持有的时间短,且线程不希望在重新调度上花太多成本时,就需要使用
自旋锁
,属性修饰符atomic
,本身就有一把自旋锁; - 加入了自旋锁,当新线程访问代码时,如果发现有其他线程正在锁定代码,新线程会用死循环的方法,一直等待锁定的代码执行完成,即
不停的尝试执行代码,比较消耗性能
。
自旋锁与互斥锁
- 相同点:
同一时间保证只有一条线程
执行任务,即保证了相应同步
的功能。 - 不同点:
互斥锁
:发现其他线程执行,当前线程休眠
(即就绪状态),进入等待执行,即挂起
。一直等其他线程打开之后,然后唤醒执行;
自旋锁
:发现其他线程执行,当前线程一直询问
(即一直访问),处于忙等状态,耗费的性能比较高
。
使用场景
- 根据任务复杂度区分,使用不同的锁,但判断不全时,更多是使用互斥锁去处理;
- 当前的任务状态比较短小精悍时,用自旋锁;
- 反之的,用互斥锁。
atomic原子锁与nonatomic非原子锁的作用
atomic
和nonatomic
主要用于属性的修饰
。
-
atomic
是原子属性,是为多线程开发准备的,是默认属性!
- 仅仅在属性的
setter
方法中,增加了锁(自旋锁),能够保证同一时间,只有一条线程对属性进行写操作 - 同一时间 单(线程)写多(线程)读的线程处理技术
-
Mac
开发中常用
-
nonatomic
是非原子属性
没有锁!性能高!
移动端
开发常用
iOS开发的建议
- 所有属性都声明为
nonatomic
- 尽量避免多线程抢夺同一块资源,尽量将
加锁、资源抢夺
的业务逻辑交给服务器端处理,减小移动客户端的压力
atomic与nonatomic的区别
- nonatomic
- 非原子属性
- 非线程安全,适合
内存小
的移动设备
- atomic
- 原子属性(线程安全),针对
多线程
设计的,默认值 - 保证同一时间只有一个线程能够写入(但是同一个时间多个线程都可以取值)
- atomic 本身就有一把锁(自旋锁)
单写多读
:单个线程写入,多个线程可以读取 - 线程安全,需要消耗大量的资源
objc4-818.2
源码分析
- 全局搜索
objc_setProperty
的方法实现,源码如下
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
{
bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);
bool mutableCopy = (shouldCopy == MUTABLE_COPY);
reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy);
}
- 进入
reallySetProperty
方法查看源码
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
// atomic修饰,增加了spinlock_t的锁操作;
// 所以atomic是标示,自身并不是锁。而atomic所谓的自旋锁,由底层代码实现。
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
线程与Runloop的关系
-
runloop
与线程
是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的。但是核心的只能有一个,他们的关系保存在一个全局的字典里
。 -
runloop
是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。 -
runloop
在第一次获取时被创建,在线程结束时被销毁。 - 对于主线程来说,
runloop
在程序一启动就默认创建好了。 - 对于子线程来说,
runloop
是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器
要注意:确保子线程的runloop被创建,不然定时器不会回调。
线程间通讯
Threading Programming Guide文档,线程间的通讯有以下几种方式