为进一步研究 iOS 的多线程做准备,就首先需要对多线程的理论进行了解,日程开发中,为了解决一些问题基本都使用过 GCD
、NSOperation
、NSThread
、pthread
,那到底什么是多线程,多线程都包含哪些东西,使用多线程一定就好嘛?
本篇是对理论知识的整理。
1、什么是线程
线程 (英语:thread)
是操作系统能够进行运算调度的最小单位,是进程的基本执行单元。一个进程的所有任务都在线程中执行,进程想要执行任务,就必须有线程,一个进程至少要有一条线程。对于 iOS 程序启动会默认开启一条线程,这条线程被称为主线程。
2、什么是进程
-
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。
-
狭义定义:进程是指系统中正在运行的一个应用程序。
每一个进程都有它自己的地址空间,一般情况下,包括文本区域 (text region)
、数据区域 (data region)
和堆栈 (stack region)
。
对于 iOS 来说是单进程,苹果设计的沙盒机制,会阻断 iOS
系统中 App
之间相互干扰的情况,使得 App 更加安全。
打开资源管理器,每行就是一个进程,进程都有自己的名字,pid
等。
3、线程和进程的关系和区别
了解了线程和进程的定义,线程和进程的关系和区别主要分为以下几点:
-
地址空间:进程间具有相互独立的地址空间,同一进程的线程间共享当前进程的地址空间。某进程内的线程在其它进程不可见;
-
资源拥有:进程间的资源相互独立,同一进程的线程间共享当前进程的资源,如:内存、
I/O
、CPU
等; -
发生错误:某个进程发生错误,只会导致这个进程崩溃,其他进程不收影响,进程中的某个线程发生崩溃,会导致整个进程崩溃;
-
调度和切换:进程切换时,资源消耗大,线程上下文切换比进程上下文切换要快得多,需要频繁切换的并共享资源,需要使用多线程;
-
执行过程:每个独立的进程都有一个程序入口和顺序执行序列,显示线程不能独立执行,需要依附于进程,有进程进行调度;
-
线程是处理器调度的基本单位,进程不是。
4、多线程的意义
多线程是为了解决在单一进程等待 I/O
操作时 CPU
空闲的问题,为了解决单线程被阻塞使整个进程就被阻塞的问题而提出的命题。
当我们使用多线程时明显能感觉以下优点:
- 适当提高程序执行的效率;
- 适当提高资源的利用率,如:内存、
CPU
等; - 线程上的任务执行完成后,线程会自动销毁。
但是多线程提供方便的同时,也带来了不少缺点:
- 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
- 开启大量线程会占用大量内存,降低程序的性能
- 会使程序设计更加复杂,如:线程间通讯、线程间数据共享、线程间资源争夺等。
所以,我们需要在力所能及的范围下合理的使用多线程,开启一个合理的线程数,这样既能提高程序执行的效率,也不会占用过多资源。
5、多线程的原理
以单核心 CPU 举例,开启了5个线程, 每个线程都简单的打印了一个字符串,在控制台发现,5个字符串基本上同时打印出来,此时,感觉多线程就是多个线程同时执行。其实不是的,看下方图:
图上有3个线程,每段黑色实线代表当前 CPU 正在执行任务的时间,虚线代表等待时间。由此可以看出,CPU 只是在各个线程之间来回切换而已,只是切换的速度非常的快。
CPU 把任务调度分割成了很多个时间片,在时间片之间不断的切换,才造成了同时输出的假象,要实现真的多线程,需要多核 CPU ,如果是双核 CPU ,那么统一时间是 2 个任务同时执行。
6、 线程的生命周期
作为开发者,只要是一个对象,就有相应的生命周期。
当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在线程的生命周期中,它要经过新建 (New)
、就绪 (Runnable)
、运行(Running)
、阻塞 (Blocked)
和死亡 (Dead)
5种状态。尤其是当线程启动以后,它不可能一直"霸占"着 CPU 独自运行,所以 CPU 需要在多条线程之间切换,于是线程状态也会多次在运行、阻塞之间切换。
-
新建:当程序创建了一个线程之后,该线程就处于新建状态,此时仅由进程为其分配内存,并初始化其成员变量的值;
-
就绪:当线程对象调用了
start()
方法之后,该线程处于就绪状态。进程会为其创建方法调用栈和程序计数器,等待调度运行; -
运行:如果处于就绪状态的线程获得了 CPU,开始执行
run()
方法的线程执行体,则该线程处于运行状态; -
阻塞:当处于运行状态的线程失去所占用资源之后,便进入阻塞状态;
-
死亡:任务执行完成或被强制退出。
7、线程池的原理
多线程下每个线程都是需要创建的,但是创建完成之后,就需要对各个线程进行管理和回收,这个操作就要由线程池来完成。
对于一个线程池,它的大小是额定的,所以当新的任务需要线程的时候,线程池就需要先查看自身是否小于核心线程池的大小,如果还有资源,那么就可以直接开启一个线程执行任务,如果核心线程池已满,那么就需要判断线程池中工作队列是否已满,如果还有空余队列,就可以让任务 push 进队列,按照队列 FIFO
的顺序依次执行。如果没有空余队列,就可以进行以下策略选择:
-
AbortPolicy
:默认策略,新任务提交时直接抛出未检出的异常RejectedExecutionException
,该异常可由调用者捕获; -
CallerRunsPolicy
:为了调节机制,既不抛弃任务也不抛出异常,而是将任务回退到调用者,不会在线程池的线程中执行新的任务,而是在调用exector
的线程中运行新的任务; -
DiscardPolicy
:新提交的任务被抛弃; -
DiscardOldestPolicy
:抛弃等待时间最久的任务,然后尝试新提交的任务。
8、线程间的通讯
线程间的通讯就是一个线程传递数据给另一个线程,在一个线程中执行完特定任务后,转到另一个线程继续执行任务。
线程间通信常用的方法:
- 基于
NSThread
:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
- 基于
GCD
:
- (void)dispatch_async_function {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
/// 子线程操作
/// ...
/// 回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
/// ...
});
});
}
- 基于
port
:
// viewcontroller.m
- (void)function {
/// 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];
}
// model.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 = [@"name" dataUsingEncoding:NSUTF8StringEncoding];
NSData *data2 = [@"address" 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];
}
9、线程和队列的关系
一个队列由一个或多个任务组成,当这些任务要开始执行时,系统会分别把他们分配到某个线程上去执行。当有多个系统核心时,为了高效运行,这些核心会将多个线程分配到各核心上去执行任务,对于系统核心来说并没有任务的概念。 对于一个并行队列来说,其中的任务可能被分配到多个线程中去执行,即这个并行队列可能对应多个线程。对于串行队列,它每次对应一个线程,这个线程可能不变,可能会被更换。
每一时刻,一个线程都只能执行一个任务。一个线程也可能是闲置或者挂起的,因此线程存在时不一定就在执行任务。 队列和线程可以说是两个层级的概念。队列是为了方便使用和理解的抽象结构,而线程是系统级的进行运算调度的单位,他们是上下层级之间的关系。
10、线程和 runloop 的关系
线程 和 runloop
的关系主要为以下4点:
-
runloop
与线程是一一对应的,一个runloop
对应一个核心的线程,runloop
是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里; -
runloop
是来管理线程的,当线程的runloop
被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务; -
runloop
在第一次获取时被创建,在线程结束时被销毁; -
对于主线程来说,
runloop
在程序一启动就默认创建好了,对于子线程来说,runloop
是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意确保子线程的runloop
被创建,不然定时器不会回调。
11、结语
到这里,多线程理论知识就结束了,比较枯燥乏味,但是这些东西对于一个开发者来说还是需要了解的,有了这些基础知识,才能真正的开始多线程源码研究。