IO 多路复用和多线程有什么区别?

#计算机网络#操作系统#面试八股

IO 多路复用和多线程有什么区别?

两者都在解决"同时处理很多连接",但路径不同:多路复用把"等"集中到一个线程,靠事件循环分发; 多线程把"等"分摊到多个线程,每个线程阻塞在自己的连接上。前者省内存、省切换,但要求所有操作都是非阻塞的; 后者编程模型简单,但线程数与内存开销随连接数线性增长。

1. 两种模型

多路复用(Reactor)
        ┌─────────────────────────────┐
连接1 ─►│                             │
连接2 ─►│  epoll_wait  → 就绪列表      │──► 单线程/少量线程处理
连接3 ─►│  (一个线程等所有 fd)        │
连接N ─►│                             │
        └─────────────────────────────┘
  内存:N 个 fd + 一个就绪队列;线程数固定

多线程(Thread-per-Connection)
  连接1 ──► 线程1(阻塞 read,睡在等数据)
  连接2 ──► 线程2(阻塞 read,睡在等数据)
  连接N ──► 线程N(每个线程 1~8MB 栈 + 内核调度实体)
  内存:N × 栈;线程数 = 连接数(N 大时直接爆)

2. 对比

维度IO 多路复用多线程阻塞
并发上限万~百万(受 fd 与内存限制)几千(受线程栈与调度限制)
内存占用每连接几百字节每线程 MB 级栈
上下文切换少(线程数固定)多(线程多时调度开销显著)
编程复杂度高,状态机/回调/协程低,同步写法
阻塞调用一个阻塞就卡死整个循环天然隔离,互不影响
调试排查难(栈不直观)易(线程栈清晰)
适用场景高并发短连接、网关、IM低并发、CPU 密集、依赖阻塞库

3. 代码对比

// 多线程:一个连接一个线程,写法最直观
void *worker(void *arg) {
    int fd = *(int *)arg;
    char buf[4096];
    for (;;) {
        ssize_t n = read(fd, buf, sizeof buf);   // 阻塞,本线程睡
        if (n <= 0) break;
        write(fd, buf, n);
    }
    close(fd);
    return NULL;
}
// accept 之后 pthread_create(&t, NULL, worker, &fd);
// 多路复用:一个线程盯所有 fd,谁就绪处理谁
struct epoll_event evs[256];
for (;;) {
    int n = epoll_wait(ep, evs, 256, -1);        // 唯一的等待点
    for (int i = 0; i < n; i++) {
        int fd = evs[i].data.fd;
        if (fd == listen_fd) { accept_new(); continue; }
        handle_readable(fd);                     // 必须非阻塞,且要读到 EAGAIN
    }
}

注意:事件循环里不能有阻塞调用。一次 read 卡住、一次慢 SQL,整个循环就停了, 所以多路复用通常要配非阻塞 IO + 业务线程池(Reactor + Worker 两级)。

4. 常见追问

Q:连接数不多时,多线程是不是更好? 通常是。线程模型简单、可读性强、能直接调用阻塞库(JDBC、文件 IO)。并发几百以内, 线程开销完全可接受,没必要为了"高性能"引入事件循环的复杂度。

Q:Go 的 goroutine 算哪一种? 都算又都不算:写法是"一连接一协程"(像多线程),底层是 netpoller(epoll)+ GMP 调度(像多路复用)。 用户写阻塞代码,运行时在阻塞点自动把协程挂起、把线程让给别人,兼顾了两者的优点。

Q:为什么 Redis 单线程能扛十万 QPS? 因为它的操作都是内存级,单次耗时微秒级,瓶颈在网络而非 CPU;epoll 让一个线程就能盯住所有连接, 避免了锁与上下文切换。真正的重活(大 key 删除、持久化)才交给后台线程。

Q:多路复用怎么利用多核? 常见做法是多开几个事件循环(master-worker),每个 worker 一个 epoll、绑一个核,用 SO_REUSEPORT 让内核把新连接分散到各 worker,Nginx 就是这套模型。

小结

多路复用是"少量线程 + 事件驱动",用非阻塞换并发密度;多线程是"大量线程 + 阻塞等待", 用内存换编程简单。选型的判断点:连接数是否上万、是否必须调用阻塞库、团队能否驾驭事件循环。 答这题最好补一句"goroutine 是二者的融合",能体现对运行时调度的理解。

笔记加载中…