如何区分IO密集型、CPU密集型任务?_内核态

前言

日常开发中,我们时常会听到什么IO密集型、CPU密集型任务...

那么这里提一个问题:大家知道什么样的任务或者代码会被认定为IO/CPU密集?又是用什么样的标准来认定IO/CPU密集?

如果你没有明确的答案,那么就随着这篇文章一起来聊一聊吧。

正文

最近团队里有基础技术的同学对项目中的线程池进行了重新设计,调整了IO线程池等线程池的优化。因此借助这个机会也就了解了一波开篇的那些问题。

一、宏观概念区分

这一部分经验丰富的同学都很熟悉。比如:

1.1、IO密集型任务

一般来说:文件读写、DB读写、网络请求等

1.2、CPU密集型任务

一般来说:计算型代码、Bitmap转换、Gson转换等

二、用代码区分

上一part都是咱们凭借经验划分的,这一part咱们就来用正经的指标来划分任务。

先看有哪些数据指标可以用来进行评估(以下方法以系统日志为准,加之开发经验为辅):

1. wallTime

任务的整体运行时长(包括了running + runnable + sleep等所有时长)。获取方案:

  1. run() {
  2. long start = System.currentTimeMillis();
  3. // 业务代码
  4. long wallTime = System.currentTimeMillis() - start;
  5. }

2. cpuTime

cputime是任务真正在cpu上跑的时长,即为running时长

获取方案1:

  1. run() {
  2. long start = SystemClock.currentThreadTimeMillis();
  3. // 业务代码
  4. long cpuTime = SystemClock.currentThreadTimeMillis() - start;
  5. }

获取方案2:

  1. /proc/pid/task/tid/sched

  2. se.sum_exec_runtime CPU上的运行时长

如何区分IO密集型、CPU密集型任务?_内核态_02

如何区分IO密集型、CPU密集型任务?_字段_03

3. iowait time/count

指线程的iowait耗时。获取方案:

  1. /proc/pid/task/tid/sched

  2. se.statistics.iowait_sum IO等待累计时间
  3. se.statistics.iowait_count IO等待累计次数

具体日志位置同上

4. runnable time

线程runnabel被调度的时长。获取方案:

  1. /proc/pid/task/tid/sched

  2. se.statistics.wait_sum 就绪队列等待累计时间

具体日志位置同上

5. sleep time

线程阻塞时长(包括Interruptible-sleep和Uninterruptible-sleep和iowait的时长)。获取方案:

  1. /proc/pid/task/tid/sched

  2. se.statistics.sum_sleep_runtime 阻塞累计时间

具体日志位置同上

6. utime/stime

utime是线程在用户态运行时长,stime是线程在内核态运行时长。获取方案:

  1. /proc/pid/task/tid/stat

  2. 14个字段是utime,第15个字段是stime

如何区分IO密集型、CPU密集型任务?_字段_04

7. rchar/wchar

wchar是write和pwrite函数写入的byte数。获取方案:

  1. /proc/pid/task/tid/io

  2. rchar: ...
  3. wchar: ...

(没找到合适的日志,暂不讨论此情况)基于读写char数,我们可以将IO细分成读IO密集型写IO密集型

8. page_fault

缺页中断次数,分为major/minor fault。获取方案:

  1. /proc/pid/task/tid/stat

  2. 10个字段是minor_fault,第12个字段是major_fault

如何区分IO密集型、CPU密集型任务?_线程池_05

9. ctx_switches

线程在用户/内核态的切换次数,分为voluntary和involuntary两种切换。获取方案:

  1. /proc/pid/task/tid/sched

  2. nr_switches 总共切换次数
  3. nr_voluntary_switches 自愿切换次数
  4. nr_involuntary_switches 非自愿切换次数

日志位置同上

10. percpuload

平均每个cpu的执行时长。获取方案:

  1. /proc/pid/task/tid/sched

  2. avg_per_cpu

日志位置同上

有了上述这些指标,我们就可以开始我们的任务确定了。

以下内容,大家可以自行测试加深印象。

2.1、IO密集型任务

比如这段代码:

  1. val br = BufferedReader(FileReader("xxxx"), 1024)

  2. try {
  3. while (br.readLine() != null) {}
  4. } finally {
  5. if (br != null) {
  6. br.close()
  7. }
  8. }

基于上述部分3. iowait time/count,我们可以在对应的日志文件中看出这段代码有明显的iowait

2.2、CPU密集型任务

比如这段代码:

  1. var n = 0.0
  2. for (i in 0..9999999) {
  3. n = Math.cos(i.toDouble())
  4. }

基于上述部分6. utime/stime的内容,看一看出这段代码utime会占比非常高,且几乎没有stime,此外没有io相关的耗时。

三、这玩意有啥用?

说白了,我们一切的优化手段都是为了服务于业务。对于业务开发来说:

为了不占用主线程 -> 所以启一个新线程 -> 频繁的new线程又会带来大量的开销 -> 所以使用线程池进行复用 -> 而不合理的线程池设计又会带来线程使用低效,甚至新加入的任务只能等待 -> 优化线程池

举个最简单的例子:线程池中放了最大允许俩个线程并行,那么假设运行中的俩个都是长IO的任务。那么新来的任务就只能等,哪怕它并不是特别耗时...

因此这玩意有啥用,还不是为更好的线程池设计做指导思想,更好的提升线程运行效率,降低业务上不必要的等待。

这里提供一些可供参考的工具方法和线程池设计:

3.1、判断任务类型

这里贴一些核心的思路,毕竟全部方案数据公司的代码,我也不方便全部贴出来:

  1. class TaskInfo {
  2. var cpuTimeStamp = 0.0
  3. var timeStamp = 0.0
  4. var iowaitTime = 0.0
  5. var sleepTime = 0.0
  6. var runnableTime = 0.0
  7. var totalSwitches = 0.0
  8. var voluntarySwitches = 0.0
  9. }
  10. object TaskInfoUtils {
  11. private const val SUM_SLEEP_RUNTIME = "se.statistics.sum_sleep_runtime"
  12. private const val WAIT_SUM = "se.statistics.wait_sum"
  13. private const val IOWAIT_SUM = "se.statistics.iowait_sum"
  14. private const val NR_SWITCHES = "nr_switches "
  15. private const val NR_VOLUNTARY_SWITCHES = "nr_voluntary_switches"

  16. private var schedPath = ThreadLocal<String>()

  17. fun buildCurTaskInfo(): TaskInfo {
  18. val threadInfo = TaskInfo()

  19. threadInfo.timeStamp = System.currentTimeMillis().toDouble()
  20. threadInfo.cpuTimeStamp = SystemClock.currentThreadTimeMillis().toDouble()

  21. if (schedPath.get() == null) {
  22. schedPath.set("/proc/${android.os.Process.myPid()}/task/${getTid()}/sched")
  23. }
  24. BufferedReader(FileReader(schedPath.get()), READ_BUFFER_SIZE).use { br ->
  25. br.readLines().forEach { line ->
  26. when {
  27. line.startsWith(SUM_SLEEP_RUNTIME) -> threadInfo.sleepTime = line.split(":")[1].toDouble()
  28. line.startsWith(WAIT_SUM) -> threadInfo.runnableTime = line.split(":")[1].toDouble()
  29. line.startsWith(IOWAIT_SUM) -> threadInfo.iowaitTime = line.split(":")[1].toDouble()
  30. line.startsWith(NR_SWITCHES) -> threadInfo.totalSwitches = line.split(":")[1].toDouble()
  31. line.startsWith(NR_VOLUNTARY_SWITCHES) -> threadInfo.voluntarySwitches = line.split(":")[1].toDouble()
  32. }
  33. }
  34. }
  35. return threadInfo
  36. }
  37. }
  38. object TaskBoundJudge {
  39. private const val CPU_CPUTIME_INTERVAL = 0.8
  40. private const val CPU_SWITCHES_INTERVAL = 0.1
  41. private const val CPU_IOWAIT_INTERVAL = 0.01
  42. private const val CPU_SLEEP_INTERVAL = 0.02
  43. private const val CPU_CPUTIME_WEIGHTS = 0.1
  44. private const val CPU_SWITCHES_WEIGHTS = 0.35
  45. private const val CPU_IOWAIT_WEIGHTS = 0.15
  46. private const val CPU_SLEEP_WEIGHTS = 0.40

  47. private const val IO_CPUTIME_INTERVAL = 0.5
  48. private const val IO_SWITCHES_INTERVAL = 0.4
  49. private const val IO_IOWAIT_INTERVAL = 0.1
  50. private const val IO_SLEEP_INTERVAL = 0.15
  51. private const val IO_CPUTIME_WEIGHTS = 0.1
  52. private const val IO_SWITCHES_WEIGHTS = 0.35
  53. private const val IO_IOWAIT_WEIGHTS = 0.35
  54. private const val IO_SLEEP_WEIGHTS = 0.2

  55. fun isCpuTask(start: TaskInfo?, end: TaskInfo?): Boolean {
  56. if (start == null || end == null) {
  57. return false
  58. }
  59. val wallTime = end.timeStamp - start.timeStamp
  60. val cpuTime = end.cpuTimeStamp - start.cpuTimeStamp
  61. val runnableTime = end.runnableTime - start.runnableTime
  62. val totalSwitches = end.totalSwitches - start.totalSwitches
  63. val voluntarySwitches = end.voluntarySwitches - start.voluntarySwitches
  64. val iowaitTime = end.iowaitTime - start.iowaitTime
  65. val sleepTime = end.sleepTime - start.sleepTime
  66. var result = 0.0
  67. if (cpuTime / (wallTime - runnableTime) > CPU_CPUTIME_INTERVAL) {
  68. result += CPU_CPUTIME_WEIGHTS
  69. }
  70. if (voluntarySwitches / totalSwitches < CPU_SWITCHES_INTERVAL) {
  71. result += CPU_SWITCHES_WEIGHTS
  72. }
  73. if (iowaitTime / sleepTime < CPU_IOWAIT_INTERVAL) {
  74. result += CPU_IOWAIT_WEIGHTS
  75. }
  76. if (sleepTime / (wallTime - runnableTime) < CPU_SLEEP_INTERVAL) {
  77. result += CPU_SLEEP_WEIGHTS
  78. }
  79. return result > 0.5
  80. }

  81. fun isIOTask(start: TaskInfo?, end: TaskInfo?): Boolean {
  82. if (start == null || end == null) {
  83. return false
  84. }
  85. val wallTime = end.timeStamp - start.timeStamp
  86. val cpuTime = end.cpuTimeStamp - start.cpuTimeStamp
  87. val runnableTime = end.runnableTime - start.runnableTime
  88. val totalSwitches = end.totalSwitches - start.totalSwitches
  89. val voluntarySwitches = end.voluntarySwitches - start.voluntarySwitches
  90. val iowaitTime = end.iowaitTime - start.iowaitTime
  91. val sleepTime = end.sleepTime - start.sleepTime

  92. var result = 0.0
  93. if (cpuTime / (wallTime - runnableTime) < IO_CPUTIME_INTERVAL) {
  94. result += IO_CPUTIME_WEIGHTS
  95. }
  96. if (voluntarySwitches / totalSwitches > IO_SWITCHES_INTERVAL) {
  97. result += IO_SWITCHES_WEIGHTS
  98. }
  99. if (iowaitTime / sleepTime > IO_IOWAIT_INTERVAL) {
  100. result += IO_IOWAIT_WEIGHTS
  101. }
  102. if (sleepTime / (wallTime - runnableTime) > IO_SLEEP_INTERVAL) {
  103. result += IO_SLEEP_WEIGHTS
  104. }
  105. return result > 0.5
  106. }
  107. }

当我们想对某个方法进行计算是CPU还是IO。可以在这个方法的开始、结束调用 ​TaskInfoUtils.buildCurTaskInfo()​​;然后调用 ​TaskBoundJudge.isCpuTask(start,end)​​, ​TaskBoundJudge.isIOTask(start,end)​即可。

3.2、线程池

IO密集型参考线程池:

  1. public static final ExecutorService IO_EXECUTOR = new ThreadPoolExecutor(
  2. 2,
  3. 128,
  4. 15,
  5. TimeUnit.SECONDS,
  6. new SynchronousQueue<>(),
  7. new CustomThreadFactory("MDove-IO", CustomThreadPriority.NORMAL),
  8. AbortPolicy() // 根据业务情况,自行定义拒绝实现。比如上报监控平台
  9. );

CPU密集型参考线程池:

  1. public static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
  2. public static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
  3. private static final int CPU_CORE_POOL_SIZE = Math.max(Math.min(MAXIMUM_POOL_SIZE, 4), Math.min(CPU_COUNT + 1, 9));

  4. public static final ExecutorService CPU_EXECUTOR = new ThreadPoolExecutor(
  5. CPU_CORE_POOL_SIZE,
  6. CPU_COUNT * 2 + 1,
  7. 30,
  8. TimeUnit.SECONDS,
  9. new LinkedBlockingQueue<>(256),
  10. new SSThreadFactory("MDove-CPU", CustomThreadPriority.NORMAL),
  11. AbortPolicy() // 根据业务情况,自行定义拒绝实现。比如上报监控平台
  12. );

上述线程池中设计的额外代码:

  1. class CustomThreadFactory : ThreadFactory {
  2. var name: String
  3. private set
  4. private var priority = CustomThreadPriority.NORMAL

  5. constructor(name: String, priority: CustomThreadPriority) {
  6. this.name = name
  7. this.priority = priority
  8. }

  9. override fun newThread(r: Runnable): Thread {
  10. val name = name + "-" + sCount.incrementAndGet()
  11. return object : Thread(r, name) {
  12. override fun run() {
  13. if (priority == CustomThreadPriority.LOW) {
  14. Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)
  15. } else if (priority == CustomThreadPriority.HIGH) {
  16. Process.setThreadPriority(Process.THREAD_PRIORITY_DISPLAY)
  17. }
  18. super.run()
  19. }
  20. }
  21. }

  22. companion object {
  23. private val sCount = AtomicInteger(0)
  24. }
  25. }

  26. enum class CustomThreadPriority {
  27. LOW, NORMAL, HIGH, IMMEDIATE
  28. }

尾声

OK,这篇文章到这里就结束了。希望这篇文章能给大家在线程的使用线程池的设计上带来帮助。

最后,让我们一起加油吧,“打工人”!