[TOC]
一、 线程和进程
1.1 线程的定义
- 线程是进程的
基本执行单元
,一个进程的所有任务倒在线程中执行 - 进程要想执行任务,必须得有线程,进程至少要有一条线程
- 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程
1.2 进程的定义
- 进程是指在系统中正在运行的一个应用程序
- 每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存
1.3 iOS开发为什么是单进程的
- 沙盒-单进程数据更加隐私、安全
- 进程切换,消耗资源大
1.4 线程和队列的关系
- 线程和队列是两个完全独立的概念,线程是由系统分配、系统调度,队列是用来管理任务,开发者可以管理任务的串行、并行、优先级、先后顺序;
- 开发者管理任务队列,系统根据队列的属性,来给队列中的任务分配线程;
- 队列是提供给开发者使用的抽象概念,线程是由系统调度的真实对象,开发者可以通过管理队列来指定系统分配、调度线程的数量、顺序等。
- 任务执行速度:CPU、线程、队列、任务的复杂度、任务的优先级
- 大量的临时变量:使用 autorealease 处理
1.5 线程和进程的关系和区别
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间
- 资源拥有:同一进程内的线程共享进程的资源,如内存、I/O、cpu等,但是进程之间的资源是独立的
- 健壮性:一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都会死掉。多以多进程要比多线程健壮
- 并发操作:进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能使用线程不能用进程
- 执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存于应用程序中,由应用程序提供多个线程执行控制
- 调度单位:线程是处理器调度的基本单位,但进程不是
1.6 进程和线程与堆和栈的关系
- 进程的地址空间
每个进程的地址空间是独立的
,是操作系统把物理内存映射到进程的虚拟地址上。
进程只能访问自己的地址空间,不能访问别的进程的地址空间。
通俗的理解:如果有a.exe和b.exe同时在系统中运行,那么a和b都可以有0x00000000-0xffffffff
的虚拟地址空间(不考虑操作系统占用等因素)。
假如a b都有1个变量在地址0x12345678
处,看起来地址一样,实际上在物理内存中不是一个地方。 -
进程是线程的容器
,windows调度运行的单位是线程,一个进程至少有1个主线程。一个进程内所有的线程都处于同一个虚拟地址空间。 -
进程初始化的时候,系统会在进程的地址空间中创建一个堆,叫进程默认堆
。进程中所有的线程共用这一个堆。当然,可以增加1个或几个堆,给不同的线程共同使用或单独使用。 - 创建线程的时候,系统会在进程的地址空间中分配1块内存给线程栈,通常是1MB。
线程栈是独立的,不共享
。iOS中是512KB
1.7 补充内存相关知识
内存五大分区
1、栈区(stack
):由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
2、堆区(heap)
:一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据结构中的堆是两回事,分配方式类似于链表。new出来的放在这里。
3、全局区(静态区)
:(static)全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
4、文字常量区
:常量字符串就是放在这里的。程序结束后由系统释放
5、程序代码区
:存放函数体的二进制代码。-
栈和堆的区别
-
管理方式不同
。栈由操作系统自动分配释放,无需我们手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏; -
空间大小不同
。每个进程拥有的栈的大小要远远小于堆的大小。理论上,程序员可申请的堆大小为虚拟内存的大小,进程栈的大小64bits的Windows默认1MB,64bits的Linux默认10MB; -
生长方向不同
。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。 -
分配方式不同
。
堆都是动态分配的,没有静态分配的堆。
栈有2种分配方式:静态分配和动态分配。静态分配是由操作系统完成的,比如局部变量的分配。
栈的动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,栈的动态分配是由操作系统进行释放,无需我们手工实现。 -
分配效率不同
。
栈由操作系统自动分配,会在硬件层级对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
堆则是由C/C++提供的库函数或运算符来完成申请与管理,实现机制较为复杂,频繁的内存申请容易产生内存碎片。显然,堆的效率比栈要低得多。
-
二、 多线程
2.1 多线程的意义
- 优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU、内存)
- 线程上的任务执行完成后,线程会自动销毁
- 缺点
- 开启线程需要占用一定的内存空阿金(默认情况下,每一个线程都占 512 KB)
- 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信、多线程的数据共享
2.2 多线程的原理
CPU 在单位时间片里快速在各个线程之间切换
-
单线程
单核 CPU单线程 任务1 任务2 任务3
- 其本质也是单线程,一次只能执行一个任务
- 单核 CPU的多线程是通过 任务1执行一部分再执行任务2再切换任务3,这种方式达到多线程的效果
- 不停切换、时间片、共赢、达到利益最大化
-
多线程
多核 CPU线程1 任务1 线程2 任务2 线程3 任务3
2.3 多线程技术方案
-
pthread
- 一套通用的多线程API
- 适用于 Unix/Linux/Windows 等系统
- 跨平台/可移植
- 使用难度大
-
NSThread
- 使用更加面向对象
- 简单易用,可直接操作线程对象
-
GCD
- 旨在替代 NSTread 等线程技术
- 充分利用设备的多核
-
NSOperation
- 基于GCD(底层是GCD)
- 比 GCD 多了一些更简单使用的功能
- 使用更加面向对象
三、线程、线程池、生命周期、安全、通讯、runloop
3.1 线程的生命周期
新建:Start,创建线程,调用start进入就绪状态
就绪:Runnable,CPU调度当前线程,进入运行状态
运行:Running,任务执行完毕,退出线程并销毁,CPU调度当前线程
阻塞:Blocked,调用 sleep方法/等待同步锁/从可调度线程池移出
死亡:Dead,任务执行完成、强制退出
-
可调度线程池
- 当前线程
- 其他线程
- CPU调度的线程是从线程池中获取
-
正常流程和堵塞流程
- 正常流程:
新建->就绪->运行->死亡
- 堵塞流程:
新建->就绪->运行->堵塞->就绪->...->堵塞
image.png
- 正常流程:
3.2 线程池的原理
- 参数解释
- corePoolSize:线程池的基本大小(核心线程池大小)
- maxinumPoolSize:线程池的最大大小
- keepAliveTime:线程池中超过corePoolSize数目的空闲线程的最大存活时间
- unit:keepAliveTime参数的时间单位
- workQueue:任务阻塞队列
- threadFactory:新建线程的工厂
- handler:当提交的任务数超过maxnumPoolSize与workQueue之和时,任务会交给RejectedExecutionHandler来处理
- 【原理】线程池大小小于核心线程池大小
- 创建线程执行任务,流程和线程的生命周期一样
- 【原理】线程池大小不小于核心线程池大小
- 线程池判断工作队列未满
将任务push进队列 - 线程池判断工作队列已满
- 且
maxinumPoolSize>corePoolSize
,将创建新的线程来执行任务 - 交给报策略去处理
- Abort策略:默认策略,新任务提交时直接抛出未检查的异常 RejectedExecution,该异常可由调用者捕获
- CallerRuns策略:为调节机制,既不抛弃任务也不抛出异常,而是将某些任务会退到调用者。不会在线程池的线程中执行新的任务,而是在调用exector的线程中运行新的任务
- Discard策略:新提交的任务被抛弃。
- DiscardOldest策略:队列的是“对头”的任务,然后尝试提交新的任务。(不适合工作队列为优先队列场景)
- 且
- 线程池判断工作队列未满
3.3 线程和runloop的关系
- runloop与线程的对应关系
- runloop与线程是一一对应的,一个runloop对应一个核心的线程;
- 为什么说是核心的?因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里;
- runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完成后进入休眠状态,有了任务就会被唤醒去执行任务。
- runloop在第一次或失去时被创建,在线程结束时被销毁。
- 对于主线程来说,runloop在程序一启动就默认创建好了。
- 对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。
3.4 线程安全和线程通讯
-
atomic
与nonatomic
的区别- 属性定义
-
nonatomic
:非原子属性,适合内存小的移动设备 -
atomic
:原子属性(线程安全),针对多线程设计,默认值,需要消耗大量的资源。atomic只保证了写安全,但对于可变数组这类容器中的元素的读写安全并不能保证。
-
- 线程安全解释
- 保证同一时间只有一个线程能够写入
- atomic本身就有一把锁(自旋锁)
- 单写多读:单个线程写入,多个线程可以读取
- iOS开发的建议
- 所有属性都声明为 nonatomic
- 尽量避免多线程抢夺一块资源
- 尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力
- 属性定义
-
互斥锁小结
- 保证锁内的代码,同一时间,只有一条线程能够执行
- 互斥锁的锁定范围,应该尽量小,锁定范围越大,效率越差
- 互斥锁参数
- 能够加锁的任意 NSObject 对象
- 注意:锁对象一定要保证所有的线程都能够访问
- 如果代码中只有一个地方需要加锁,大多都使用 self,这样可以避免单独再创建一个锁对象
四、iOS中常用多线程技术
4.1 pthread
pthread_create 创建线程
-
参数:
- pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀
_t / Ref
结尾
同时不需要*
- 线程的属性,nil(空对象 - OC 使用的) / NULL(空地址,C 使用的)
- 线程要执行的
函数地址
void *
: 返回类型,表示指向任意对象的指针,和 OC 中的 id 类似
(*)
: 函数名
(void *)
: 参数类型,void *
- 传递给第三个参数(函数)的
参数
- 返回值:C 语言框架中非常常见
int
0 创建线程成功!成功只有一种可能
非 0 创建线程失败的错误码,失败有多种可能!
- 返回值:C 语言框架中非常常见
// 1: pthread pthread_t threadId = NULL; //c字符串 char *cString = "HelloCode"; int result = pthread_create(&threadId, NULL, pthreadTest, cString); if (result == 0) { NSLog(@"成功"); } else { NSLog(@"失败"); }
void *pthreadTest(void *para){ // 接 C 语言的字符串 // NSLog(@"===> %@ %s", [NSThread currentThread], para); // __bridge 将 C 语言的类型桥接到 OC 的类型 NSString *name = (__bridge NSString *)(para); NSLog(@"===>%@ %@", [NSThread currentThread], name); return NULL; }
- pthread_t:要创建线程的结构体指针,通常开发的时候,如果遇到 C 语言的结构体,类型后缀
4.2 NSThread
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];
/**
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];
NSLog(@"%@", myName);
}
NSLog(@"over");
}
4.3 GCD
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self threadTest];
});
4.4 NSOperation
[[[NSOperationQueue alloc] init] addOperationWithBlock:^{
[self threadTest];
}];