线程和进程
线程
线程是进程的基本执行单元,一个进程的所有任都在线程中执行- 进程要想执行任务,必须的有线程,
进程至少要有一条线程 程序启动会默认开启一条线程,这条线程被称为主线程或者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被创建,不然定时器不会回调。