第十二篇:iOS里多线程
首先我们来看下线程和进程的区别以及介绍:
- 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行
- 进程要想执行任务,必须得有线程,进程至少要有一条线程
- 程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程
-
进程是指在系统中正在运行的一个应用程序
-
每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
-
通过“活动监视器”可以查看 Mac 系统中所开启的进程
-
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.
-
相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
-
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
-
地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
-
资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的。
-
执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
-
根本区别:进程是操作系统进行资源分配的基本单位,而线程是操作系统进行任务调度和执行的最小单位。
多线程的意义
- 优点
- 能适当提高程序的执行效率
- 能适当提高资源的利用率(CPU,内存)
- 线程上的任务执行完成后,线程会自动销毁
- 缺点
- 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB)
- 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
- 线程越多,CPU 在调用线程上的开销就越大
- 程序设计更加复杂,比如线程间的通信、多线程的数据共享
通过如下代码可以知道当前支持的最大线程并发数量:
WechatIMG1987.jpeg线程的生命周期
image.png线程池
image.png我们来研究下线程池,我们写如下代码,然后进行运行打印,我们发现如果在理论上当我们点击屏幕的时候,线程会按顺序1,2,3....这种顺序去创建和打印,但是实际上并没有,同时也会出现两条同序列号的线程,这个是因为其实GCD在维护一个线程池,它会在线程池里拿到缓存的并没有执行的任务的线程进行返回,所以没有必要调用dispatch_async(dispatch_queue_create就去创建线程,造成没必要的浪费。线程的复用机制。GCD的线程池里缓存了64条线程。之前我们打印线程数是指当前并发的线程数,CUP多少核指的是最大并发线程数。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",[NSThread currentThread]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
for (int i = 0; i < 200; i ++) {
dispatch_async(dispatch_queue_create("hpw", DISPATCH_QUEUE_SERIAL), ^{
NSLog(@"%@",[NSThread currentThread]);
});
}
}
2022-05-23 18:29:07.521031+0800 001--Test[10014:210251] <_NSMainThread: 0x600002e680c0>{number = 1, name = main}
2022-05-23 18:29:12.601367+0800 001--Test[10014:210934] <NSThread: 0x600002e25480>{number = 5, name = (null)}
2022-05-23 18:29:12.601411+0800 001--Test[10014:210939] <NSThread: 0x600002e29a40>{number = 7, name = (null)}
2022-05-23 18:29:12.601429+0800 001--Test[10014:210935] <NSThread: 0x600002e4ea00>{number = 6, name = (null)}
2022-05-23 18:29:12.601452+0800 001--Test[10014:210936] <NSThread: 0x600002e4e000>{number = 3, name = (null)}
2022-05-23 18:29:12.601667+0800 001--Test[10014:211152] <NSThread: 0x600002e649c0>{number = 11, name = (null)}
2022-05-23 18:29:12.601572+0800 001--Test[10014:210937] <NSThread: 0x600002e5ee00>{number = 4, name = (null)}
2022-05-23 18:29:12.601791+0800 001--Test[10014:211154]
GCD线程
GCD 简介
什么是GCD?
全称是 Grand Central Dispatch
纯 C 语言,提供了非常多强大的函数
GCD的优势
GCD 是苹果公司为多核的并行运算提出的解决方案
GCD 会自动利用更多的CPU内核(比如双核、四核)
GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码
下面来看几个金典的GCD打印输出:
下面这个打印首先是1,2在3前面,3在4前面,2和5没有顺序
dispatch_queue_t queue = dispatch_queue_create("hpw", DISPATCH_QUEUE_CONCURRENT);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_sync(queue, ^{//同步的是不开线程的
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
2022-05-23 18:50:39.861532+0800 002--GCD面试题[10463:226575] 1
2022-05-23 18:50:39.861588+0800 002--GCD面试题[10463:226575] 5
2022-05-23 18:50:39.861593+0800 002--GCD面试题[10463:226703] 2
2022-05-23 18:50:39.861638+0800 002--GCD面试题[10463:226703] 3
2022-05-23 18:50:39.861671+0800 002--GCD面试题[10463:226703] 4
下面这个当运行时候,会出现线程死锁,这个我们画个图来解释下原因,因为这个是串行队列,那么它就需要满足FIFO也就是先进先执行的原则,因为56到60行这些代码是先放到queue里的,57行代码也是放在queue队列里,但是57行代码是之后放的。所以在队列里第一个线程要先执行完,才能执行后面57行,但是56到60行执行完就要执行里面的57,这样就违背了FIFO原则,先进先执行,所以会造成死锁。
WechatIMG1989.jpeg WechatIMG1988.jpeg
那如果把代码sync改成async后就可以运行了,这个是因为 NSLog(@"2")和 NSLog(@"4")是先加载到第一个queue里的,然后 NSLog(@"3") ,所以在里面先输出2,4再输出3。
- (void)test2 {
dispatch_queue_t queue = dispatch_queue_create("hpw", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(queue, ^{
NSLog(@"2");
dispatch_async(queue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
2022-05-23 19:58:47.784562+0800 002--GCD面试题[11410:256488] 1
2022-05-23 19:58:47.784637+0800 002--GCD面试题[11410:256488] 5
2022-05-23 19:58:47.784665+0800 002--GCD面试题[11410:256586] 2
2022-05-23 19:58:47.784787+0800 002--GCD面试题[11410:256586] 4
2022-05-23 19:58:47.784905+0800 002--GCD面试题[11410:256586] 3
队列没有执行能力,如果是串行队列只能决定执行的顺序,但是具体执行任务需要线程来操作的,线程的切换大概是90us
下面这个当运行时候,打印为2,4,6,3. 3的输出始终在6后面,这个是因为标注1和标注3里代码先加载到queue里。标注2后加,所以标注3里代码比标注2里代码先运行,因为这个是串行队列,按加入顺序执行。
- (void)test2 {
dispatch_queue_t queue = dispatch_queue_create("hpw", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{//标注1
NSLog(@"2");
dispatch_async(queue, ^{标注2
NSLog(@"3");
});
NSLog(@"4");
});
dispatch_async(queue, ^{//标注3
NSLog(@"6");
});
}
2022-05-23 21:35:31.333036+0800 002--GCD面试题[13190:312368] 2
2022-05-23 21:35:31.333083+0800 002--GCD面试题[13190:312368] 4
2022-05-23 21:35:31.333120+0800 002--GCD面试题[13190:312368] 6
2022-05-23 21:35:31.333150+0800 002--GCD面试题[13190:312368] 3
队列
队列和线程之间没有任何关系的,队列是用来存储任务的。队列没有能力去调度任务,只有线程才可以调度任务。
队列有串行队列,并行队列,全局并发队列,主队列
1.dispatch_get_main_queue探究
dispatch_get_main_queue(void)
{
return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q);
}
struct dispatch_queue_static_s _dispatch_main_q = {
DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
#if !DISPATCH_USE_RESOLVERS
.do_targetq = _dispatch_get_default_queue(true),
#endif
.dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
DISPATCH_QUEUE_ROLE_BASE_ANON,
.dq_label = "com.apple.main-thread",
.dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
.dq_serialnum = 1,
};
在上面 DQF_WIDTH(1)这个是串行队列标志
dispatch_queue_attr_info_t
_dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
{
dispatch_queue_attr_info_t dqai = { };
if (!dqa) return dqai;
#if DISPATCH_VARIANT_STATIC
if (dqa == &_dispatch_queue_attr_concurrent) {
dqai.dqai_concurrent = true;
return dqai;
}
#endif
if (dqa < _dispatch_queue_attrs ||
dqa >= &_dispatch_queue_attrs[DISPATCH_QUEUE_ATTR_COUNT]) {
#ifndef __APPLE__
if (memcmp(dqa, &_dispatch_queue_attrs[0],
sizeof(struct dispatch_queue_attr_s)) == 0) {
dqa = (dispatch_queue_attr_t)&_dispatch_queue_attrs[0];
} else
#endif // __APPLE__
DISPATCH_CLIENT_CRASH(dqa->do_vtable, "Invalid queue attribute");
}
通过上面的dqa知道,当传入的是NULL时候,其返回的是串行队列。
队列总结
串行队列和并行队列都是FIFO先进先执行
DQF_WIDTH(大于1) 并发队列 --- 理解成多车道
DQF_WIDTH(1) 串行队列 --- 理解成单行道
画个图来理解下,我们可以这样理解,串行队列口子小,只有等上面执行完,才能执行下面的,因为口子被堵住了。并行队列就不是,其有不同的口子,所以可以多个口子执行任务。
WechatIMG1990.jpeg