并发场景概述:什么是并发、并发与并行的区别、常见的并发模型

各位同学,咱们这门课叫《服务并发场景的抓包分析》。开篇第一讲,我得先跟你把「并发」这个概念聊透了。你想想看,如果连并发本身都说不清楚,后面抓包分析时看到一堆乱序的包,你根本不知道问题出在哪。

我刚开始带团队时,有个新人问我:「老大,并发不就是同时处理多个请求吗?」我当时笑了笑,没直接回答。后来在一次线上故障复盘会上,他因为搞混了并发和并行,把系统架构设计错了,导致高峰期服务大面积超时。嗯,从那以后,我每次讲并发都会先把这个坑说清楚。

1.1 到底什么是并发

并发,说白了就是「多个任务在同一个时间段内交替执行」。注意我的用词——是「交替」,不是「同时」。

举个例子:你一个人坐在工位上,左手敲代码,右手接电话,嘴里还跟旁边的同事对需求。看起来你好像同时在干三件事,但实际上你的大脑在快速切换:敲几行代码→接电话说两句→扭头回同事一句→再回来敲代码。这就是并发。

在计算机世界里也一样。一个单核 CPU 在同一时刻只能执行一个指令。但它可以飞快地在多个任务之间切换,让你感觉所有任务都在「同时」进行。这种快速切换的能力,就是并发的基础。

核心要点:并发关注的是「任务调度」和「资源争用」。多个任务共享同一个 CPU 时间片,谁先谁后、谁多谁少,由调度器决定。

我在项目中遇到过最典型的并发场景,就是 Web 服务器同时处理成千上万个客户端请求。每个请求看起来是「同时」被处理的,但实际上底层是线程或进程在争抢 CPU 时间片。你抓包时看到的那些交错到达的 TCP 报文,就是并发最直观的体现。

1.2 并发 vs 并行:别再搞混了

这两个词,我见过太多人混着用了。甚至有些技术文档都写得不严谨。咱们今天一次性把它掰扯清楚。

维度 并发(Concurrency) 并行(Parallelism)
核心定义 多个任务在同一个时间段内交替执行 多个任务在同一个时刻同时执行
硬件前提 单核 CPU 即可实现 必须多核 CPU 或多处理器
关注点 任务调度、资源争用、数据一致性 计算加速、吞吐量提升
典型例子 单核服务器处理 1000 个 HTTP 请求 8 核 CPU 同时运行 8 个计算任务
抓包表现 报文交错到达,时间戳密集但不同时 多个连接的数据包几乎同时出现

我习惯用一个比喻来解释:并发是「一个人吃三碗面」,并行是「三个人各吃一碗面」。前者靠的是快速切换,后者靠的是人多力量大。

我曾经踩过的坑:有一次做性能优化,我天真地以为把并发模型改成并行模型就能线性提升吞吐量。结果上了 8 核机器,性能只提升了不到 2 倍。为什么?因为任务之间有锁竞争、有 IO 等待,并行度被严重拖累了。抓包一看,大量时间花在了锁等待和上下文切换上。所以,并行不是万能的,你得先搞清楚瓶颈在哪。

你抓包分析时,一定要区分这两个概念。如果你看到多个连接的数据包时间戳完全重叠,那说明系统确实在并行处理。但如果时间戳是交错但密集的,那只是并发调度得好,别误判成并行。

1.3 常见的并发模型

搞清楚了概念,咱们来看看实际工程中怎么实现并发。我归纳了三种主流模型:多线程、多进程、异步 IO。每种模型都有它的脾气,选错了后面全是坑。

1.3.1 多线程模型

这是最常用的并发模型。一个进程内创建多个线程,共享同一块内存空间。线程之间切换开销小,通信也方便——直接读写共享变量就行。

但方便归方便,坑也不少。共享内存意味着你要处理锁、竞态条件、死锁这些问题。我在项目中见过最离谱的一次,一个简单的计数器因为没加锁,线上跑了三天后数值变成了负数。抓包分析时根本看不出问题,最后是加日志才定位到是并发写导致的。

// 一个典型的多线程服务端伪代码
void handle_request(int client_fd) {
    // 每个请求创建一个线程处理
    pthread_t tid;
    pthread_create(&tid, NULL, worker_thread, &client_fd);
    pthread_detach(tid);  // 分离线程,自动回收资源
}

void* worker_thread(void* arg) {
    int fd = *(int*)arg;
    char buffer[1024];
    read(fd, buffer, sizeof(buffer));
    // 处理业务逻辑...
    write(fd, response, strlen(response));
    close(fd);
    return NULL;
}

我的建议:多线程适合 CPU 密集型任务,或者任务之间需要频繁共享数据的场景。但千万别创建太多线程——线程数超过 CPU 核心数的 2-3 倍后,上下文切换的开销会吃掉所有性能提升。我一般控制在「CPU 核心数 × 2 + 1」这个范围。

1.3.2 多进程模型

多进程模型跟多线程最大的区别:每个进程有自己独立的内存空间。进程之间不共享数据,通信得靠管道、消息队列、共享内存这些 IPC 机制。

好处是隔离性好——一个进程挂了不会影响其他进程。坏处是创建进程的开销大,进程间通信也比线程间通信慢得多。

我记得早期 Apache 服务器用的就是多进程模型,每个请求 fork 一个子进程来处理。这种方式在请求量不大的时候还行,一旦并发上来,fork 的开销和进程切换的成本就扛不住了。后来 Nginx 改用异步 IO 模型,才解决了这个问题。

对比项 多线程 多进程
内存开销 低(共享内存) 高(独立内存)
创建/切换开销 低 高
数据共享 容易(需加锁) 困难(需 IPC)
隔离性 差(一个线程崩溃影响整个进程) 好(进程间互不影响)
适用场景 高并发、频繁数据交互 高可靠性、任务隔离要求高

1.3.3 异步 IO 模型

这个模型,我个人认为是目前高并发场景下的最优解。它的核心思想是:不等待。

传统的阻塞 IO 模型,一个线程发起 read 调用后,如果数据还没准备好,线程就卡在那了。异步 IO 不一样——你发起 IO 请求后,直接返回去做别的事。等数据准备好了,系统会通知你:「嘿,数据到了,你来处理吧。」

Nginx、Node.js、Python 的 asyncio、Java 的 NIO,底层都是这个套路。一个线程可以管理成千上万个连接,因为大部分时间线程都在处理业务逻辑,而不是傻等 IO。

// 异步 IO 的典型模式(伪代码)
event_loop = create_event_loop();

// 注册 10000 个连接
for (i = 0; i < 10000; i++) {
    event_loop.register(fd[i], ON_READABLE, handle_read);
}

// 单线程事件循环
while (true) {
    events = event_loop.poll();  // 获取就绪的事件
    for (event in events) {
        event.callback(event.fd);  // 处理就绪的连接
    }
}

关键点:异步 IO 模型下,你抓包时会看到大量连接的数据包交错出现,但发起这些请求的线程可能只有一个。这就是「单线程高并发」的秘密——线程不阻塞在 IO 上,而是充分利用 CPU 时间片处理就绪的任务。

我曾经用异步 IO 模型重构过一个网关服务。原来用多线程模型,8 核机器只能撑 2000 并发连接,CPU 使用率却飙到 90%。改成异步 IO 后,同样的机器撑到了 5 万并发连接,CPU 使用率反而降到了 30%。抓包对比一看,多线程版本大量时间花在了线程切换和锁等待上,而异步版本几乎全是有效的数据处理。

1.4 三种模型的选型建议

你可能会问:「那我到底该用哪种?」我的经验是:

  • 任务计算量大、数据共享频繁 → 多线程。比如实时数据处理、图像渲染。
  • 任务之间需要强隔离、稳定性要求极高 → 多进程。比如浏览器每个标签页一个进程,一个崩了不影响其他。
  • 高并发 IO 密集型、连接数巨大 → 异步 IO。比如 Web 服务器、消息队列、网关。

当然,实际项目中经常是混合使用。比如用多进程做隔离,每个进程内部再用异步 IO 处理高并发连接。Nginx 就是这么干的。

注意:选型没有银弹。我见过有人为了追求「高性能」,强行把 CPU 密集型任务改成异步 IO,结果因为回调嵌套太深,代码维护成本暴涨,性能反而没提升。先搞清楚你的瓶颈是 CPU 还是 IO,再选模型。

1.5 本章知识体系总览

下面这张图,把咱们这一章的核心逻辑串起来了。你把它存脑子里,后面学抓包分析时,每看到一个现象就往这张图上对应,思路会清晰很多。

并发场景知识体系 并发(Concurrency) 核心概念 并发 vs 并行 并发模型 任务交替执行 资源争用 单核交替 多核同时 多线程 多进程 异步 IO 模型 事件循环 非阻塞 IO 抓包分析关注点 报文交错模式 → 判断并发类型 | 时间戳密集程度 → 评估调度效率 | 连接数 vs 线程数 → 识别模型

这张图把并发、并行、三种模型以及它们之间的关系都串起来了。你后面做抓包分析时,看到报文的时间戳分布,就能反推出底层用的是哪种并发模型。这就是咱们这门课的核心能力——从网络现象反推系统架构。