Is it better to have more threads open | Interesting thread pool

foreword

Thread optimization has always been an essential item in startup optimization. As an Android programmer, you definitely hope that when the application starts, the firepower is full, the thread pool is full, and each CPU core is fully loaded.

But when you fill up the thread pool, will the startup time be reduced?

The result is obviously negative. I encountered a similar problem when I was doing startup optimization before. After I introduced a DAG-like startup library, I set the number of thread pools to:

Number of CPU cores * 2 + 1

It seems that there is no problem, and the subsequent startup time actually increases a little bit.

rotten

Why does such a problem occur? Let's have a good chat today.

1. Do an experiment

Let's do an experiment first. During the application startup process, we mainly do two steps:

  • The main thread loops 10w times and does some simple calculations
  • The thread pool does some asynchronous tasks, reads the file, and then writes the read data to the database. This asynchronous task is submitted 1000 times

Number of core threads = 2 * CPU 核心数 + 1, variable maximum number of threads:

  1. Experiment 1: Maximum number of threads =2 * CPU 核心数 + 1
  2. Experiment 2: Maximum number of threads = Int.MAX_VALUE

On the simulator, the average startup time of experiment 2 is 6505 ms, and the average startup time of experiment 1 is 5521 ms. From this point of view, too many threads have an impact on the main thread.

2. Basic knowledge

Basic knowledge is essential in the startup process. From top to bottom, threads, thread pools, cores, and CPUs are all commonplace.

1. Thread

A thread is the smallest unit of operation scheduling by the operating system, which can be understood as a task performed by the system.

As a task, it can have various states:

  1. NEW (new): a newly created thread, not yet started
  2. RUNNABLE: A thread that can be run
  3. BLOCKED (blocked): a thread in a blocked state
  4. WAITTING: waiting state
  5. TIME_WAITTING (time waiting)
  6. TERMINATED (terminated)

The various states can be transitioned as follows:

MainGroup usage process

Threads in the runnable state are not necessarily running. If the number of CPU cores < the number of threads, at a certain point in time, the number of running threads can only be equal to the number of CPU cores at most.

In addition, only threads in the runnable state have the opportunity to gain the favor of the CPU, so that they can be allocated time slices and executed.

2. Thread pool

The knowledge of thread pools is very familiar, let's take a brief look.

2.1 Core thread

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

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

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

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

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

2.2 任务划分

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

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

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

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

3. 内核

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

3.1 调度程序

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

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

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

Parallelism and Concurrency

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 rate

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

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

4. CPU

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

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

professional team

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

二、线程数如何设置

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

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

1. 任务因素

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

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

2. 任务闲置

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

idle rate

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

  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. Unify in-app thread pools and formulate appropriate core threads and maximum number of threads
  3. When writing a company library, if you need to use a thread pool, provide an interface for setting the thread pool
  4. A third-party library that can set its own thread pool, preferably the in-app thread pool, such as OkHttp
  5. Hook used by third-party libraries New Thread, changed to in-app thread pool
  6. Can lazy load third-party libraries as lazy as possible to avoid premature competition for system resources

These are the main things, if there is something wrong, see you in the comment area~

Guess you like

Origin juejin.im/post/7118911405759627272