第三部分:并发与异步编程模型 —— 驾驭多核时代的力量
多核处理器已经成为现代计算机的标配,如何充分借助并发与并行来提升程序性能,成为开发者必须掌握的核心技能。但并发编程有一个显著特点:它既能带来性能提升,也可能引发诸多难题——数据竞争、死锁、线程安全,每一个都不容小觑。因此,深入理解一门语言的并发模型,才是安全高效编写并发程序的关键。
3.1 并发的基本概念:并行 vs 并发,线程 vs 协程
先来厘清并发与并行的区别。并发,是指多个任务在逻辑上“同时”进行,实际上是通过时间片轮转交替切换;并行,则是多个任务在物理上真正同时执行,这需要多核处理器的支持。打个比方:一个人轮流吃三碗饭是并发,三个人各自同时吃一碗才是并行。
那么线程和协程又有何不同?线程是操作系统调度的最小单位,切换成本大约在几微秒到几十微秒之间。而协程是由语言运行时在用户态调度的轻量级“线程”,切换成本低至纳秒级,创建几十万个协程也毫无压力。

3.2 异步编程模型:回调 → Promise → async/await 的演进
异步编程的核心在于:程序在等待I/O操作时,可以腾出手来处理其他任务,避免线程白白阻塞。掌握异步模型,是编写高响应、高吞吐应用的前提条件。
来看一个实际例子:JavaScript的Promise链和async/await。
// 模拟异步操作
function fetchUser(id) {
return new Promise(resolve => {
setTimeout(() => resolve({ id, name: `User${id}` }), 100);
});
}
function fetchPosts(userId) {
return new Promise(resolve => {
setTimeout(() => resolve([`Post1 by ${userId}`, `Post2 by ${userId}`]), 100);
});
}
// Promise链式调用
fetchUser(1)
.then(user => {
console.log(user.name);
return fetchPosts(user.id);
})
.then(posts => {
console.log(posts);
})
.catch(err => console.error(err));
// async/await 更清晰的写法
async function displayUserContent(id) {
try {
const user = await fetchUser(id);
console.log(user.name);
const posts = await fetchPosts(user.id);
console.log(posts);
} catch (err) {
console.error(err);
}
}
displayUserContent(2);
这里的关键在于:async函数始终返回一个Promise,await会在当前函数内部暂停执行,直到Promise完成——但注意,它并不会阻塞事件循环。相比曾经的“回调地狱”(Callback Hell),这种写法读起来要友好得多。
再看看Python的asyncio如何实现并发网络请求。
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://example.org',
'https://example.net',
]
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for url, html in zip(urls, results):
print(f"{url}: {len(html)} bytes")
asyncio.run(main())
这里asyncio.gather并发地调度多个协程。当程序等待网络响应时,事件循环可以切换到其他协程继续执行,从而在I/O密集型场景下实现高效并发。这,才是异步编程的真正价值所在。
3.3 多线程同步:锁、原子操作、并发集合
一旦多个线程共享可变状态,同步机制就必须上场了。否则数据一致性将无从谈起。
以Java的线程安全计数器为例,我们看看不同实现方式的性能差异。
// 非线程安全
class UnsafeCounter {
private int count = 0;
public void increment() { count++; } // 多线程下丢失更新
public int get() { return count; }
}
// 使用synchronized
class SynchronizedCounter {
private int count = 0;
public synchronized void increment() { count++; }
public synchronized int get() { return count; }
}
// 使用AtomicInteger(无锁CAS)
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() { count.incrementAndGet(); }
public int get() { return count.get(); }
}
// 使用LongAdder(高并发下更好)
import java.util.concurrent.atomic.LongAdder;
class AdderCounter {
private LongAdder count = new LongAdder();
public void increment() { count.increment(); }
public long get() { return count.sum(); }
}
性能方面:synchronized在低竞争场景下已经够用;AtomicInteger基于CAS,没有锁的开销,但高竞争时可能频繁自旋;而LongAdder则通过分段计数进一步降低竞争。进阶开发者会根据场景灵活选择最合适的方案。
再看Go语言中的Mutex和RWMutex。
type SafeCounter struct {
mu sync.Mutex
m map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.m[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.m[key]
}
// 读多写少场景使用RWMutex,允许并发读
type SafeMap struct {
mu sync.RWMutex
m map[string]string
}
func (s *SafeMap) Get(key string) string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.m[key]
}
处理读多写少的场景时,RWMutex更为合适——它允许多个读操作并发执行,写操作则独占访问。
3.4 数据竞争与死锁的检测与避免
数据竞争的本质是:多个线程同时访问同一内存位置,且至少有一个是写操作,同时没有任何同步保护。Go语言内置了数据竞争检测器,只需在测试时加上 go test -race 就能直接定位问题。
而死锁,则是指两个或多个线程互相等待对方持有的锁,最终导致所有线程都无法继续执行。
来看一个经典的Java死锁示例。
public class DeadlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();
public void method1() {
synchronized(lock1) {
sleep(100); // 模拟工作
synchronized(lock2) {
System.out.println("method1 done");
}
}
}
public void method2() {
synchronized(lock2) {
sleep(100);
synchronized(lock1) {
System.out.println("method2 done");
}
}
}
private void sleep(int ms) { try { Thread.sleep(ms); } catch(Exception e) {} }
public static void main(String[] args) {
DeadlockDemo demo = new DeadlockDemo();
new Thread(demo::method1).start();
new Thread(demo::method2).start();
}
}
要避免死锁,以下几个原则值得牢记:
- 固定锁顺序:始终以相同的顺序获取锁。比如先拿
lock1再拿lock2。 - 使用超时尝试:Java中的
tryLock(timeout),获取失败时释放已持有的锁,然后重试。 - 减小锁粒度:采用并发集合(如
ConcurrentHashMap)或无锁数据结构来替代粗粒度锁。 - 使用更高级的同步工具:像
java.util.concurrent包中的Semaphore、CountDownLatch等,比手动管理锁更加安全可靠。
