美文网首页OC面试相关
iOS 多线程原理 - 线程与队列底层

iOS 多线程原理 - 线程与队列底层

作者: 顶级蜗牛 | 来源:发表于2022-05-27 16:08 被阅读0次

    libdispatch-1271.120.2 下载
    苹果官方资源opensource

    多线程相关文献:
    iOS 多线程原理 - 线程与队列底层
    iOS 多线程原理 - GCD函数底层
    iOS 线程底层 - 锁

    本章节探究:
    1.了解进程、线程
    2.串行队列和并发队列
    3.线程死锁的原因
    4.同步函数 dispatch_sync 和 异步函数 dispatch_async
    5.面试题
    6.自定义线程池思想

    一、概念相关

    1.进程与线程

    进程:
    进程是指在系统中正在运行的一个应用程序,每个进程之间是独立的,每个进程均运行在其专用的且受保护的内存空间内 (通过“活动监视器”可以查看 Mac 系统中所开启的进程)。

    线程:
    线程是进程的基本执行单元,一个进程的所有任务都在线程中执行,进程要想执行任务,必须得有线程,进程至少要有一条线程,程序启动会默认开启一条线程,这条线程被称为主线程或 UI 线程。

    进程与线程的关系:

    • 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行。
    • 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。
    • 所处环境:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)
    • 地址空间:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间。
    • 资源拥有:同一进程内的线程共享本进程的资源如内存、I/Ocpu等,但是进程之间的资源是独立的。
    • 执行过程:每个独立的进程程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
    • 根本区别:进程是操作系统进行资源分配的基本单位,而线程是操作系统进行任务调度和执行的最小单位。
    2.线程的声明周期
    3.多线程

    时间片的概念:CPU在多个任务直接进行快速的切换,这个时间间隔就是时间片。

    单核CPU同一时间,CPU只能处理 1 个线程上的任务。

    多线程同时执行:
    CPU 快速的在多个线程之间的切换,CPU 调度线程的时间足够快,就造成了多线程的“同时”执行的效果;如果线程数非常多,CPU 会在 N 个线程之间切换,消耗大量的 CPU 资源,每个线程被调度的次数会降低,线程的执行效率降低。

    多线程的意义:

    • 优点
      • 能适当提高程序的执行效率
      • 能适当提高资源的利用率(CPU,内存)
      • 线程上的任务执行完成后,线程会自动销毁
    • 缺点
      • 开启线程需要占用一定的内存空间(默认情况下,主线程占1M,其它线程各占 512 KB)
      • 如果开启大量的线程,会占用大量的内存空间,降低程序的性能
      • 线程越多,CPU 在调用线程上的开销就越大
      • 程序设计更加复杂,比如线程间的通信、多线程的数据共享

    开辟一条线程大概需要90微秒的时间。

    // 获取设备能够支持线程的最大并发数量
    NSLog(@"%ld", [NSProcessInfo processInfo].activeProcessorCount);
    

    过多的开辟线程没有意义

    4.线程池

    GCD内部维护了一个线程池去管理64条线程,在App需要线程调度任务的时候实现复用;当前线程完成任务后就会被缓存到线程池里,下次再调用开辟线程的代码,GCD会从线程池上找已经开辟且就绪状态的线程。

    所以开辟线程的代码,并不是真正意义上的开辟线程。尽管GCD线程池里已有64条线程,但是最大并发数量还得是 [NSProcessInfo processInfo].activeProcessorCount;

    线程池的工作 饱和策略
    5.GCD

    GCD全称是Grand Central Dispatch,是苹果公司为多核的并行运算提出的解决方案,它是纯 C 语言并提供了非常多强大的函数;GCD会自动利用更多的CPU内核(比如双核、四核);GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)。

    程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码。

    6. 线程和Runloop的关系
    • 1.runloop与线程是一一对应的,一个runloop对应一个核心的线程,为什么说是核心的,是因为runloop是可以嵌套的,但是核心的只能有一个,他们的关系保存在一个全局的字典里
    • 2.runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务
    • 3.runloop在第一次获取时被创建,在线程结束时被销毁
    • 4.对于主线程来说,runloop在程序一启动就默认创建好了
    • 5.对于子线程来说,runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的runloop被创建,不然定时器不会回调

    二、串行队列 与 并发队列

    队列和线程没有任何关系,队列是存储任务的,线程是从队列中取出任务去执行的。
    队列分四种:串行队列并发队列全局并发队列主队列
    队列的特性:先进先出 FIFO

    打开libdispatch源码

    • 获取主队列:dispatch_get_main_queue
    DISPATCH_INLINE DISPATCH_ALWAYS_INLINE DISPATCH_CONST DISPATCH_NOTHROW
    dispatch_queue_main_t
    dispatch_get_main_queue(void)
    {
        return DISPATCH_GLOBAL_OBJECT(dispatch_queue_main_t, _dispatch_main_q); // _dispatch_main_q
    }
    

    DISPATCH_GLOBAL_OBJECT是一个宏定义好多地方有,没有办法定位到实际调用的哪个宏定义。但是通过lldb打印堆栈bt的话,又会多出好多别的函数调用不相关的东西。

    打印主线程,它会有一个特定的名称:com.apple.main-thread

     NSLog(@"%@",dispatch_get_main_queue()); 
    // <OS_dispatch_queue_main: com.apple.main-thread>
    

    源码里全局搜主线程名称,就能找到main_queue的初始化的地方

    主队列是串行队列的一个标志性的东西:DQF_WIDTH(1)
    在队列创建的时候看看源码就知道了。

    • 创建队列:dispatch_queue_create
    // label: 队列名称   attr是串行队列还是并发列表
    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_TARGET_QUEUE_DEFAULT = NULL
    }
    

    只需要关心第二个参数attr对于串行与并发的区别,它在_dispatch_lane_create_with_target的形参名称是dqa

    封装区分串行/并发的参数

    dqa封装成了dqai,它是怎么封装的?

    过多的不需要太关注拉。
    再回来看看_dispatch_lane_create_with_target初始化队列的步骤:
    1.规范化参数 (qos, overcommit, tq)

    2.初始化队列

    初始化队列

    初始化的时候会判断串行并发标志位去限制width是多少,串行指定是1,并发是14

    来看看_dispatch_queue_init的队列初始化,我们关注的队列是串行和并发的根本区别就是DQF_WIDTH(width),串行是DQF_WIDTH(1)

    dq->dq_serialnum它其实是标志是这个队列是什么队列

    _dispatch_queue_serial_numbers

    串行队列与并发队列区别实质的总结
    DQF_WIDTH(1) - 串行队列 - 举例:单行道
    DQF_WIDTH(>1) - 并发队列 - 举例:多车道

    ps: 可以把队列看成是工厂流水线,保存着需要加工的部件,线程就是完成部件的工人。一条流水线有几个部件道就是串行队列与并发队列的区别。

    三、线程死锁的原因

    先来看看这个死锁现象

    - (void)viewDidLoad {
        [super viewDidLoad];
        NSLog(@"1");
        dispatch_sync(dispatch_get_main_queue(), ^{ // 这里产生了死锁
            NSLog(@"这里不会来了");
        });
        NSLog(@"2");
    }
    

    造成线程死锁的原因:
    NSLog(@"2");的任务需要等待dispatch_sync里的任务执行完才能执行,而dispatch_sync里的任务是最后加入到主队列的,需要等待NSLog(@"2");执行完才会执行。相互等待造成死锁

    崩溃的信息也有展示出来:

    打开libdispatch源码
    搜索这个崩溃信息:__DISPATCH_WAIT_FOR_QUEUE__

    死锁崩溃信息回调函数

    可以清晰看到造成线程死锁会通过这个if条件判断,解开这个条件判断相当于看清了造成死锁崩溃的本质了。(其实这段提示信息就已经解释了线程死锁的原因:dispatch_sync called on queue already owned by current thread

    _dq_state_drain_locked_by的源码声明:

    _dq_state_drain_locked_by

    _dispatch_lock_is_locked_by的源码声明:

    _dispatch_lock_is_locked_by

    要产生死锁(这个函数返回true)必须是lock_valuetid是相等。

    造成线程死锁的总结:
    在和当前队列相关的线程 同步地 向串行队列添加任务,就会产生死锁。
    死锁的必备条件:1.线程同步 2.串行队列

    • 死锁案例:
    - (void)viewDidLoad {
        [super viewDidLoad];
        // dispatch_sync不具备开辟线程的能力,所以一直在主线程工作。
        dispatch_queue_t q = dispatch_queue_create("AnAn", DISPATCH_QUEUE_SERIAL);
        dispatch_sync(q, ^{
            NSLog(@"%@", [NSThread currentThread]); // main 主线程的环境是在q队列里
            NSLog(@"1");
            dispatch_sync(dispatch_get_main_queue(), ^{ // 死锁
                NSLog(@"2"); // 主线程的环境是在主队列里,所以死锁了
            });
            NSLog(@"3");
        });
        NSLog(@"4");
    }
    
    //  main 1 死锁
    

    四、同步函数 与 异步函数

    看同/异步函数的源码我们关注的点:
    1.任务(block)的调用时机
    2.关于线程相关的操作

    • 同步函数dispatch_sync
    dispatch_sync

    _dispatch_Block_invoke其实就是任务(block)封装成Block_layout结构体

    _dispatch_Block_invoke

    接下来需要关注_dispatch_sync_f函数的第三个参数就是我们的任务(func),它是什么时候执行的。

    _dispatch_sync_f的源码声明:

    _dispatch_sync_f

    _dispatch_sync_f_inline的源码声明:

    _dispatch_sync_f_inline

    _dispatch_sync_f_inline里面有很多个地方进行if条件判断并使用了func
    这是因为队列参数dq有四种 主队列/串行队列/并发队列/全局并发队列 导致有很多种分支

    由于libdispatch源码是没办法编译的,所以我们可以在新建工程demo,并且在使用同步函数dispatch_sync时打上符号断点,哪里使用了func就打上哪个符号,就可以拦截func在不同情况下的去了哪个分支了。(也可以使用lldb的调试命令bt看看func的去向)

    global_queue + dispatch_sync 组合为例,进行调试

    走到了_dispatch_sync_f_slow分支,再来看看这个函数的源码

    _dispatch_sync_f_slow的源码声明:

    _dispatch_sync_f_slow

    demo上继续打上这俩函数符号断点,继续走

    _dispatch_sync_function_invoke的源码声明:

    _dispatch_client_callout的源码声明:

    _dispatch_client_callout

    到这里dispatch_sync的执行就结束了,别的组合有兴趣可以自己去试试。

    回忆我们的关注点,在看dispatch_sync源码的时候,并没有发现线程相关的操作,没有发现对任务的保存操作,任务在一直传递到底层代码后,立即被执行

    dispatch_sync结论:
    同步函数dispatch_sync :立即执行、阻塞当前线程、不具备开辟子线程的能力

    • 异步函数dispatch_async
    dispatch_async
    • _dispatch_continuation_init保存任务,设置优先级

    _dispatch_continuation_init的源码声明:

    _dispatch_continuation_init

    _dispatch_continuation_init_f的源码声明:

    _dispatch_continuation_priority_set是设置优先级,直接返回了qos

    可以看到_dispatch_continuation_init并没有对线程和任务执行的操作,仅仅只是保存了任务,在需要的时候拿出来执行。

    • _dispatch_continuation_async

    _dispatch_continuation_async的源码声明:

    _dispatch_continuation_async

    dx_push是宏定义:

    #define dx_push(x, y, z) dx_vtable(x)->dq_push(x, y, z)
    

    找到dq_push的声明:

    dq_push

    根据不同的队列赋值给dq_push不一样的函数
    以并发队列为例:

    _dispatch_lane_concurrent_push的源码声明:

    _dispatch_lane_concurrent_push

    _dispatch_continuation_redirect_push的源码声明:

    这里会发现又走到了dx_push,即递归了!综合前面队列创建时可知,队列也是一个对象,有父类、根类,所以会递归执行到根类的方法。

    do_targetq是什么呢?得回到队列的创建dispatch_queue_create去查看:

    dispatch_queue_create _dispatch_lane_create_with_target

    dispatch_queue_create的时候tq就赋值出来是_dispatch_get_root_queue了。
    看看_dispatch_get_root_queue的源码声明:

    _dispatch_get_root_queue

    回到_dispatch_continuation_redirect_push上面说它递归调用了dx_push,此时它的类型却是dispatch_queue_global_t了。(dx_pushdq_push的宏定义)

    dq_push

    进去_dispatch_root_queue_push

    _dispatch_root_queue_push

    进去_dispatch_root_queue_push_inline

    _dispatch_root_queue_push_inline

    进去_dispatch_root_queue_poke

    _dispatch_root_queue_poke

    进去_dispatch_root_queue_poke_slow:

    DISPATCH_NOINLINE
    static void
    _dispatch_root_queue_poke_slow(dispatch_queue_global_t dq, int n, int floor)
    {
        int remaining = n;
        int r = ENOSYS;
    
        _dispatch_root_queues_init();//重点
        
        ...
        //do-while循环创建线程
        do {
            _dispatch_retain(dq); // released in _dispatch_worker_thread
            while ((r = pthread_create(pthr, attr, _dispatch_worker_thread, dq))) {
                if (r != EAGAIN) {
                    (void)dispatch_assume_zero(r);
                }
                _dispatch_temporary_resource_shortage();
            }
        } while (--remaining);
        ...
    }
    

    走到了这里就进行了线程的操作啦。

    分析一下:_dispatch_root_queues_init

    DISPATCH_STATIC_GLOBAL(dispatch_once_t _dispatch_root_queues_pred);
    DISPATCH_ALWAYS_INLINE
    static inline void
    _dispatch_root_queues_init(void)
    {
        dispatch_once_f(&_dispatch_root_queues_pred, NULL,
                _dispatch_root_queues_init_once);
    }
    

    发现是一个dispatch_once_f单例(下面会介绍单例),其中传入的func_dispatch_root_queues_init_once

    综上所述,异步函数dispatch_async的底层分析如下:

    【准备工作】:首先,将异步任务拷贝并封装,并设置回调函数func
    【block回调】:底层通过dx_push递归,会重定向到根队列,然后通过pthread_creat创建线程,最后通过dx_invoke执行block回调(注意dx_pushdx_invoke 是成对的)。

    总结 dispatch_async子线程创建的调用流程:
    1.dispatch_async -> _dispatch_continuation_async -> dx_push -> dq_push -> 并发队列:_dispatch_lane_concurrent_push -> _dispatch_continuation_redirect_push

    2._dispatch_continuation_redirect_push -> dx_push(此时是global_queue) ->_dispatch_root_queue_push -> _dispatch_root_queue_push_inline -> _dispatch_root_queue_poke -> _dispatch_root_queue_poke_slow -> 线程池调度,创建线程pthread_create


    总结同/异步函数特性:
    同步函数dispatch_sync :
    1. 阻塞当前线程进⾏等待,直到当前添加到队列的任务执⾏完成;
    2. 只能在当前线程执⾏任务,不具备开启新线程的能⼒。

    异步函数dispatch_async:
    1. 不会阻塞线程,不需要等待,任务可以继续执⾏;
    2. 可以在新的线程执⾏任务,具备开启新线程的能⼒。(并发队列可以开启多条⼦线程,串⾏队列只能开启⼀条⼦线程)

    五、面试题

    ps: 注意要考虑任务复杂度

    - (void)test1 {
        dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT); // 并发队列
        NSLog(@"1");
        dispatch_async(queue, ^{
            // sleep(2);
            NSLog(@"2");
            dispatch_sync(queue, ^{
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        //sleep(2);
        NSLog(@"5");
    }
    // 1最前面  2在3前面 3在4前面  2和5没有顺序
    
    
    
    - (void)test2 {
        dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_SERIAL); // 串行队列
        NSLog(@"1");
        dispatch_async(queue, ^{
            NSLog(@"2");
            dispatch_sync(queue, ^{ // 这里死锁
                NSLog(@"3");
            });
            NSLog(@"4");
        });
        NSLog(@"5");
    }
    // 1最先 2和5没有顺序 死锁
    
    
    - (void)test3 {
        dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_SERIAL); // 串行队列
        dispatch_async(queue, ^{
            NSLog(@"1");
            dispatch_async(queue, ^{
                NSLog(@"2");
            });
            NSLog(@"3");
        });
    
        // sleep(3); // 万一主线程这里复杂操作呢,把下面的任务延迟添加到queue队列
        dispatch_async(queue, ^{
            // sleep(3);
            NSLog(@"4");
        });
    }
    // 13   4和2没有顺序
    
    
    - (void)test4 {
        self.num = 0;
        while (self.num < 100) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                self.num ++;
            });
        }
        NSLog(@"self.num = %d",self.num);
    }
    // 比100大一点
    
    
    - (void)test5 {
        self.num = 0;
        for (int i = 0; i < 100; i ++) {
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                self.num ++;
            });
        }
        NSLog(@"self.num = %d",self.num);
    }
    // 0-100的其中一个数
    
    
    
    -(void)test6 {
        dispatch_queue_t queue = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
        dispatch_async(queue, ^{
            NSLog(@"1");
        });
        dispatch_async(queue, ^{
            NSLog(@"2");
        });
        // 阻塞主线程
        dispatch_sync(queue, ^{
            // sleep(2);  // 主线程
            NSLog(@"3");
        });
        // sleep(2);
        NSLog(@"0");
        dispatch_async(queue, ^{
            NSLog(@"7");
        });
        dispatch_async(queue, ^{
            NSLog(@"8");
        });
        dispatch_async(queue, ^{
            NSLog(@"9");
        });
    }
    // 12789在子线程;30在主线程;3一定在0之前执行;789一定在30的后面执行
    
    
    -(void)test7 {
        dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"1");
        dispatch_sync(t, ^{
            NSLog(@"2");
            dispatch_async(t, ^{
                //sleep(2);
                NSLog(@"3");
            });
            // sleep(2);
            NSLog(@"4");
        });
        //sleep(2);
        NSLog(@"5");
    }
    // 12一定先 5一定在4后面 3和5顺序不一定 3和4顺序不一定
    
    
    -(void)test8 {
        dispatch_queue_t t = dispatch_queue_create("AnAn", DISPATCH_QUEUE_CONCURRENT);
        NSLog(@"1");
        dispatch_async(t, ^{
    //        sleep(2);
            NSLog(@"2");
            dispatch_sync(t, ^{
    //            sleep(2);
                NSLog(@"3");
            });
    //        sleep(2);
            NSLog(@"4");
        });
    //    sleep(2);
        NSLog(@"5");
    }
    // 125顺序不一定;3一定在2之后;4一定在3之后
    
    
    -(void)test9 {
        dispatch_queue_t t = dispatch_queue_create("lg", DISPATCH_QUEUE_SERIAL);
        NSLog(@"1");
        dispatch_sync(t, ^{
            // 在主线程
            NSLog(@"2");
            dispatch_async(t, ^{
    //            sleep(2); // 子线程
                NSLog(@"3");
            });
            sleep(2);
            NSLog(@"4");
        });
    //    sleep(2);
        NSLog(@"5");
    }
    // 124一定先 35顺序不一定
    

    六、自定义线程池思想

    通过分析YYKit的线程池进行分析自己构造一个线程池的思想。准确来说YYKit的线程池应该被叫做是队列池。

    核心思想:创建一个串行队列数组,数组里的每一个队列都进行异步操作任务,串行+异步=开辟一条线程的能力。每次需要完成任务时,从数组中轮询获取队列进行异步操作。

    来看看YYKit源码是如何实现一个线程池的。

    准确来说不是创建线程,是从系统线程池的64条线程中拿到对应个数的线程。

    相关文章

      网友评论

        本文标题:iOS 多线程原理 - 线程与队列底层

        本文链接:https://www.haomeiwen.com/subject/hnqrprtx.html