多线程 - 基础总结
前言
一、进程和线程的概念
二、多线程的原理
三、进程和线程的关系
四、多线程的优缺点
五、线程的生命周期 (重点)
六、线程的优先级
七、IOS的多线程方案
八、java的多线程方案
多线程在我们日常开发中经常用到,今天就具体的总结一下多线程的基础知识以及ios和android常用的多线程方案
一、进程和线程的概念
进程:
进程是指系统中正在运行的一个应用程序。
每一个进程是独立的,每个进程均运行在专用的且受保护的内存空间内。
线程:
线程是进程的基本执行单元,一个进程的所有任务都在线程中执行。
进程要想执行任务,必须得有线程,进程至少要有一条线程(比如主线程)。
程序启动的时候,会默认开启一条线程,这条线程被称为主线程或UI线程。
例如MAC中的活动监视器,可以看到每一个进程都有n条线程

在IOS的开发中,一般都是单进程,每一个应用程序就是一个进程。当我们开发扩展程序的时候,比如键盘、分享、VPN等,需要多进程。
而在android的开发中,会经常用到多进程,通过binder去做跨进程的通信。
二、多线程的原理
1、时间片:CPU在多个任务直接进行快速切换,这个时间间隔就是时间片。
对于单核CPU来说:
(1)同一时间,只能处理1个线程。也就是同一时间只有1个线程在执行。
(2)我们理解的多线程同时执行,其实是CPU快速的在多个线程之间进行切换。CPU调度线程的速度足够快,就造成了多线程的“同时”执行效果。
这种CPU快速切换的机制就是时间片轮转机制,也叫RR调度.
(3)如果线程数非常多,CPU会在N个线程之间切换,也就消耗了大量的CPU资源。每个线程被调度的次数会降低,线程的执行效率也就降低了。
而对于多核CPU来说:
内核按道理来讲,每一个内核对应一条线程,是一对一的关系。
但是因为有超线程技术,变成了1比2的关系,如下图:


虽然我的电脑内核数是8,但是能开启16个逻辑处理器。也就是我的电脑可以同时运行16条线程。而每个处理器和单核处理器一样,通过cpu时间片轮转机制,让我们可以“开启很多条线程”。
由于CPU执行一条指令的速度大概是0.6ns(纳秒),非常的快,所以"时间片轮转机制"并不会卡顿的。
2、线程的上下文切换
上下文切换是指:一个线程被暂停、剥夺使用权,另一个线程被选中开始或者继续运行的过程。
一次上下文切换大概耗时20000个cpu周期(CPU每取出一条指令并执行这条指令,都要完成一系列的操作,这一系列操作所需要的时间通常叫做一个指令周期。换言之指令周期是取出一条指令并执行这条指令的时间。)。
3、并行和并发的区别
并行是同一时间,有不同的线程同时在执行任务。比如多核CPU,同一时间有多个线程同时在执行任务。
并发是单位时间内,一条线程执行多个任务。并发是一定要以时间为单位的。比如时间片轮转机制实际上就是并发执行。
三、进程和线程的关系
(1)地址空间:
同一个进程内的线程共享本进程的地址空间,但是进程之间则是独立的地址空间。
(2)资源:
同一个进程内的线程共享进程的资源,如内存、I/O、cpu等,但是进程之间的资源是独立的。
(3)执行过程:
每个独立的进程都有一个程序运行的入口、顺序执行序列。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
(4)线程是cpu调度的基本单位,而进程不是。
(5)线程没有地址空间,线程是包含在进程的地址空间中。
(6)场景:
<1> 当一个进程崩溃后,在保护模式下不会对其他进程产生影响。但是一个线程崩溃则会影响整个进程都死掉。所以多进程要比多线程健壮。
<2> 进程切换时,消耗的资源大,效率低。所以涉及到频繁切换时,使用线程要好于进程。如果要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
四、多线程的优缺点
优点:
1、能适当的提高程序的执行效率。
2、能适当的提供资源(cpu、内存)的利用率。
3、线程上的任务执行完成后,线程会自动销毁。
比如以下OC的代码:
/**
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];
//I/O打印
NSLog(@"%@", myName);
}
NSLog(@"over");
}
缺点:
1、开启线程需要占用一定的内存空间(默认情况下,每一条线程都占512KB)。如果开启大量的线程,会占用大量的内存空间,降低程序的性能。
2、线程越多,CPU在调用线程上的开销就越大。
3、程序设计会变的复杂,比如线程间的通信以及数据共享。
五、线程的生命周期 (重点)
先来看一个面试题:
当创建一个线程对象时(比如OC的NSThread 或者 java的Thread),此时调用start方法,线程就会立马被调度执行吗?当调用cancel(ios)/interrupt(java)方法线程会立马结束吗?
答案是:当调用start方法,线程不会立马执行,需要等待cpu调度此线程时,才会去执行。
当调用cancel/interrupt方法,正在执行的线程不会被结束。
生命周期图:

如图所示:线程的生命周期有五个:新建、就绪、运行、阻塞、死亡。
注意:当线程因为sleep、等待同步锁或者被移出可调度线程池而阻塞后,在sleep到时,获取同步锁或者重新添加到可调度线程池之后,并不会立马运行,而是会再次进入就绪的状态,等待cpu调度。
线程池:
线程池的机制图:

六、线程的优先级
1、对于IOS来说,有以下2个属性可以设置优先级:
typedef NS_ENUM(NSInteger, NSQualityOfService) {
//与用户交互的任务,这些任务通常跟UI级别的刷新相关,比如动画,这些任务需要在一瞬间完成
NSQualityOfServiceUserInteractive = 0x21,
//由用户发起的并且需要立即得到结果的任务,比如滑动table view时去加载数据用于后续cell的显示,这些任务通常跟后续的用户交互相关,在几秒或者更短的时间内完成
NSQualityOfServiceUserInitiated = 0x19,
//一些可能需要花点时间的任务,这些任务不需要马上返回结果,比如下载的任务,这些任务可能花费几秒或者几分钟的时间
NSQualityOfServiceUtility = 0x11,
//这些任务对用户不可见,比如后台进行备份的操作,这些任务可能需要较长的时间,几分钟甚至几个小时
NSQualityOfServiceBackground = 0x09,
//优先级介于user-initiated 和 utility,当没有 QoS信息时默认使用,开发者不应该使用这个值来设置自己的任务
NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));
//0-1 double类型
@property double threadPriority API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0)); // To be deprecated; use qualityOfService below
@property NSQualityOfService qualityOfService API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0)); // read-only after the thread is started
threadPriority已经被qualityOfService代替了。
优先级越高,任务就会执行越快吗?
任务执行的速度取决于资源大小、复杂度、优先级以及cpu的调度。
如果单纯用优先级大小来判断,是不足以百分百确定快慢的。
2、优先级反转 (Priority Inversion)
关于优先级反转的问题,先来看IOS一道高频出现的面试题:苹果为什么要弃用自旋锁?
很多人都知道弃用自旋锁是因为线程优先级反转的问题。那么是自旋锁造成优先级反转吗?当然不是。
先来看什么是优先级反转:

假设有3个线程:线程A(优先20),线程B (优先级15),线程C(优先级10)。
(1)在时间点T1时,线程C加互斥锁锁并执行任务。
(2)在时间点T2时,高优先级的线程A被唤醒,它也要操作互斥数据。当它加锁时,因为互斥锁在T1被线程C锁掉了,所以线程A放弃CPU进入阻塞状态。
(3)在时间点T3时,线程B被唤醒,因为它比当前执行的线程C优先级高,所以它会立即抢占CPU。而线程C被迫进入READY状态等待。
(4)直到时间点T4,线程B放弃CPU,这时线程C再次占据CPU继续执行,最后在T5的时间点完成解锁。此时线程A获取互斥锁开始执行。线程C再次进入READY等待状态。
在上面这个时序里,因为程序逻辑,线程A确实应该等到线程C完成之后,才能获取CPU,这里没有问题。但是线程B从T3到T4占据CPU运行的行为,就是优先级反转。
3、系统是如何解决优先级反转问题的

系统会通过“优先级继承”来解决优先级反转问题,当在T2时间点时,系统会拿到持有锁的线程,把低优先级的线程优先级提高到高优先级线程的优先级。当T3时间点时,就不会被线程B所抢占了。
当然系统也可能通过“优先级天花板”的方式,把该线程的优先级提升到可访问这个资源的所有线程中最高的优先级。
4、为什么自旋锁会造成优先级反转问题无法解决?
(1)首先优先级反转问题的出现跟自旋锁没有关系,但是当在使用OSSpinLock时,这种锁不会记录持有它的线程信息,所以当发生优先级反转时,系统找不到对应的线程,也就无法通过提高优先级来解决优先级反转的问题。
(2)高优先级线程使用自旋锁进行轮训等待锁时在一直占用CPU时间片,使得低优先级的线程拿到时间片的概率降低,从而影响效率。
另外,使用信号量(dispatch_semaphore)的时候,也会造成无法解决优先级反转的问题,它虽然不会忙等,但是它的api也没有记录持有信号量的线程信息。
高效的线程同步有两个关键点:
1、不忙等
2、记录持有者
七、IOS的多线程方案
IOS线程的成本

如图所示:
(1)iOS的主线程栈大小不超过1MB,OS X主线程栈最大8MB,子线程栈最大512KB。
(2)子线程在创建的时候可以更改栈的大小,子线程允许设置的最小栈大小为16 KB,并且栈大小必须为4 KB的倍数。注意:主线程的栈大小无法修改。
(3)创建线程时间大约是90微秒。
通过打印([NSThread currentThread].stackSize / 1024),ios12上面主线程是512k
1、pthread (不经常使用)
- (void)createPthread {
/**
pthread_create 创建线程
参数:
1. pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀 `_t / Ref` 结尾
同时不需要 `*`
1. 线程的属性(pthread_attr_t),nil(空对象 - OC 使用的) / NULL(空地址,0 C 使用的)
2. 线程要执行的`函数地址`
void *: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*): 函数名
(void *): 参数类型,void *
1. 传递给第三个参数(函数)的`参数`
返回值:C 语言框架中非常常见
int
0 创建线程成功!成功只有一种可能
非 0 创建线程失败的错误码,失败有多种可能!
*/
pthread_t threadId = NULL;
pthread_create(&threadId, NULL, test, "123");
}
void *test(void *name) {
NSString *ocName = [NSString stringWithUTF8String:name];
NSLog(@"%@ %@",[NSThread currentThread],ocName);
return NULL;
}
特点:(1)语言C(2)跨平台(3)难度大(4)线程的生命周期需要程序员管理。
2、NSThread (偶尔使用)
- (void)createNSThread {
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(nsTest) object:nil];
//线程的名称
thread.name = @"测试线程";
//线程所占的占大小
thread.stackSize = 1024*1024;
//线程的优先级
thread.threadPriority = 1;
[thread start];
}
- (void)nsTest {
}
特点:(1)语言OC(2)线程的生命周期需要程序员管理。
3、GCD 经常使用,后面会具体分析。
4、NSOperation 经常使用,基于GCD,比GCD多了一些更简单实用的功能,使用起来也更加面向对象。
八、java的多线程方案
There are two ways to create a new thread of execution
通过Thread的源码,可以看到,java开启多线程只有两种方式:
1、Thread
private static class MyThread extends Thread {
@Override
public void run() {
super.run();
// do my work;
System.out.println("I am extendec Thread");
}
}
public static void main(String[] args)
throws InterruptedException, ExecutionException {
MyThread myThread = new MyThread();
myThread.start();
}
2、Runnable
private static class MyRunnable implements Runnable {
@Override
public void run() {
// do my work;
System.out.println("I am implements Runnable");
}
}
public static void main(String[] args) throws InterruptedException, ExecutionException {
MyThread myThread = new MyThread();
myThread.start();
}
MyRunnable myRunnable = new MyRunnable();
new Thread(myRunnable).start();
3、Thread 和 Runnable的区别
Thread才是java中对线程的抽象。而Runnable是对任务的抽象。
4、stop方法不建议使用
stop在终结一个线程的时候,会导致线程持有的资源不会正常的释放。
比如一个线程正在下载一个10k的文件,当下载到5k时,突然执行了stop方法,此时文件被写入了一半,文件并没有正常结束,就会是一个缺失的文件。