本文由快学吧个人写作,以任何形式转载请表明原文出处。
一、资料准备
《POSIX多线程程序设计》,支持正版。所以自己找资源吧。
苹果官方关于线程的文档 : 苹果官方线程文档
二、线程和进程的概念
1. 什么是进程
(1). 进程的理解
进程可以分成广义和狭义的解释 :
广义 :
进程是 : 一个具有一定独立功能的程序,关于某个数据集合的活动。
进程是操作系统动态执行的基本单元,既是基本分配单元,也是基本执行单元。也就是说,操作系统想要动态的运行起来,必须得有进程。没有进程,操作系统就不能执行。
狭义 :
进程就是一个正在运行的程序实例。
以iOS的app举例,原则上iOS是不支持多进程的。这个不支持的意思就是 : 每个进程之间的运行都是独立的,每个进程都在它自己专用的、收到保护的内存空间上执行,拥有着独立运行所需要的全部资源。
仅以iOS为例,可以更狭义的理解进程 :
仅以iOS原则为例,更狭义的说,一个进程就是一个正在运行的app。
(2). iOS为什么是单进程
(1). iOS系统是沙盒模式的设计,一个进程一个sandBox。进程与进程之间是不可以互相访问和利用资源的。这使得每个进程的资源都更加的安全、隐私性更加的优秀。
(2). 进程之间的切换和通信,可以使效率更高,但是随之而来的是资源消耗的增大。
(3). 个人猜测 : 没必要多进程。因为苹果的官方工程师都是大牛,没必要给我这种小菜这么大的权限可以切换进程。因为多进程就意味着更多的资源,做不好资源的优化,只会使app越来越卡。
2. 什么是线程
在计算机中,线程是一种能够实现某种功能的基本单元。而针对iOS来说,因为iOS原则上不支持多进程,我们可以更具体的理解这句话为 :
线程是进程的基本单元。是程序执行流的最小单元。是处理器调度的基本单位。一个进程的的所有任务都在线程中执行。
针对iOS :
- 针对iOS,进程想要执行任务,必须至少拥有一条线程。
- 在iOS进程(就是app)启动的时候,默认会开启一条线程,这条默认线程就是我们常说的主线程。也可以叫它UI线程,因为这条默认的线程负责的是处理UI事件,包括显示和刷新。
3. 进程和线程之间的关联
- 地址空间共享。所谓地址空间共享,就是同一个进程中的所有线程,共享这个进程的地址空间。
- 资源分配共享。同一个进程中的所有线程,共享这个进程的资源,包括但不限于 : 内存、I/O、CPU。
- iOS中,线程才是处理器调度的基本单位。
4. 多线程
4.1 多线程存在的意义
多线程是为了使进程中的任务处理不延误,一个任务的延误不耽误其他任务的执行。加快了任务的处理效率。
4.2 单核CPU实现多线程的原理
多核才存在真正意义上的多线程,单核的多线程只是一个伪概念。
正常来说,单核的CPU,在同一个时间点,只能处理一个任务。但是如果单核CPU在单位时间里,在多条线程之间快速切换调度,就会造成一种单核CPU可以多线程并行的假象。这种行为或者说思想,也被叫做时间片轮转。
为什么要不停的时间片轮转呢?
举个例子说,有5个人,饿的不行了,突然遇到了一个人给他们喂饭吃,如果让第一个人吃饱了,再让第二个人吃,然后再让第三个人吃,直到第五个人,那么第五个人可能还没等到前四个人吃完呢,就gg了。为了不让后面吃饭的人gg,最好的办法就是一人吃一口,都先别奔着吃饱使劲,都奔着别gg使劲 。
这5个人就是线程,这个喂饭的人就是单核CPU。这么做既是为了让所有的线程都可以存活,也自然的提升了单核CPU的利用率,也是利益最大化的体现。
4.3 多线程的优缺点
优点 :
- 可以适当的提高程序的执行效率。
- 可以适当的提高资源的利用率。例如 : CPU和内存的利用率。
- 线程上的任务执行完成之后,线程是可以自动销毁的。
缺点 :
- 对比多进程来说,多线程是不够健壮的。
例如 : 如果是多进程,当一个进程crash之后,是不会影响其他的进程。但是多线程就不行,同一个进程中的多线程,一旦有一个线程发生问题,比如crash,那么整个程序都会crash。
- 多线程是要占用内存空间的。
例如 : iOS中进程默认开启的主线程是需要1M的栈区内存。其他的二级线程要占用512K的栈区空间。
- 多线程影响CPU性能
例如 : CPU在多条内存之间快速切换调用是需要性能的,要调用的线程越多,CPU切换的就越频繁,这就会导致每条线程被调用的频次就会降低,频次越低,线程里的任务执行的效果就越差,如果想要保证每条线程里的任务都可以完成的很好,就要加大资源开销,让人在感官上感受不到执行效率变差了。
- 多线程的程序的设计就会更复杂
例如 : 多线程之间如何通信、多线程的数据共享。
5. 线程的生命周期
- 创建。可以理解为创建一个线程对象。
- 就绪。线程是可以运行的状态,并且被加入了可调度线程池,但是在等待CPU的调度。比如进程刚刚启动,或者这个进程刚从阻塞状态中恢复,或者可能被调度的权利被其他的线程抢占了。
- 运行。线程正在执行自己内部的任务,也就是被CPU调度了。在线程执行完任务之前,线程的状态可能会在就绪和运行之间多次切换,这个切换,是由CPU决定的,不受我们的控制。
- 阻塞。线程处于无法执行任务的状态,比如调用了
sleep()、等待同步锁(@synchronized),从可调度线程池中移除。- 销毁。线程中的任务执行完毕了。执行完毕就可以退出。也可能是满足某个条件之后,在线程的内部或者主线程中手动终止线程的执行,进而结束执行任务,进行销毁。
- 线程绝大多数的时间都处于2,3,4这三种状态,也就是 : 就绪、运行、阻塞。
对于线程的销毁,有两种常见的方法 : cancel和exit,它们的区别是 :
exit: 强制终止线程。只要exit,后面的所有代码都不会执行。
cancel: 不会强制终止正在执行的线程。终止的只是当前的线程。
6. 线程池
容纳线程的容器,就叫线程池。线程池存在三种容量 : 最大容量,核心容量,当前容量。
线程池是如何容纳线程的 :
四个缓存策略 :
AbortPolicy : 饱和策略的默认策略。当前的线程池已经与核心线程池的容量一致,并且当前线程池的工作队列已经满了,并且当前线程池中的所有线程都在工作。那么就只能在新任务要加入的时候抛出异常(调用这个接口
RejectedExecutionExeception),这个异常可以由调用者捕获。CallerRunsPolicy : 这是饱和策略的调节策略。既不放弃任务,也不抛出异常,而是将某些任务回退到调用者。它不会在线程池的线程中执行新的任务,而是在
exector线程中执行新的任务。DiscardPolicy : 直接抛弃新提交的任务。
DiscardOldestPolicy : 抛弃最长时间没执行的任务,就是队列头部的任务,然后尝试提交新的任务。这种策略不适合工作队列为优先队列的场景。
三、iOS中的多线程
1. 多线程方案
常见的4种iOS的
| 方案 | 简介 | 语言 | 线程生命周期 | 使用频率 |
|---|---|---|---|---|
| pthread | 1. 一套通用的多线程API; 2. 适用于 Unix、Linux、Windows等系统; 3. 具有跨平台性、可移植性; 4. 使用难度较大。 | C | 程序员管理 | 极少使用(不是大牛,不建议直接操作pthread) |
| NSThread | 1. 面向对象 2. 比 pthread简单,直接操作线程对象 | OC | 程序员管理 | 较少使用 |
| GCD | 1. 苹果推荐,替代NSThread 2. 充分利用设备的多核 | C | 自动管理 | 经常使用 |
| NSOperation | 1. 基于GCD实现 2. 比 GCD多一些简单实用的功能 3. 更加的面向对象 | OC | 自动管理 | 经常使用 |
2. 举例
- (void)jd_thread_test:(NSString *)name
{
NSLog(@"\n方式 : %@\n当前线程 : %@ -- 主线程 : %@",name,[NSThread currentThread],[NSThread mainThread]);
}
void *jd_pthread_use(void *param)
{
NSLog(@"\n方式 : %@\n当前线程 : %@ -- 主线程 : %@",param,[NSThread currentThread],[NSThread mainThread]);
return NULL;
}
- (IBAction)jd_pthread:(UIButton *)sender {
// 定义一个线程标识符,程序中用线程标识符来表示线程
pthread_t jd_thread;
/**
参数 :
1. pthread_t类型的线程变量,要传地址,如果不是指针,那就&取地址
2. 线程的属性,不知道填什么,或者没什么特定的情况,就填NULL
3. 线程中要执行的函数的地址,也就是说,不要在这里传参数,直接把函数名放进去就行了
4. 3中的函数的参数
*/
pthread_create(&jd_thread,NULL,jd_pthread_use,@"pthread");
}
- (IBAction)jd_NSThread:(UIButton *)sender {
[NSThread detachNewThreadSelector:@selector(jd_thread_test:) toTarget:self withObject:@"NSThread"];
}
- (IBAction)jd_gcd:(UIButton *)sender {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self jd_thread_test:@"GCD"];
});
}
- (IBAction)jd_NSOperation:(UIButton *)sender {
NSOperationQueue *q = [[NSOperationQueue alloc] init];
[q addOperationWithBlock:^{
[self jd_thread_test:@"NSOperation"];
}];
}
3. 线程间的通讯
苹果官方给了7种通信机制 : 官方线程通信机制
翻译一下 :
| 通信机制 | 描述 |
|---|---|
| 直接消息传递 | Cocoa应用程序支持直接在其他线程上执行方法。 这意味着一个线程可以直接在其他任何的线程上执行一个方法。 因为方法是在目标线程的上下文执行的,所以以这种方式通信发送的消息会自动在该线程上自动化。 |
| 全局变量、 共享内存、 对象 | 在两个线程间通信,另一种简单的方法就是通过全局变量、共享内存块、对象。 这种方法快速而简单,但是对比直接消息传递更脆弱。必须使用锁或者其他同步机制保护共享的变量,确保代码的正确。 如果不这样做的话,可能会引发竞态条件、损坏数据甚至崩溃。 |
| Conditions | Conditions是一种同步工具,本身是一种特殊类型的锁。 使用Conditions可以控制线程中特定代码的执行时间。 可以把Conditions看作一个守卫,只有条件满足了,才允许线程运行。 |
| Run loop sources | 通过自定义Runloop source的配置,可以用来让线程接收特定消息。 由于Runloop source是依靠事件来驱动的,所以Runloop source在无事可做的时候,会让线程进入自动休眠状态,这也提高了线程的效率。 |
| Ports and sockets | 基于端口的通信是两个线程之间更复杂的一种通信方法,但是它更可靠。 端口和套接字还可以用于与其他进程和服务通信。 为了提高效率,端口是通过Runloop source实现的,所以端口上没有数据的时候,也会让线程进入休眠状态。 |
| 消息队列 | 传统的多进程服务定义了FIFO抽象队列,用来管理消息的传入和传出。 消息队列优点是简单方便。 缺点是没有其他通信机制的高效率。 |
| Cocoa分布式对象 | 基于Cocoa的分布式对象,它提供了基于端口通信的高级实现,可以作用于线程间通信。但是资源开销大。 可以尝试用它做进程间通信,而不是线程间通信。 |