一文理解后端核心概念:同步/异步、阻塞/非阻塞、进程/线程/协程

一文理解后端核心概念:同步/异步、阻塞/非阻塞、进程/线程/协程

作为开发者,你是否也曾被这些问题困扰?

"同步和异步到底有啥区别?"

"阻塞和非阻塞是一回事吗?"

"进程、线程、协程的关系怎么理清?"

"并发和并行能划等号吗?"

这些概念看似零散,实则归属于不同维度与层次。本文尝试用通俗的语言、形象的类比和可运行的代码,系统梳理这些核心概念,帮助你理解其内在逻辑。

一、先看全局:核心概念关系图

首先用一张图理清所有概念的层级关系:

简单总结:应用程序由进程构成,进程包含线程,线程内可调度协程;而同步/异步描述执行顺序,阻塞/非阻塞描述等待状态,并发/并行描述任务关系。

二、同步 vs 异步:关注"执行顺序"

核心区别:是否需要等待当前任务完成才能执行下一个

同步(Synchronous)

  • 任务按顺序执行,必须等当前任务完成,才能启动下一个
  • 调用者需要等待结果返回,期间无法做其他事

异步(Asynchronous)

  • 任务可独立执行,无需等待当前任务完成
  • 调用者发起操作后立即返回,结果通过回调、Future等方式通知

生活类比

  • 同步:去餐厅点餐 → 等厨师做饭 → 拿到饭 → 开始吃(全程等待,啥也干不了)
  • 异步:点外卖 → 立即做其他事 → 外卖到了通知你 → 去拿外卖(无需等待,可正常工作)

C++代码示例

同步读取文件
cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>

void synchronous_read() {
    std::cout << "开始读取文件..." << std::endl;
    
    std::ifstream file("data.txt");
    std::string content;
    
    // 此处会阻塞,直到读取完成
    if (file.is_open()) {
        std::getline(file, content, '\0');
        file.close();
    }
    
    std::cout << "文件读取完成: " << content << std::endl;
    std::cout << "继续执行其他操作" << std::endl;
}
异步读取文件(C++20协程)
cpp 复制代码
#include <iostream>
#include <coroutine>
#include <thread>
#include <future>

// 简化的异步文件读取
std::future<std::string> async_read_file(const std::string& filename) {
    return std::async(std::launch::async, [filename]() {
        std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟IO耗时
        return "文件内容: Hello World";
    });
}

void asynchronous_read() {
    std::cout << "开始异步读取文件..." << std::endl;
    
    // 发起异步操作,立即返回
    auto future = async_read_file("data.txt");
    
    std::cout << "可以继续做其他事情!" << std::endl;
    std::cout << "做一些其他工作..." << std::endl;
    
    // 需要结果时再获取
    std::string content = future.get();
    std::cout << "异步读取完成: " << content << std::endl;
}

三、阻塞 vs 非阻塞:关注"等待状态"

核心区别:等待结果时,线程是否会挂起并失去CPU控制权

阻塞(Blocking)

  • 调用后,线程会挂起等待,失去CPU控制权
  • 直到操作完成或超时才返回,期间无法执行其他任务

非阻塞(Non-blocking)

  • 调用后立即返回结果(可能是"尚未完成"的状态)
  • 线程保持活跃,可执行其他任务,需通过轮询或事件通知获取最终结果

生活类比

  • 阻塞:去银行办业务 → 排队 → 叫号 → 站在窗口前等待办理(全程不能离开,啥也干不了)
  • 非阻塞:取号后 → 去旁边玩手机 → 时不时看叫号屏幕 → 叫到号再去办理(可做其他事,无需傻等)

C++代码示例

阻塞IO(Socket读取)
cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>

void blocking_read() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    // ... 连接服务器代码省略 ...
    
    char buffer[1024];
    
    std::cout << "等待接收数据(阻塞中)..." << std::endl;
    
    // recv是阻塞调用,会一直等待直到有数据到达
    ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0);
    
    std::cout << "收到数据: " << bytes << " 字节" << std::endl;
    // 只有收到数据后,才会执行到这里
    
    close(sock);
}
非阻塞IO(Socket读取)
cpp 复制代码
#include <iostream>
#include <fcntl.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <errno.h>

void non_blocking_read() {
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    
    // 设置为非阻塞模式
    int flags = fcntl(sock, F_GETFL, 0);
    fcntl(sock, F_SETFL, flags | O_NONBLOCK);
    
    // ... 连接服务器代码省略 ...
    
    char buffer[1024];
    
    while (true) {
        // recv立即返回,不会等待
        ssize_t bytes = recv(sock, buffer, sizeof(buffer), 0);
        
        if (bytes > 0) {
            std::cout << "收到数据: " << bytes << " 字节" << std::endl;
            break;
        } else if (bytes == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
            // 暂时没有数据,可以做其他事
            std::cout << "暂无数据,做点其他事..." << std::endl;
            sleep(1);  // 模拟其他工作
        } else {
            // 真正的错误
            break;
        }
    }
    
    close(sock);
}

四、关键区分:同步/异步 与 阻塞/非阻塞

这是最容易混淆的两对概念!记住核心:

  • 同步/异步:关注任务执行顺序(消息通知机制)
  • 阻塞/非阻塞:关注线程等待状态

它们可以组合出5种主要场景,对应不同的使用场景:

组合 特点 使用场景 例子
同步阻塞 发起调用后阻塞等待完成 简单场景,逻辑清晰 普通的read/write
同步非阻塞(轮询) 立即返回,需主动重试 需要持续检查状态 O_NONBLOCK + 轮询
同步非阻塞(多路复用) 阻塞等待多个FD就绪 高并发服务器 select/poll/epoll
异步阻塞 发起异步操作但阻塞等待 罕见,效率低 aio_read + aio_suspend
异步非阻塞 发起后立即返回,完成后回调 极致性能 io_uring, Linux AIO, IOCP

重点澄清:epoll不是异步IO!

很多人误以为epoll是"异步非阻塞",这是常见误解!

根据POSIX标准定义:

  • 同步I/O:导致请求进程阻塞,直到I/O操作完成
  • 异步I/O:不导致请求进程阻塞

epoll的本质是同步I/O多路复用------用一个阻塞调用同时监听多个文件描述符。虽然监听的socket是非阻塞的,但epoll_wait()本身会阻塞等待事件,且最终的数据拷贝操作(如recvfrom)仍然会阻塞进程。

真正的异步非阻塞IO示例(io_uring)

cpp 复制代码
#include <iostream>
#include <liburing.h>

// 真正的异步非阻塞IO: io_uring
void async_io_uring_example() {
    struct io_uring ring;
    io_uring_queue_init(32, &ring, 0);
    
    char buffer[1024];
    int fd = open("test.txt", O_RDONLY);
    
    // 1. 提交异步读请求(立即返回,不阻塞)
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buffer, sizeof(buffer), 0);
    io_uring_sqe_set_data(sqe, buffer);
    io_uring_submit(&ring);
    
    std::cout << "异步读请求已提交,立即返回,可以做其他事..." << std::endl;
    std::cout << "做其他工作..." << std::endl;
    
    // 2. 等待完成(此处可选择阻塞或非阻塞等待)
    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);
    
    // 3. 处理结果
    if (cqe->res >= 0) {
        std::cout << "异步读取完成: " << cqe->res << " 字节" << std::endl;
    }
    
    io_uring_cqe_seen(&ring, cqe);
    io_uring_queue_exit(&ring);
    close(fd);
}

五、进程、线程、协程:资源粒度递减

这三个概念描述的是任务执行的资源分配单位,核心区别在于资源占用和调度开销:

特性 进程(Process) 线程(Thread) 协程(Coroutine)
创建时间 毫秒(ms) 微秒(μs) 纳秒(ns)
内存占用 数MB ~1MB栈 几十字节
切换开销 极小
通信方式 IPC(管道/共享内存) 直接共享内存 直接访问
并行能力 真并行 真并行 并发(非并行)

核心特点

  • 进程:最重量级,有独立内存空间,隔离性强,一个进程崩溃不影响其他进程
  • 线程:中等重量,共享进程内存,一个线程崩溃可能导致整个进程崩溃
  • 协程:最轻量级,在线程内调度,协作式调度(需要主动让出CPU)

C++代码示例

进程示例
cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>

void process_example() {
    pid_t pid = fork();
    
    if (pid == 0) {
        // 子进程
        std::cout << "子进程ID: " << getpid() << std::endl;
        sleep(2);
        exit(0);
    } else {
        // 父进程
        std::cout << "父进程ID: " << getpid()
                  << ", 子进程ID: " << pid << std::endl;
        wait(nullptr);  // 等待子进程结束
    }
}
线程示例
cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>

void worker(int id) {
    std::cout << "线程 " << id << " 开始工作" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::cout << "线程 " << id << " 完成工作" << std::endl;
}

void thread_example() {
    std::vector<std::thread> threads;
    
    // 创建5个线程
    for (int i = 0; i < 5; i++) {
        threads.emplace_back(worker, i);
    }
    
    // 等待所有线程完成
    for (auto& t : threads) {
        t.join();
    }
}
协程示例(C++20)
cpp 复制代码
#include <iostream>
#include <coroutine>
#include <thread>

// 简单的协程返回类型
struct Task {
    struct promise_type {
        Task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

Task coroutine_worker(int id) {
    std::cout << "协程 " << id << " 开始工作" << std::endl;
    
    // 模拟异步操作,主动让出CPU
    co_await std::suspend_always{};
    
    std::cout << "协程 " << id << " 恢复工作" << std::endl;
}

void coroutine_example() {
    // 轻松创建10万个协程,换成线程内存早溢出了!
    for (int i = 0; i < 100000; i++) {
        coroutine_worker(i);
    }
}

六、并发 vs 并行:任务的两种执行方式

核心区别:是否真正同时执行

并发(Concurrency)

  • 多个任务交替执行,看起来同时进行,实际是快速切换
  • 单个CPU即可实现(如单线程事件循环)

并行(Parallelism)

  • 多个任务真正同时执行
  • 需要多个CPU核心支持

生活类比

  • 并发:一个厨师做多道菜 → 炒一会儿菜1,再炒一会儿菜2,快速切换(单核CPU)
  • 并行:多个厨师同时做菜 → 厨师1炒莱1,厨师2炒莱2(多核CPU)

C++代码示例

并发示例(单线程事件循环)
cpp 复制代码
#include <iostream>
#include <queue>
#include <functional>

// 简单的事件循环(并发但不并行)
class EventLoop {
private:
    std::queue<std::function<void()>> tasks;
public:
    void add_task(std::function<void()> task) {
        tasks.push(task);
    }
    
    void run() {
        while (!tasks.empty()) {
            auto task = tasks.front();
            tasks.pop();
            
            // 执行任务(单线程,交替执行)
            task();
        }
    }
};

void concurrent_example() {
    EventLoop loop;
    
    // 添加多个任务
    loop.add_task([]() {
        std::cout << "任务1执行" << std::endl;
    });
    loop.add_task([]() {
        std::cout << "任务2执行" << std::endl;
    });
    loop.add_task([]() {
        std::cout << "任务3执行" << std::endl;
    });
    
    // 并发执行(快速切换)
    loop.run();
}
并行示例(多线程)
cpp 复制代码
#include <iostream>
#include <thread>
#include <vector>

void parallel_worker(int id) {
    std::cout << "并行任务 " << id
              << " 在线程 " << std::this_thread::get_id()
              << " 执行" << std::endl;
}

void parallel_example() {
    std::vector<std::thread> threads;
    
    // 创建多个线程,真正并行执行
    for (int i = 0; i < 4; i++) {
        threads.emplace_back(parallel_worker, i);
    }
    
    for (auto& t : threads) {
        t.join();
    }
}

七、异步编程的核心:回调与事件循环

1. 回调(Callback)

回调是异步操作完成后,用于通知调用者的函数,是异步编程的基础。

生活类比

点外卖时留电话:你下单 → 留电话(回调函数)→ 做其他事 → 外卖到了(调用回调)→ 取外卖

C++代码示例
cpp 复制代码
#include <iostream>
#include <functional>
#include <thread>
#include <chrono>

// 异步下载函数,接受回调
void async_download(
    const std::string& url,
    std::function<void(const std::string&)> callback) {
    // 创建线程模拟异步下载
    std::thread([url, callback]() {
        std::cout << "开始下载: " << url << std::endl;
        
        // 模拟下载耗时
        std::this_thread::sleep_for(std::chrono::seconds(2));
        
        // 下载完成,调用回调
        callback("下载完成的数据");
    }).detach();
}

void callback_example() {
    std::cout << "主线程:发起下载请求" << std::endl;
    
    // 发起异步下载,传入回调函数
    async_download("http://example.com/file",
        [](const std::string& data) {
            std::cout << "回调执行: " << data << std::endl;
        }
    );
    
    std::cout << "主线程:继续做其他事情" << std::endl;
    
    // 等待异步操作完成
    std::this_thread::sleep_for(std::chrono::seconds(3));
}

2. 事件循环(Event Loop)

事件循环是不断检查并处理事件的无限循环,是单线程异步系统(如Node.js、浏览器)的核心。

工作原理
C++实现简单事件循环
cpp 复制代码
#include <iostream>
#include <queue>
#include <functional>
#include <thread>
#include <mutex>
#include <condition_variable>

class SimpleEventLoop {
private:
    std::queue<std::function<void()>> event_queue;
    std::mutex mtx;
    std::condition_variable cv;
    bool running = true;
public:
    // 添加事件到队列
    void post_event(std::function<void()> event) {
        std::lock_guard<std::mutex> lock(mtx);
        event_queue.push(event);
        cv.notify_one();
    }
    
    // 事件循环主函数
    void run() {
        while (running) {
            std::unique_lock<std::mutex> lock(mtx);
            
            // 等待事件
            cv.wait(lock, [this]() {
                return !event_queue.empty() || !running;
            });
            
            if (!running) break;
            
            // 取出事件
            auto event = event_queue.front();
            event_queue.pop();
            
            lock.unlock();
            
            // 执行事件处理
            event();
        }
    }
    
    void stop() {
        running = false;
        cv.notify_all();
    }
};

void event_loop_example() {
    SimpleEventLoop loop;
    
    // 在另一个线程运行事件循环
    std::thread loop_thread([&loop]() {
        loop.run();
    });
    
    // 向事件循环投递事件
    loop.post_event([]() {
        std::cout << "事件1: 处理网络请求" << std::endl;
    });
    
    loop.post_event([]() {
        std::cout << "事件2: 处理文件IO" << std::endl;
    });
    
    loop.post_event([]() {
        std::cout << "事件3: 更新UI" << std::endl;
    });
    
    // 等待事件处理
    std::this_thread::sleep_for(std::chrono::seconds(2));
    
    loop.stop();
    loop_thread.join();
}

八、实战指南:如何选择合适的方案?

场景 推荐方案 理由
CPU密集型任务 多进程/多线程 需要真正的并行计算,充分利用多核CPU
IO密集型任务(高并发) IO多路复用(epoll)+协程 单线程处理大量连接,减少线程切换开销
IO密集型任务(极致性能) io_uring(真异步) 零拷贝,内核异步处理,性能最优
简单脚本工具 同步阻塞 代码简单,开发效率高,够用即可
高性能服务器 事件循环+IO多路复用+线程池 结合多种技术,平衡性能和开发复杂度
Web后端 协程+IO多路复用 如Go、Python asyncio方案,兼顾高并发和开发效率

最佳实践

  1. 优先使用同步阻塞,除非有明确的性能瓶颈
  2. IO密集型用IO多路复用,CPU密集型用多线程/进程
  3. 百万级连接场景用协程,资源占用少、切换快
  4. 主流高性能方案:事件循环+epoll(Nginx、Redis、Node.js均采用)
  5. 极致性能需求用io_uring(Linux 5.1+支持)

九、常见问题

  1. 同步一定是阻塞的吗?

    不一定!同步非阻塞(如轮询)和IO多路复用都是同步但非阻塞的反例。

  2. epoll是异步IO吗?

    不是!epoll是同步IO多路复用,epoll_wait()会阻塞等待事件,真正的异步IO是io_uring、Linux AIO。

  3. 协程和线程的区别?

    协程是用户态调度,轻量级(纳秒级创建);线程是内核态调度,重量级(微秒级创建)。

  4. 单核CPU能实现并行吗?

    不能!单核只能实现并发(快速切换任务),并行需要多核CPU支持。

十、总结

其实这些概念并不复杂,关键是抓住核心维度:

  • 同步/异步 → 执行顺序
  • 阻塞/非阻塞 → 等待状态
  • 进程/线程/协程 → 资源粒度
  • 并发/并行 → 任务关系
  • 回调/事件循环 → 实现机制
相关推荐
zhangrelay2 小时前
linux下如何通过与AI对话设置thinkpad电池充电阈值
linux·运维·笔记·学习
小王努力学编程2 小时前
LangChain——AI应用开发框架(核心组件2)
linux·服务器·c++·人工智能·python·langchain·信号
云服务器租用费用2 小时前
京东云主机企业用户能参与的优惠活动汇总
服务器·网络·京东云
浅水壁虎2 小时前
任务调度——XXLJOB3(执行器)
java·服务器·前端·spring boot
abcy0712133 小时前
请简述OSI七层网络模型,并分别列举每一层的主要功能及对应的主要协议。
网络
郝学胜-神的一滴3 小时前
深入理解TCP协议:数据格式与核心机制解析
linux·服务器·网络·c++·网络协议·tcp/ip
数据知道3 小时前
一文掌握 MongoDB 详细安装与配置(Windows / Linux / macOS 全平台)
linux·数据库·windows·mongodb·macos
人间不清醒ab3 小时前
FREERTOS检测任务栈内存情况
c语言·单片机
济6173 小时前
linux 系统移植(第十三期)---Linux 内核移植(2)- CPU 主频修改--- Ubuntu20.04
linux·运维·服务器