线程和进程
线程
线程
是进程的基本执行单元
,一个进程的所有任都在线程中执行
- 进程要想执行任务,必须的有线程,
进程至少要有一条线程
程序启动会默认开启一条线程
,这条线程被称为主线程
或者UI线程
进程
进程
是指在系统中正在运行的一个应用程序
- 每个
进程之间是独立
的,每个进程均运行在其专用的且受保护的内存空间内 - 通过“活动监视器”可以查看mac系统中所开启的线程
进程是线程的容器,而线程用来执行任务。在iOS中是单进程开发,一个进程就是一个app
,进程
之间是相互独立
的,如支付宝、微信、qq等,这些都是属于不同的进程
进程与线程的关系
- 线程是依附于进程的,不能独立存在,它包含在进程之中,是进程中的实际运作单位。进程一旦结束,所有线程都结束。
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
进程与线程之间的关系主要涉及两个方面
线程是进程中的一个执行单元,由CPU
独立调度执行,负责当前进程中任务的执行。一个进程可以有一个或多个线程,同一进程中的多个线程将共享程序的内存空间
,也就是该进程中的代码段
(代码和常量),数据段
(全局变量和静态变量),扩展段
(堆存储)等系统资源。
进程和线程的区别
-
根本区别
进程
是操作系统分配资源
的最小单位,线程
是程序执行
的最小单位 -
地址空间
- 同一个进程的
线程共享本进程的地址空间
- 而
进程之间则是独立的地址空间
- 同一个进程的
-
资源拥有
- 同一个进程内
线程共享本进程的资源
,如内存、I/O、cpu等 - 但是
进程之间资源是独立的
- 同一个进程内
两个之间的关系就相当于工厂与流水线的关系
,工厂与工厂之间是相互独立的,而工厂中的流水线是共享工厂的资源的,即 进程
相当于一个工厂
,线程
相当于工厂中的一条流水线
-
多进程要比多线程健壮
一个进程崩溃后,在保护模式下不会对其他进程产生影响
- 而
一个线程崩溃整个进程都死掉
-
调度和切换
进程切换时,消耗的资源大,效率高
。所以涉及到频繁的切换
时,使用线程要好于进程
。- 同样如果
要求
同时进行并且又要共享某些变量的并发操作
,只能用线程
不能用进程
-
执行过程
- 每个独立的
进程
有一个程序运行的入口
、顺序执行序列
和程序入口
- 但是
线程不能独立执行,必须依存在应用程序中
,由应用程序提供多个线程执行控制。
- 每个独立的
线程和Runloop的关系
runloop与线程是一一对应的
,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个
,他们的关系保存在一个全局 的字典里。runloop是来管理线程的
,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务就会被唤醒去执行任务。- runloop在第一次获取时被创建,在线程结束时被销毁。
- 对于
主线程
来说,runloop在程序一启动就默认创建
好了。 - 对于
子线程
来说,runloop是懒加载的
,只有当我们使用的时候才会创建,所以在子线程用定时器
要注意确保子线程的runloop被创建,不然定时器不会回调
。
多进程 vs 多线程
多任务既可以由多进程
实现,也可以由单进程内的多线程
实现,还可以混合多进程+多线程
。混合多进程和多线程的程序涉及到同步、数据共享的问题,这种模型更复杂,实际很少采用。
和多进程相比,多线程
的优势在于:
- 线程的
调度与切换
比进程快
很多,同时创建一个线程的开销也比进程要小很多; - 线程之间的
通信更方便
,同一进程下的线程共享全局变量、静态变量等数据,线程间通信就是读写同一个变量,速度很快。而进程之间的通信需要以通信的方式(Inter Process Communication,IPC)进行。
而多进程
的优点在于:
- 多进程程序
更健壮
,在多进程的情况下,一个进程崩溃不会影响其他进程,而在多线程的情况下,任何一个线程崩溃会直接导致整个进程崩溃。
多线程
单核CPU是怎么执行多任务的呢?
时间片轮转调度
:简单地说就是把一个处理器划分为若干个短的时间片
,每个进程
会被操作系统分配一个时间片(即每次被 CPU 选中来执行当前进程所用的时间),每个时间片依次轮流
地执行处理各个应用程序,时间一到,无论进程是否运行结束,操作系统都会强制将 CPU 这个资源转到另一个进程去执行,由于一个时间片很短,从而达到多个应用程序在同时进行的效果。
多线程原理
- 对于
单核CPU
,同一时间,CPU只能处理一条线程
,即只有一条线程在工作, - iOS中的
多线程同时执行
的本质是CPU在多个任务之间进行快速的切换
,由于CPU调度线程
的时间足够快
,就造成了多线程的“同时”执行
的效果。其中切换的时间间隔就是时间片
多线程意义
-
能适当
提高程序的执行效率
-
能适当
提高资源的利用率
,如CPU、内存 -
线程上的任务执行完成后,
线程会自动销毁
-
开启线程需要占用一定的内存空间
,默认情况下,每一个线程占用512KB
-
如果开启
大量线程,会占用大量的内存空间,降低程序的性能
-
线程越多
,CPU在调用线程上的开销就越大
-
程序设计更加复杂
,比如线程间的通信,多线程的数据共享
多线程生命周期
多线程的生命周期主要分为5部分:新建 - 就绪 - 运行 - 阻塞 - 死亡,如下图所示
多线程声明周期
-
新建
(new) 主要是实例化线程Thread对象,创建完成后就需要为线程分配内存。当线程处于"新线程"状态时,仅仅是一个空线程对象,它还没有分配到系统资源。因此只能启动start()或终止stop()它。任何其他操作都会引发异常。 -
就绪(Runnable)
线程对象调用start()方法
,将线程对象加入可调度线程池
,等待CPU的调用
,即调用start方法,并不会立即执行
,进入就绪状态
,需要等待一段时间,经CPU调度后才执行run()
,也就是从就绪状态进入运行状态
-
运行(Running)
就绪状态
的线程并不一定立即运行run()
方法,线程还必须同其他线程竞争CPU时间
,只有获得CPU时间
才可以运行线程 -
阻塞(Blocked)
当满足某个预定条件时,可以
使用休眠,即sleep
,suspend()
,IO阻塞
,wait()等待通知
,等待同步锁
,阻塞线程执行。下面关于休眠的时间设置,都是
NSThread
的sleepUntilDate:
阻塞当前线程,直到指定的时间为止,即休眠到指定时间
sleepForTimeInterval:
在给定的时间间隔内休眠线程,即指定休眠时长
- 同步锁:
@synchronized(self):
当stop()时间到
,resume()
,IO阻塞返回
,收到通知
,获得同步锁
等,线程从阻塞状态进入就绪状态,等待CPU调度。
-
死亡(Dead)
正常死亡
,即线程执行完毕非正常死亡
,即当满足某个条件后,在线程内部(或者主线程中)终止执行(调用exit,stop方法等退出),或者发生了错误Error或异常Exception
总结
简要说明,就是处于运行中的线程
拥有一段可以执行的时间(称为时间片
),
- 如果
时间片用尽
,线程就会进入就绪状态队列
- 如果
时间片没有用尽
,且需要开始等待某事件
,就会进入阻塞状态队列
- 等待事件发生后,线程又会重新进入
就绪状态队列
- 每当一个
线程离开运行
,即执行完毕或者强制退出后,会重新从就绪状态队列
中选择一个线程继续执行
线程的
exit
和cancel
说明
exit
:一旦强行终止线程,后续的所有代码都不会执行cancel
:取消当前线程,但是不能取消正在执行的线程
线程的优先级越高,是不是任务的执行越快?
并不是,线程执行的快慢,除了要看优先级,还需要查看资源的大小
(即任务的复杂度
)、以及 CPU 调度
情况。在NSThread中,线程优先级threadPriority
已经被服务质量qualityOfService
取代,以下服务质量枚举
线程池原理
线程池原理
-
【第一步】判断核心线程池是否都正在执行任务
- 返回NO,创建新的工作线程去执行
- 返回YES,进入【第二步】
-
【第二步】判断线程池工作队列是否已经饱满
- 返回NO,将任务存储到工作队列,等待CPU调度
- 返回YES,进入【第三步】
-
【第三步】判断线程池中的线程是否都处于执行状态
- 返回NO,安排可调度线程池中空闲的线程去执行任务
- 返回YES,进入【第四步】
-
【第四步】交给饱和策略去执行,主要有以下四种(在iOS中并没有找到以下4种策略)
AbortPolicy
:直接抛出RejectedExecutionExeception异常来阻止系统正常运行CallerRunsPolicy
:将任务回退到调用者DisOldestPolicy
:丢掉等待最久的任务DisCardPolicy
:直接丢弃任务
1.如果当前运行的线程少于corePoolSize,则创建新线程来执行任务
2.如果运行的线程等于或多于corePoolSize,则将任务加入BlockingQueue。 线程池会让corePoolSize里执行完任务的线程,反复的获取BlockingQueue的任务执行。
3.如果无法将任务加入BlockingQueue(队列已满),则创建新的线程来处理任务
4.如果创建新线程将使当前运行的线程超出maximumPoolSize,就启用饱和策略。
iOS中多线程的实现方案
iOS中的多线程实现方式,主要有四种:pthread、NSThread、GCD、NSOperation
,汇总如图所示
//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(@"失败");
}
//NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
//GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
//NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];
- (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
。
线程和Runloop的关系
runloop与线程是一一对应的
,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个
,他们的关系保存在一个全局的字典里。runloop是来管理线程的
,当线程的runloop被开启后,线程会在执行完任务后进入休 眠状态,有了任务就会被唤醒去执行任务。- runloop在第一次获取时被创建,在线程结束时被销毁。
- 对于
主线程
来说,runloop在程序一启动就默认创建
好了。 - 对于
子线程
来说,runloop是懒加载的,只有当我们使用的时候才会创建
,所以在子线程用定时器
要注意:确保子线程的runloop被创建,不然定时器不会回调
。