欢迎光临
我们一直在努力

并发编程(一):多线程与上下文切换,从 “什么时候用” 到 “隐形消耗”

今天咱们开个新坑,聊聊并发编程。

很多人一上来就觉得 “多线程 = 快”,恨不得把所有代码都写成多线程,结果线上一跑,不仅没快,反而更慢了,还多了一堆莫名其妙的问题。

今天咱们就用大白话讲一讲 “多线程到底什么时候用”“用多少线程才合适”,以及它背后的 “隐形杀手”—— 上下文切换。


一、多线程到底是干嘛的?

简单来说:

  • 你是一个老板,手下有个工人(单线程),他干活很卖力,但中间经常要等材料(比如等网络请求、等硬盘 IO)。
  • 等材料的时候,他就站那儿摸鱼,啥也不干,你付着工资,他却在发呆。
  • 这时候,你再雇几个工人(多线程),第一个工人等材料的时候,第二个、第三个工人就可以接着干别的活,这样整体效率就上去了。

多线程的本质,就是利用 “等待时间”,让 CPU 别闲着。

它解决的核心问题是:CPU 浪费比例严重。


二、关键指标:CPU 浪费比例

什么是 “CPU 浪费比例”?

就是 “等待时间” 和 “干活时间” 的比值。

CPU浪费比例 = 等待时间 / 干活时间

  • 如果一个任务,干活 2ms,等待 42ms,那浪费比例就是 42 / 2 = 21。
  • 这意味着,CPU 有 21 倍的时间在摸鱼,这时候开多线程,收益巨大。
  • 如果一个任务,干活 2s,等待 1s,浪费比例是 0.5,这时候开多线程,收益就很小,甚至可能因为线程切换,反而更慢。

记住:多线程只适合 “CPU 浪费比例> 1” 的场景。 不是 “等待时间长” 就适合,而是 “等待时间远大于干活时间” 才适合。


三、线程数量怎么算?

很多人会问:“我开多少线程合适?”

答案是:看摸鱼时间。

还是刚才的例子:

  • 干活:2ms
  • 摸鱼:42ms
  • 总耗时:44ms

如果只开 1 个线程,那它 42ms 在摸鱼,2ms 在干活,效率极低。

如果开 22 个线程:

  • 每个线程摸鱼 42ms,干活 2ms。
  • 当第 1 个线程开始摸鱼时,第 2 个线程就可以开始干活。
  • 这样,CPU 几乎全程都在干活,没有浪费。

线程数 ≈ (等待时间 / 干活时间) + 1

这个公式不是绝对的,但它给了我们一个非常重要的直觉:线程数是由 “等待时间” 决定的,不是越多越好。


四、反例:不是所有场景都适合多线程

咱们再看一个反例:处理图片。

  • 任务:从网络下载一张图片(等待 1s),然后对图片进行压缩(干活 2s)。
  • 浪费比例:1 / 2 = 0.5。

这时候,如果你开 10 个线程:

  • 每个线程都要先等 1s,再干 2s。
  • 线程之间频繁切换,光 “换岗” 就耗了不少时间。
  • 最终总耗时,可能比单线程还要长。

这就像:

  • 雇 10 个工人干 1 个活,大家轮流上,光协调、排队的时间,比干活时间还长。

所以,多线程滥用只会自讨苦吃。


五、多线程的 “副作用”:上下文切换

当我们开了多线程,让 CPU “忙起来” 的时候,一个新的问题出现了:上下文切换。

咱们还是用工人的例子来理解:

  • 现在你有 22 个工人,但只有一个工作台(CPU 核心)。
  • 大家轮流上,每个人干一会儿,就换下一个。
  • 每次换人,都要把上一个工人的 “干活进度”(比如算到哪一步了、手里拿了什么工具)记下来,再把下一个工人的进度恢复。
  • 这个 “记下来、再恢复” 的过程,就是上下文切换。

1. 上下文切换的“开销”有多大?

在现代 CPU 上,一次上下文切换的时间大约是不到 1 毫秒。

听起来不多,但如果每秒切换 1000 多次,累积起来就是:

  • 1000 次 / 秒 × 1ms / 次 = 1 秒 / 秒
  • 这意味着,CPU 一半的时间都在 “换岗”,而不是在 “干活”。

这就是多线程的 “隐形消耗”。

2. 上下文切换不是越少越好!

很多人一看到切换开销,就想:“那我少开点线程,切换不就少了吗?”

这又走进了另一个误区:

  • 如果线程太少,CPU 又会回到 “摸鱼” 的状态,浪费大量时间。
  • 如果线程太多,切换太频繁,CPU 又会把时间都花在 “换岗” 上。

所以,线程数量和上下文切换次数需要达到一个平衡,才能达到最优性能。

  • 线程太少 → CPU 浪费比例增高,整体变慢。
  • 线程太多 → 上下文切换频繁,整体变慢。

性能最佳的核心,是线程数量合适,而不是上下文切换次数最少。


六、如何减少不必要的上下文切换?

既然切换是必要的,但又不能太多,我们该怎么办?

核心思路是:减少线程的 “无效等待”。

  • 使用线程池:复用线程,避免频繁创建和销毁线程,减少切换。
  • 避免锁竞争:锁竞争会导致线程阻塞,频繁从阻塞队列到就绪队列的切换。
  • 使用无锁编程:比如 CAS、ThreadLocal,从根源上消除锁竞争。
  • 控制线程数量:根据 “CPU 浪费比例” 计算合适的线程数,不要盲目开多。

  • 七、总结:多线程的 “适用边界” 与 “平衡之道”

    最后,咱们用几句话总结一下今天的核心内容:

  • 多线程的本质:利用 “等待时间”,让 CPU 别闲着。
  • 适用场景:CPU 浪费比例严重(等待时间远大于干活时间),比如网络 IO、数据库操作、文件读写。
  • 线程数量:由 “等待时间” 和 “干活时间” 的比例决定,不是越多越好。
  • 隐形消耗:上下文切换是多线程的必要代价,单次开销小,但频繁切换会累积成巨大消耗。
  • 平衡之道:线程数量和切换次数要匹配,既不浪费 CPU,也不浪费切换时间。
  • 赞(0)
    未经允许不得转载:171主机测评 » 并发编程(一):多线程与上下文切换,从 “什么时候用” 到 “隐形消耗”
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址