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

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条线程中拿到对应个数的线程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容