线程开的越多就越好吗|趣谈线程池

前言

线程优化一直是启动优化中的一个必不可少的项目。作为一个 Android 程序员,你肯定希望应用启动的时候,火力全开,线程池拉满,每一个 CPU 核心满载而行。

可你把线程池拉满的时候,启动时长就一定会降低吗?

结果显然是否定的,之前我在进行启动优化的时候,就遇到了类似的问题。我引入了有向无环图类似的启动库后,又将线程池的数量设置为:

CPU核心数 * 2 + 1

看似没什么问题,后续启动时长居然还增长了一点点。

摆烂

为什么会出现这样的问题?我们今天就好好聊聊。

一、做个实验

先做个实验,在应用启动过程中,主要做了两步:

  • 主线程循环 10w 次,做一些简单的计算
  • 线程池做一些异步任务,读取文件,然后将读取到的数据写入数据库,这个异步任务提交了 1000 次

核心线程数 = 2 * CPU 核心数 + 1,变量最大线程数:

  1. 实验一:最大线程数 = 2 * CPU 核心数 + 1
  2. 实验二:最大线程数 = Int.MAX_VALUE

在模拟器上,实验二平均启动时长 6505 ms,实验一平均启动市场 5521 ms,从这点看,线程开太多对主线程是有影响的。

二、基础知识

在启动流程中基础知识必不可少,从上往下讲就是线程、线程池、内核 和 CPU,这些知识都是老生常谈了。

1. 线程

线程是操作系统进行运算调度的最小单位,可以理解为它就是系统执行的任务。

作为任务,它会有各种状态:

  1. NEW(新建):新创建的线程,还没有启动
  2. RUNNABLE(可运行):可以运行的线程
  3. BLOCKED(阻塞):阻塞状态的线程
  4. WAITTING(等待):等待状态
  5. TIME_WAITTING(计时等待)
  6. TERMINATED(终止)

各种状态可以进行如下转换:

MainGroup使用流程

处于可运行状态的线程不一定处于运行中,如果 CPU 核心数 < 线程数量,在某个时间点,处于运行中的线程数量最多也只能等于 CPU 核心数。

除此以外,只有处于可运行状态的线程才有机会获取 CPU 的青睐,从而分到时间片,得以执行。

2. 线程池

线程池的知识都很熟悉了,简单了解一下。

2.1 核心线程

简单来说,我们想了解的部分就是线程池的核心线程和非核心线程:

  • 核心线程:核心线程会一直存在
  • 非核心线程:当非核心线程闲置超过指定的时间,就会被销毁

通过配置合适的核心线程数和非核心线程数可以帮助我们管理好线程,可以带来以下好处:

  1. 降低资源消耗:重复利用线程,降低资源消耗
  2. 提供响应速度:任务一来就执行
  3. 管理好线程资源:避免无节制的使用线程,引发性能问题

除此以外,在配置核心线程数和非核心线程数的时候,还需要根据业务场景,将 CPU 密集型和 I/O 密集型任务考虑进去。

2.2 任务划分

我们经常将任务分为 I/O密集型 和 CPU密集型 任务,那么这两种有什么区别呢?

I/O 密集型任务指的是该任务的大部分时间用来提交 I/O 请求或者等待 I/O 请求。这类任务常常运行很短暂的一会儿,然后进入阻塞状态,等待更多的 I/O 请求。常见的如数据库操作、网络操作、键盘事件、屏幕操作等。

CPU 密集型任务指的是任务的大部分代码用来执行代码。该类任务常常会一直运行并占用着 CPU,直到时间片用完。常见的如数据计算、无限循环等。

那线程数如何设置?我们下面再去讲。

3. 内核

哪个线程先运行?什么时间运行?运行多久?这些都是调度程序说了算!

3.1 调度程序

调度程序是一个内核子系统,它是多任务操作系统的基础。多任务操作系统就是能够同时并发地交互执行多个进程的操作系统。

即使是单核处理器,它也可以并发的处理多个任务,只不过在一个时间点,只有一个正在执行的任务。

就好比安卓开发小王,身背几个需求,被产品要求同一天上线,虽然也能够完成,但他在某个时间点,只能写一个需求,如果想一个时间点同时进行两个需求,那得加人,也就是我们通常说的双核处理器,这就具备了并行的能力。

并行和并发

3.2 抢占式和非抢占式

多任务操作系统可以分为两种类型:非抢占式多任务和抢占式多任务。

Android 使用的是抢占式多任务,在这种模式下,每个任务都会被分配到一定的时间用来执行,一旦时间片用完,就会自动切换到下一个任务,分配的时间我们称之为时间片。

还拿小王来举例,小王身背三个需求,每天的计划中,上午需求 A,下午需求 B,晚上需求C。到了下午,即使需求 A 没做完,也要去做需求 B,这样可以保证了每个需求每天都会有进度。

从启动的角度来说,我们肯定不希望主线程和子线程分得同样的时间片,这可能会让我们的应用看着很慢。

为了给主线程分得更长的时间片,每个进程都有一个 nice 值,它会影响时间片的分配,但我们改不了这个,我们能够处理的就是给线程设置优先级,Android 中线程的优先级从 -19 到 19,值越低代表优先级越高,分得的时间片也就越长。

3.3 线程多了会怎么分配

上面的这些东西看似和我们应用层开发没关系,实则不然。

比如线程数量多了以后,我们先拿小王举例:

原先小王手里有 5 个需求,每个 2 天工时,做完一个再做下一个,10天能搞定。

现经理要求他同时开发 5 个需求,保证 5 个需求每天都有进度,那可就麻烦了,先不算 10 天开发时间,还得加上如下时间:

  • 每天切其他四个项目时间成本
  • 思考时间:每次切到下一个项目,都会想上次开发到哪,上次的思路是什么

加上这些乱七八槽的,原来 10 天能搞定的东西,现在得变成 12 天。

线程多了,也会有这样的问题,每次切换时间片都是成本。另外,线程的闲置率会上升,像这样运行 14ms 要等 185 ms:

idle率

还拿小王来看,原先五个需求,桉顺序做,每个需求的生命周期就 2 天,但是并行开发后,每个需求的生命周期都拉长了,到了 12 天左右。对于启动的主线程来讲可不是好事!

理想的情况应该是量力而行,当小王开发一个需求遇到问题需要等产品回复而停滞,在等待的这段时间内,开发另外一个需求,知道产品回复完,再找一个合适的时间切回来,这样,反而会提升效率,将工作时间缩短到 9 天。

4. CPU

在2022年发布的 Android 低端机上,也都标配了 8 核心的 CPU,核心数越多,就意味着并行能力越强。

注意,这里用的是并行,而不是并发。

专业团队

一个核心,就代表着团队只有一个开发,8 核代表着团队有八个开发,意味着一个时间点最高可以有8个需求同时进行。

二、线程数如何设置

上面说了那么多,大家最想知道的就是线程数如何设置。

一般而言,核心线程数和最大线程数都设置为 CPU核心数 * 2 +1 ,阻塞队列使用 LinkedBlockingDeque

1. 任务因素

但这个数字肯定不是绝对的,我们需要考虑到 CPU 密集型任务 和 IO 密集型任务的区别。

如果我们使用子线程都是处理网络、数据库、读文件等操作,这个数字就可以设置大一点;如果子线程仅执行一些耗时的计算代码,这个数字就可以设置小一点。

2. 任务闲置

即使我们自己设置的线程池没什么问题,但程序一启动,任务执行时候的线程闲置率一看就知道还有问题,比如这张图:

idle率

为什么会出现这种闲置率太高的情况,原因可能如下:

  1. 过多使用 New Thread 或者不节制的使用线程池
  2. 很多第三方 SDK 都使用自身的线程池或者线程

查看闲置率有两种,分别是使用Android Studio中的Profiler和Shell命令。

推荐大家使用 Profiler,好处可太多了:

  • 可以查看线程总数
  • 可以查看CPU的负载情况
  • 可以查看每个任务的闲置率
  • ...

直接使用 Profiler 中的 System TraceView 只能查看系统级别的方法,如果是我们想查看的方法,需要这么处理:

public void test{
    Trace.beginSection("名称");
    //... 代码省略
    Trace.endSection();
}

对每个方法做上述过程确实太麻烦,所以都是配合函数插桩使用。

另外一个就是使用 Shell 命令,我们可以在 Android Studio 中 Logcat 窗口看到应用的进程 Id,进入 adb shell 后,就可以通过输入命令 cat /proc/{进程ID}/schedstat 查看:

emulator64_x86_64_arm64:/ $ cat /proc/7775/schedstat
5511910111 2055599424 6712
// 参数一 CPU运行时间
// 参数二 该进程等待时间
// 参数三 主动切换和被动切换的次数

这些数据只能够我们查看大概的情况。

总结

关于线程我们能做的并不多,尽量去收敛线程:

  1. 禁止使用 New Thread 方式去创建线程
  2. 统一应用内线程池,并制定合适的核心线程和最大线程数量
  3. 编写公司库的时候,如需使用线程池,提供设置线程池的接口
  4. 可以设置自身线程池的第三方库,优先设置应用内线程池,比如 OkHttp
  5. Hook 第三方库使用 New Thread,改为应用内线程池
  6. 能懒加载尽量懒加载第三方库,避免过早的竞争系统资源

主要就这些,如有不对的地方,评论区见~

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

推荐阅读更多精彩内容