多线程-GCD(1)(附带经典面试执行顺序求解)
一、进程和线程
1.1 什么是进程和线程
- 进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内
- 线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程要想执行任务,必须得有线程,进程至少要有一条线程。程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程
1.2 进程和线程的关系
-
一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行.相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
-
所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
- 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
-
资源拥有:同一进程内的线程共享本进程的资源如内存、I/O、cpu等,但是进程之间的资源是独立的
-
执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制
-
根本区别:进程是操作系统进行资源分配的基本单位,而线程是操作系统进行任务调度和执行的最小单位
1.3 线程调度
一个 CPU 核心同一时刻只能执行一个线程。当线程数量超过 CPU 核心数量时,一个 CPU 核心往往就要处理多个线程,这个行为叫做线程调度。 就是 一个CPU 核心轮流让各个线程分别执行一段 时间,也就是说一个设备并发执行的线程数量是有限的。CPU在多个任务直接进行快速的切换,这 个时间间隔就是时间片。
二、多线程
2.1 线程的生命周期
2.2 多线程的优缺点
主线程是1M
,子线程是512kb
,开启一条线程大概也需要90
微秒的时间。
2.2.1 优点:
* 能适当提高程序的执行效率 * 能适当提高资源的利用率(CPU,内存) * 线程上的任务执行完成后,线程会自动销毁
2.2.2 缺点
* 开启线程需要占用一定的内存空间(默认情况下,每一个线程都占 512 KB) * 如果开启大量的线程,会占用大量的内存空间,降低程序的性能 * 线程越多,CPU 在调用线程上的开销就越大 * 程序设计更加复杂,比如线程间的通信、多线程的数据共享
2.3 线程池
2.4 通过一个代码简单的了解一下多线程:
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"%@",[NSThread currentThread]); //输出设备能够支持线程的最大并发数量 NSLog(@"%@",[NSProcessInfo processInfo].activeProcessorCount); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { //主线程的number为1 for (int i = 0; i < 200; i ++) { dispatch_async(dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL), ^{ NSLog(@"%@",[NSThread currentThread]); }); } }
我们从运行结果可以发现,主线程的number为1,多线程下不会一直开启新线程,也会从原来空闲的线程中拿已经有的线程。
三、GCD
全称是 Grand Central Dispatch,纯 C 语言,提供了非常多强大的函数。GCD 是苹果公司为多核的并行运算提出的解决方案。GCD 会自动利用更多的CPU内核(比如双核、四核)。GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。
3.1 经典面试题
我们先了解一个概念:serial 串行 concurrent 并发
- (void)test1 { //每个打印都相当于是一个任务,任务执行的时间未知 dispatch_queue_t queue = dispatch_queue_create("lg", DISPATCH_QUEUE_CONCURRENT); NSLog(@"1"); dispatch_async(queue, ^{ NSLog(@"2"); dispatch_sync(queue, ^{ NSLog(@"3"); }); NSLog(@"4"); }); NSLog(@"5"); } //第一个执行的一定是1 //async是异步执行的 所以在异步执行函数内外的2和5 没有先后顺序 //sync是同步执行的 所以在同步执行函数内外的2和3 有先后顺序 先2后3 //async开启了线程,在新的线程里执行 所以内外顺序不一致 //sync没有开启线程 有先后顺序 所以234在同一个线程里 按照234执行 //1234(5不确定)
/这里需要注意创建队列是串行还是并行队列 - (void)test2 { //死锁 //因为这个队列是FIFO的结构 //async先执行,但是async需要等待sync先执行,这违反了FIFO的结构 dispatch_queue_t queue = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL); NSLog(@"1"); dispatch_async(queue, ^{ NSLog(@"2"); dispatch_sync(queue, ^{//async可以解决这个问题 NSLog(@"3"); }); NSLog(@"4"); }); NSLog(@"5"); }
- (void)test6 { dispatch_queue_t queue = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ NSLog(@"2"); dispatch_async(queue, ^{ NSLog(@"3"); }); NSLog(@"4"); }); dispatch_async(queue, ^{ NSLog(@"6"); }); } //串行队列 2463 //代码的执行是从上往下,所以此时只需要看两个大的async即可(先不管包含的async),所以6在3的前面执行
3.2 同步异步和串行并发的搭配
同步+串行:可能死锁 同步+并发:不开启新线程,任务顺序执行 异步+串行:开启新线程,任务顺序执行 异步+并发:开启新线程,任务异步执行,没有顺序,与CPU调度有关
3.3 串行队列和并发队列的源码解析
- (void)test { //主队列 dispatch_queue_t mainQueue = dispatch_get_main_queue(); //全局并发队列 dispatch_queue_t globQueue = dispatch_get_global_queue(0, 0); //自己创建的串行队列 dispatch_queue_t normalQueue = dispatch_queue_create("com.demo.serial", DISPATCH_QUEUE_SERIAL); }
我们找到关于dispatch_queue_create的api实现部分:
dispatch_queue_t dispatch_queue_create(const char *label, dispatch_queue_attr_t attr) { return _dispatch_lane_create_with_target(label, attr, DISPATCH_TARGET_QUEUE_DEFAULT, true); }
_dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa, dispatch_queue_t tq, bool legacy) { dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
我们可以看到是调用_dispatch_lane_create_with_target并添加2个默认参数实现的,找到对应实现。然后看到这个方法里面,把我们传入的DISPATCH_QUEUE_SERIAL
或者DISPATCH_QUEUE_CONCURRENT
参数进行封装,封装成了dqai
。我们可以大致看看封装的实现:
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"); } size_t idx = (size_t)(dqa - _dispatch_queue_attrs); dqai.dqai_inactive = (idx % DISPATCH_QUEUE_ATTR_INACTIVE_COUNT); idx /= DISPATCH_QUEUE_ATTR_INACTIVE_COUNT; dqai.dqai_concurrent = !(idx % DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT); idx /= DISPATCH_QUEUE_ATTR_CONCURRENCY_COUNT; dqai.dqai_relpri = -(int)(idx % DISPATCH_QUEUE_ATTR_PRIO_COUNT); idx /= DISPATCH_QUEUE_ATTR_PRIO_COUNT;
这里我们可以看到dqai
里面有个dqai_concurrent
的属性,顾名思义是代表是否是并发,那么默认的就是串行。接下来我们继续看如何根据dqai创建队列:通过init
方法初始化,第三个参数,如果是并发传入DISPATCH_QUEUE_WIDTH_MAX
,如果是串行传入1
_dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ? DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER | (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
static inline dispatch_queue_class_t _dispatch_queue_init(dispatch_queue_class_t dqu, dispatch_queue_flags_t dqf, uint16_t width, uint64_t initial_state_bits) { uint64_t dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(width); dispatch_queue_t dq = dqu._dq; dispatch_assert((initial_state_bits & ~(DISPATCH_QUEUE_ROLE_MASK | DISPATCH_QUEUE_INACTIVE)) == 0); if (initial_state_bits & DISPATCH_QUEUE_INACTIVE) { dq->do_ref_cnt += 2; // rdar://8181908 see _dispatch_lane_resume if (dx_metatype(dq) == _DISPATCH_SOURCE_TYPE) { dq->do_ref_cnt++; // released when DSF_DELETED is set } } dq_state |= initial_state_bits; dq->do_next = DISPATCH_OBJECT_LISTLESS; dqf |= DQF_WIDTH(width); os_atomic_store2o(dq, dq_atomic_flags, dqf, relaxed); dq->dq_state = dq_state; dq->dq_serialnum = os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed); return dqu; }
我们可以看到这里有一个DQF_WIDTH(width)的定义:
- 串行队列 :它的DQF_WIDTH等于1,相当以它只有一条通道。所以队列中的任务要串行执行,也就 是一个一个的执行,必须等上一个任务执行完成之后才能开始下一个,而且一定是按照先进先出的 顺序执行的,比如串行队列里面有4个任务,进入队列的顺序是a、b、c、d,那么一定是先执行 a,并且等任务a完成之后,再执行b... 。
- 并发队列:它的DQF_WIDTH大于1,相当于有多条通道。队列中的任务可以并发执行,也就任务可以同时执行,比如并发队列里面有4个任务,进入队列的顺序是a、b、c、d,那么一定是先执行 a,再执行b...,也是按照先进先出(FIFO, First-In-First-Out)的原则调用的,但是执行b的时候a不一定执行完成,而且a和b具体哪个先执行完成是不确定的。通道有很多,哪个任务先执行完得看 任务的复杂度,以及cpu的调度情况。
- 队列的作用是用来存储任务。队列分类串行队列和并发队列。串行队列和并发队列都是 FIFO ,也就是先进先出的数据结构。
四 补充内容
线程和runloop的关系 1:runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里。 2:runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。 3:runloop在第一次获取时被创建,在线程结束时被销毁。 4:对于主线程来说,runloop在程序一启动就默认创建好了。 5:对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调。