1. 调度与同步概述:并发编程的挑战、任务调度的基本概念、同步机制简介、协同优化的意义
并发编程的挑战:为什么我们总在“打架”
说实话,我刚入行那会儿,觉得并发编程挺酷的。多线程一开,性能蹭蹭往上涨。直到有一次,我写了个简单的计数器,两个线程各加 100 万次,结果跑出来不是 200 万。嗯,当时我就懵了。
这就是并发编程的第一个挑战——竞态条件。多个线程同时读写共享数据,结果取决于谁先谁后。你想想看,两个线程同时执行 count++,这行代码在 CPU 眼里其实是三步:读、加、写。如果线程 A 刚读完,线程 B 就写回去了,那 A 的加一操作就白费了。
核心问题:并发编程的本质矛盾在于——我们想用多核提速,但共享资源的访问顺序不可控。
除了竞态,还有几个老朋友:
- 死锁:线程 A 等 B 的资源,B 等 A 的资源,俩人就这么干瞪眼。我曾经在线上排查过一个死锁问题,两个服务互相调用的接口都加了锁,结果请求一多直接卡死,那叫一个酸爽。
- 活锁:比死锁更气人——线程们都在动,但就是干不了正事。就像两个人面对面让路,你往左我也往左,你往右我也往右。
- 饥饿:优先级低的线程永远抢不到 CPU。我见过一个日志线程,因为优先级设得太低,业务高峰期愣是一分钟没写出一条日志。
任务调度的基本概念:谁先跑,谁后跑
任务调度,说白了就是决定「下一个该谁上 CPU 跑」。操作系统里有个调度器,它手里拿着一个任务队列,按某种规则挑一个出来执行。
常见的调度策略,我整理了一张表:
| 调度策略 | 核心思想 | 适用场景 | 我踩过的坑 |
|---|---|---|---|
| 先来先服务(FCFS) | 谁先到谁先跑 | 批处理系统 | 短任务被长任务堵死 |
| 时间片轮转(RR) | 每人跑一小段,轮流来 | 分时系统 | 时间片设太短,上下文切换开销爆炸 |
| 优先级调度 | 优先级高的先跑 | 实时系统 | 优先级反转,低优先级线程间接阻塞了高优先级 |
| 多级反馈队列 | 跑得久的降级,新来的优先 | 通用操作系统 | 参数调不好,交互式任务响应慢 |
我个人习惯把调度看作一个「排队问题」。但这里的队不是简单的先来后到——你得考虑任务的紧急程度、执行时间、资源依赖。我做过一个实时音视频项目,音频线程必须每 10ms 跑一次,视频线程可以容忍 50ms。如果调度器把音频和视频同等对待,那音频就会断断续续,用户体验极差。
我的建议:设计调度策略时,先搞清楚你的任务对延迟的敏感度。I/O 密集型的任务,尽量让它在阻塞时主动让出 CPU;CPU 密集型的任务,注意不要让一个线程霸占太久。
同步机制简介:怎么让线程们“好好说话”
调度解决的是「谁先跑」,同步解决的是「跑的时候别打架」。同步机制就是给共享资源加把锁,保证同一时间只有一个线程能碰它。
常见的同步手段:
- 互斥锁(Mutex):最基础的锁。线程想进临界区,先拿锁,拿不到就等着。我见过有人把整个函数都锁住的,那跟单线程没区别。
- 读写锁(RWLock):读读不互斥,读写互斥,写写互斥。适合读多写少的场景。我做过一个配置中心,配置几乎不变,但每秒几千次读取,用读写锁性能提升很明显。
- 信号量(Semaphore):控制同时访问的线程数量。比如数据库连接池,限制最多 10 个连接同时使用。
- 条件变量(Condition Variable):线程等某个条件满足再继续。比如生产者-消费者模式,消费者等队列里有数据再取。
// 一个典型的互斥锁使用模式(伪代码)
lock(mutex);
try {
// 临界区:操作共享数据
shared_counter++;
// 这里不要做耗时操作,否则其他线程会等死
} finally {
unlock(mutex);
}
注意:锁的粒度要适中。太粗了性能差,太细了容易死锁。我见过一个极端案例——有人给每个整数变量都配了一把锁,结果代码里到处都是 lock/unlock,逻辑根本没法维护。
协同优化的意义:调度和同步不是孤岛
很多人把调度和同步分开看。调度归操作系统管,同步归应用层管。但实际项目中,这两者深度耦合。
举个例子。你有一个线程池,里面 8 个线程都在等一把锁。调度器看到这 8 个线程都阻塞了,就会把 CPU 让给其他线程。但问题是,持有锁的那个线程可能因为优先级低,迟迟得不到 CPU。结果就是——锁释放不了,8 个线程继续等,系统吞吐量直接腰斩。
这就是调度和同步没配合好的典型后果。我管这叫「调度-同步失配」。
协同优化的意义就在于:
- 减少不必要的上下文切换:同步机制设计得好,线程就不会频繁阻塞唤醒。每次上下文切换大约消耗几微秒,积少成多很可观。
- 避免优先级反转:调度策略要考虑锁的持有者。比如优先级继承协议,低优先级线程持有锁时,临时提升它的优先级,让它尽快释放。
- 提高缓存利用率:调度时尽量把访问同一数据的线程放在同一个 CPU 核心上,减少缓存失效。同步时尽量用无锁数据结构,避免缓存行 bouncing。
下面这张图,是我对调度与同步关系的理解:
你看,调度器和同步机制不是各自为政的。调度器分配线程,同步机制保护资源,两者通过「协同优化」这个纽带连接在一起。底部的三个优化目标,就是我们在实际项目中要追求的方向。
我记得有一次做高并发网关,一开始调度和同步各调各的,性能死活上不去。后来我把线程池大小、锁的粒度、任务优先级放在一起调优,吞吐量直接翻了一倍。嗯,从那以后我就养成了一个习惯——设计并发系统时,先把调度和同步放在一张图上画清楚。
一句话总结:调度和同步就像自行车的两个轮子,单独优化一个轮子,车还是跑不快。只有两个轮子协同转动,才能平稳高效地前进。