腾讯 C++ 一面被拷打:进程线程区别?还有多线程多进程该咋选

如果你面过腾讯会议的后台开发岗,大概率会遇到这道题:“什么时候用多进程?什么时候用多线程?二者该怎么配合?” 

其实这题不只是考你背概念 —— 面试官真正想知道的是:你能不能结合 “高并发、低延迟、高可用” 的会议场景,把技术选型讲明白。

今天咱们就从 “快递站类比” 入手,一步步拆透进程与线程的本质,再结合腾讯会议的实际业务,聊聊怎么选、怎么配,让你不仅能答好面试题,还能搞懂背后的工程逻辑。

Part1先破题

很多人一上来就背 “进程是资源单位,线程是调度单位”—— 这话没错,但这句话烂大街了,学过操作系统的人都知道这句话,然而大多数人知其然不知其所以然?为啥进程是资源最小分配单位,线程是调度最小单位,只有自己写一遍操作系统才能深刻理解。

面试官要的是 “你懂这个区别,还能用到实际开发里”。

比如腾讯会议的场景:百万用户同时开麦、共享屏幕,既要保证 “某个房间的媒体解码崩了,不影响其他房间”(隔离性),又要保证 “信令转发不卡,延迟低于 100ms”(高性能)。这时候选多进程还是多线程?怎么配合?才是题眼。

所以咱们的回答逻辑得是:先搞懂 “进程和线程到底差在哪”→ 再看 “不同场景要什么特性”→ 最后落地 “二者怎么配合”。

Part2理解本质:进程与线程

先抛开复杂的 OS 术语,咱们用 “小区快递站” 打个比方 —— 容易理解的类比:

  • 进程 = 快递站:每个快递站有自己的仓库(内存空间)、货车(文件描述符)、营业执照(环境变量),是一个 “独立的资源包”。你开一家快递站,就得申请这些资源,成本不低。 
  • 线程 = 快递员:快递员归快递站管,共享仓库里的包裹(进程资源),但每个快递员有自己的路线表(程序计数器 PC)、小本子(栈空间)—— 比如 A 快递员记着 “3 栋送生鲜”,B 记着 “5 栋送文件”,各跑各的,但用的是同一个快递站的资源。

再补个关键细节:快递站自己不会 “跑业务” —— 你开了一家快递站(启动进程),默认会派一个 “店长快递员”(主线程)先干活;要是单靠店长忙不过来,再招几个兼职快递员(子线程),这才是 “多线程”。

这个类比能帮你记住核心:进程是 “资源容器”,线程是 “执行干活的人” 。

Part3操作系统视角

类比只是入门,要答好面试题,得懂 OS 层面的 “硬区别”。

接下来我们从操作系统视角:扒开进程与线程的 “底层差异”。

咱们从 3 个核心维度拆:

3.1、资源归属:谁有 “独占权”,谁在 “共享”?

进程是 OS资源分配的基本单位—— 意思是 OS 给资源时,只认进程,不认线程。

比如:

  • 独立的虚拟地址空间:你在进程 A 里 new 一个 100MB 的数组,进程 B 根本看不到,除非特意共享;
  • 独立的文件描述符表:进程 A 打开了 “log.txt”,拿到描述符 3,进程 B 打开同一个文件,拿到的是描述符 5,二者互不干扰;
  • 还有环境变量、信号处理表这些,都是进程独有的。

而线程是 OSCPU 调度的基本单位—— 它只占 “干活必须的少量资源”,其他全靠共享:

  • 独占的:线程 ID(TID)、程序计数器(PC,记录下一条要执行的指令)、寄存器集合(CPU 里的临时数据)、私有栈(比如函数调用的局部变量,每个线程一份,不会乱);
  • 共享的:进程的堆内存(比如全局的用户列表)、代码区(执行的程序逻辑)、文件描述符表(线程 A 打开的文件,线程 B 也能读)。

这里插个 Linux 的 “冷知识”:Linux 里没有 “纯线程”—— 咱们说的线程,本质是 “共享资源的轻量级进程(LWP)”。OS 用同一个 PCB(进程控制块)记录共享资源,再用 TCB(线程控制块)记录每个线程的执行信息。所以 Linux 里 “线程切换”,其实是 “共享 PCB 的 LWP 切换”,开销比进程小。

咱们用 C 代码验证 “进程内存不共享,线程内存共享”(腾讯会议中 “房间状态共享” 就依赖这个特性):

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/wait.h>
// 全局变量(进程堆内存,线程共享,进程隔离)
int global_val = 10;
// 线程函数:修改全局变量
void* thread_func(void* arg) {
    global_val = 20; // 线程修改全局变量
    printf("线程内:global_val = %d(地址:%p)\n", global_val, &global_val);
    return NULL;
}
int main() {
    // 测试1:多线程共享内存
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    pthread_join(tid, NULL);
    printf("主线程:global_val = %d(地址:%p)\n", global_val, &global_val); 
    // 输出:主线程global_val=20(地址相同)——证明线程共享内存
    // 测试2:多进程隔离内存
    pid_t pid = fork();
    if (pid == 0) { // 子进程
        global_val = 30;
        printf("子进程:global_val = %d(地址:%p)\n", global_val, &global_val);
        _exit(0);
    } else { // 父进程
        wait(NULL);
        printf("父进程:global_val = %d(地址:%p)\n", global_val, &global_val);
        // 输出:父进程global_val=20(地址相同但值不同)——证明进程内存隔离(COW机制)
    }
    return 0;
}

代码说明

  • 线程修改全局变量后,主线程能看到变化,且地址相同 —— 证明线程共享进程堆内存;
  • 子进程修改全局变量后,父进程看不到变化 —— 证明进程内存隔离(Linux 用 “写时复制 COW” 优化,地址看似相同,实则映射不同物理内存)。

3.2、切换效率:为什么线程比进程 “快”?

你可能听过 “线程切换比进程快”,但快在哪?得从 “切换时要做什么” 说起。

比如腾讯会议里有两个任务:A(处理用户进房)、B(转发音视频)。

  • 要是 A 和 B 是同一进程的两个线程:切换时,OS 只需要换 “执行流信息”—— 比如把 A 的 PC 值、寄存器数据存到 TCB 里,再把 B 的 TCB 数据加载到 CPU,搞定。全程不用碰 “仓库”(地址空间)、“货车”(文件描述符),开销大概 1~5 微秒。
  • 要是 A 和 B 是不同进程的线程:切换时,除了换执行流,还得换 “资源上下文”—— 比如把进程 1 的虚拟地址空间映射(MMU 表)换成进程 2 的,刷新 TLB(地址映射的缓存,相当于快递站的 “路线缓存”)。TLB 一刷新,CPU 得重新加载映射,光这一步就比线程切换多花 10 倍时间,总开销大概 10~100 微秒。

这就是为什么 “同一进程内的线程并发,比多进程并发快”—— 省了 “资源切换” 的开销。

3.3、为什么 “线程崩了,全锅端”?

这是面试高频考点,也是腾讯会议这类业务最在意的点之一。

比如:腾讯会议的 “媒体解码线程” 因为遇到异常数据崩溃了 ——

  • 要是用多线程:这个线程崩溃会触发进程级的信号(比如 SIGSEGV 段错误)。OS 会认为 “整个进程出问题了”,直接把进程杀掉,同一进程里的 “房间管理线程”“信令线程” 也会跟着殉葬 —— 相当于 “一个快递员摔了,整个快递站关门”。
  • 要是用多进程:媒体解码是独立进程,它崩了,OS 只会回收它的资源,其他进程(比如房间进程)完全不受影响 —— 相当于 “这家快递站倒闭,隔壁快递站照样送件”。

所以结论很明确:要隔离故障,用多进程;要高效共享,用多线程,但得做好异常防护

用 C 代码模拟 “线程访问非法地址崩溃,导致整个进程退出”(腾讯会议的媒体解码线程就可能遇到这种情况):

#include <stdio.h>
#include <pthread.h>
#include <signal.h>
// 线程函数:访问非法地址(触发段错误)
void* crash_thread(void* arg) {
    int* p = NULL;
    *p = 100; // 非法访问NULL指针,触发SIGSEGV信号
    return NULL;
}
// 信号处理函数:捕获SIGSEGV
void sig_handler(int sig) {
    printf("捕获到信号%d(段错误),进程即将退出\n", sig);
    _exit(1);
}
int main() {
    // 注册信号处理函数(仅演示,实际开发中需谨慎)
    signal(SIGSEGV, sig_handler);
    pthread_t tid;
    pthread_create(&tid, NULL, crash_thread, NULL);
    pthread_join(tid, NULL);
    // 以下代码不会执行——因为线程崩溃触发进程退出
    printf("主线程:我还活着?\n");
    return 0;
}

代码说明

  • 子线程访问 NULL 指针,触发 SIGSEGV(段错误)信号;
  • 即使注册了信号处理函数,进程最终还是会退出 —— 证明 “单个线程崩溃会导致整个进程终止”;
  • 若用多进程,子进程崩溃只会自己退出,父进程不受影响(可通过waitpid回收子进程,再重启新进程)。

3.4、通信与同步:怎么 “传消息” 不乱套?

不管用多进程还是多线程,都得解决 “数据互通” 的问题 —— 这也是腾讯会议后台的核心场景(比如房间进程要给媒体进程传数据)。

(1)多进程通信(IPC):腾讯会议常用方式

不同进程是隔离的,要通信得靠 OS 提供的 “桥梁”。咱们选腾讯会议高频使用的两种方式,写代码示例:

方式 1:共享内存(最快,适合媒体数据传输)

共享内存是 “零拷贝” IPC,适合传输大体积数据(如 H.264 码流),但需配合互斥锁保证同步:

#include <stdio.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <string.h>
// 共享内存key(需唯一)
#define SHM_KEY 0x123456
// 互斥锁key
#define SEM_KEY 0x654321
// 共享内存大小(假设传输媒体码流片段)
#define SHM_SIZE 1024
// P操作(加锁)
void sem_p(int sem_id) {
    struct sembuf sb = {0, -1, SEM_UNDO};
    semop(sem_id, &sb, 1);
}
// V操作(解锁)
void sem_v(int sem_id) {
    struct sembuf sb = {0, 1, SEM_UNDO};
    semop(sem_id, &sb, 1);
}
int main() {
    // 1. 创建互斥锁(保证共享内存同步)
    int sem_id = semget(SEM_KEY, 1, IPC_CREAT | 0666);
    semctl(sem_id, 0, SETVAL, 1); // 初始值1(解锁状态)
    // 2. 创建共享内存
    int shm_id = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | 0666);
    char* shm_addr = shmat(shm_id, NULL, 0); // 挂载共享内存
    pid_t pid = fork();
    if (pid == 0) { // 子进程(模拟媒体进程:读共享内存)
        sem_p(sem_id); // 加锁
        printf("子进程(媒体进程)读取共享内存:%s\n", shm_addr);
        sem_v(sem_id); // 解锁
        // 释放资源
        shmdt(shm_addr);
        _exit(0);
    } else { // 父进程(模拟房间进程:写共享内存)
        sem_p(sem_id); // 加锁
        strcpy(shm_addr, "H.264码流片段:0x123456..."); // 写入媒体数据
        sem_v(sem_id); // 解锁
        wait(NULL);
        // 释放资源
        shmdt(shm_addr);
        shmctl(shm_id, IPC_RMID, NULL);
        semctl(sem_id, 0, IPC_RMID);
    }
    return 0;
}

方式 2:Unix 域 Socket(可靠,适合服务调用)

Unix 域 Socket 比网络 Socket 快(无需走 TCP/IP 协议栈),适合进程间 “可靠的小数据通信”(如房间进程给媒体进程发 “解码指令”):

#include <stdio.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#include <string.h>
#define SOCK_PATH "/tmp/tencent_meeting.sock"
int main() {
    pid_t pid = fork();
    if (pid == 0) { // 子进程(模拟媒体进程:服务端)
        int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (sockfd < 0) { perror("socket"); return 1; }
        // 绑定地址
        struct sockaddr_un addr;
        memset(&addr, 0, sizeof(addr));
        addr.sun_family = AF_UNIX;
        strcpy(addr.sun_path, SOCK_PATH);
        unlink(SOCK_PATH); // 防止旧文件残留
        bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
        listen(sockfd, 5);
        int connfd = accept(sockfd, NULL, NULL);
        // 读取房间进程的指令
        char buf[128] = {0};
        read(connfd, buf, sizeof(buf));
        printf("媒体进程收到指令:%s\n", buf);
        // 回复确认
        write(connfd, "解码指令已接收", strlen("解码指令已接收"));
        close(connfd);
        close(sockfd);
        unlink(SOCK_PATH);
        _exit(0);
    } else { // 父进程(模拟房间进程:客户端)
        sleep(1); // 等待服务端启动
        int sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
        if (sockfd < 0) { perror("socket"); return 1; }
        // 连接服务端
        struct sockaddr_un addr;
        memset(&addr, 0, sizeof(addr));
        addr.sun_family = AF_UNIX;
        strcpy(addr.sun_path, SOCK_PATH);
        connect(sockfd, (struct sockaddr*)&addr, sizeof(addr));
        // 发送解码指令
        write(sockfd, "请解码用户A的视频流(ID:123)", strlen("请解码用户A的视频流(ID:123)"));
        // 接收确认
        char buf[128] = {0};
        read(sockfd, buf, sizeof(buf));
        printf("房间进程收到回复:%s\n", buf);
        close(sockfd);
        wait(NULL);
    }
    return 0;
}

(2)多线程同步:腾讯会议常用工具

线程共享进程资源,必须用同步工具避免 “数据竞争”。咱们选腾讯会议高频使用的两种工具,写代码示例:

方式 1:互斥锁(Mutex)—— 保护临界区

比如 “房间用户状态更新”,多个线程同时修改用户列表,必须加互斥锁:

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#define USER_NUM 5
// 用户状态结构体(临界资源)
typedef struct {
    int id;
    int is_mute; // 0:未静音,1:静音
} User;
User room_users[USER_NUM] = {
    {1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}
};
pthread_mutex_t mutex; // 互斥锁
// 线程函数:修改用户静音状态
void* set_mute(void* arg) {
    int user_id = *(int*)arg;
    // 加锁:进入临界区
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < USER_NUM; i++) {
        if (room_users[i].id == user_id) {
            room_users[i].is_mute = 1;
            printf("线程%d:用户%d已静音\n", (int)pthread_self()%1000, user_id);
            break;
        }
    }
    // 解锁:退出临界区
    pthread_mutex_unlock(&mutex);
    return NULL;
}
int main() {
    pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
    pthread_t tids[3];
    int user_ids[3] = {2, 4, 2}; // 两个线程修改用户2的状态
    // 创建3个线程,模拟多线程修改用户状态
    for (int i = 0; i < 3; i++) {
        pthread_create(&tids[i], NULL, set_mute, &user_ids[i]);
    }
    for (int i = 0; i < 3; i++) {
        pthread_join(tids[i], NULL);
    }
    // 打印最终用户状态
    printf("\n最终用户状态:\n");
    for (int i = 0; i < USER_NUM; i++) {
        printf("用户%d:%s\n", room_users[i].id, room_users[i].is_mute ? "静音" : "未静音");
    }
    pthread_mutex_destroy(&mutex); // 销毁互斥锁
    return 0;
}

方式 2:条件变量(Condition Variable)—— 等待通知机制

比如 “媒体线程等待码流数据”,用条件变量避免 “忙等”(浪费 CPU):

#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define BUF_SIZE 1024
// 缓冲区(存储媒体码流)
char buf[BUF_SIZE] = {0};
int has_data = 0; // 标记缓冲区是否有数据
pthread_mutex_t mutex;
pthread_cond_t cond;
// 线程1:生产数据(模拟接收网络码流)
void* producer(void* arg) {
    sleep(2); // 模拟网络延迟
    pthread_mutex_lock(&mutex);
    strcpy(buf, "H.264码流:0x789ABC...");
    has_data = 1;
    printf("生产者线程:已写入码流数据\n");
    pthread_cond_signal(&cond); // 通知消费者:数据已准备好
    pthread_mutex_unlock(&mutex);
    return NULL;
}
// 线程2:消费数据(模拟解码码流)
void* consumer(void* arg) {
    pthread_mutex_lock(&mutex);
    // 若缓冲区无数据,等待条件变量(自动释放互斥锁)
    while (!has_data) {
        printf("消费者线程:等待码流数据...\n");
        pthread_cond_wait(&cond, &mutex);
    }
    // 被唤醒后,重新持有互斥锁,处理数据
    printf("消费者线程:解码码流数据:%s\n", buf);
    has_data = 0;
    pthread_mutex_unlock(&mutex);
    return NULL;
}
int main() {
    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cond, NULL);
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, producer, NULL);
    pthread_create(&tid2, NULL, consumer, NULL);
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

Part4什么时候用多进程/多线程?

那腾讯会议里,什么时候用多进程?什么时候用多线程呢?

搞懂了差异,选型就简单了 —— 核心是 “业务需要什么特性”。咱们结合腾讯会议的场景,用一张决策表总结:

多进程 vs 多线程选型决策表

业务诉求

优先选多进程?

优先选多线程?

腾讯会议场景示例

故障隔离(崩了不影响他人)

媒体解码模块、安全沙箱

资源限额控制(限 CPU / 内存)

大客户专属房间、租户隔离

高频数据共享(低延迟)

房间用户状态管理、信令缓存

IO 密集型任务(高并发)

信令转发、网络连接管理

CPU 密集型任务(低切换)

✅(线程池)

同房间多路媒体流解码

跨机器扩展(分布式)

✅(进程独立)

❌(线程绑定进程)

多服务器部署房间进程

选型避坑:别踩这些 “想当然” 的错

  • 错 1:“多线程一定比多进程快”→ 只有 “同一进程内的线程” 才快!跨进程的线程切换(如进程 A→进程 B 的线程),开销和进程切换一样大。
  • 错 2:“多进程不用考虑同步”→ 只要共享资源(如共享内存、同一个文件),就必须同步!比如两个媒体进程写同一个日志文件,不加锁会导致日志错乱。
  • 错 3:“线程崩溃能捕获”→ Linux 里线程崩溃会触发进程信号,没法单独捕获。要是想容错,只能用多进程 + 监控重启(如腾讯会议的 “主监控进程”)。

Part5多进程 + 多线程

实际开发里,很少纯用多进程或纯用多线程 —— 大多是 “外层多进程隔离,内层多线程提效”。咱们看腾讯会议的经典配合模式:“多进程分片 + 进程内线程池” 。

5.1、整体架构

[视频处理服务]  → 独立进程
  │
  └── 3个线程:视频编码、视频解码、网络传输
[音频处理服务]  → 独立进程
  │
  └── 4个线程:音频编码、音频解码、噪声抑制、网络传输
[信令处理服务]  → 独立进程
  │
  └── 5个线程:用户管理、房间管理、消息分发

5.2、核心配合逻辑

以 “百万级房间管理” 为例,架构分为两层:

第一层:多进程分片(隔离 + 负载均衡)

  • 按 “房间 ID 哈希” 拆分多个 “房间进程”—— 比如服务器有 16 核 CPU,开 4 个房间进程,每个进程管 25% 的房间;
  • 每个进程用 cgroup 限制资源(如 CPU≤4 核,内存≤8GB),避免单进程过载;

代码框架(进程启动与分片):

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdint.h>
// 房间ID哈希函数(简化版)
uint32_t hash_room_id(uint64_t room_id, int process_num) {
    return room_id % process_num;
}
// 房间进程逻辑
void room_process(int process_id, int process_num) {
    printf("房间进程%d启动:负责哈希值为%d的房间\n", process_id, process_id);
    // 1. 初始化线程池(IO线程池、业务线程池、定时线程池)
    // 2. 监听分配给自己的房间请求(如通过负载均衡器转发)
    // 3. 处理房间逻辑(用户进/出、状态更新等)
    while (1) { sleep(1); } // 模拟运行
}
int main() {
    int process_num = 4; // 启动4个房间进程(根据CPU核心数调整)
    // 启动多个房间进程
    for (int i = 0; i < process_num; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            room_process(i, process_num);
            _exit(0);
        }
    }
    // 主监控进程:监控子进程,崩了重启
    while (1) {
        pid_t dead_pid = waitpid(-1, NULL, WNOHANG);
        if (dead_pid > 0) {
            printf("房间进程%d崩溃,重启中...\n", dead_pid);
            // 重启逻辑:重新fork一个进程,分配原进程的哈希分片
            pid_t new_pid = fork();
            if (new_pid == 0) {
                int dead_process_id = hash_room_id(dead_pid, process_num); // 简化逻辑
                room_process(dead_process_id, process_num);
                _exit(0);
            }
        }
        sleep(1);
    }
    return 0;
}

第二层:进程内线程池(高效并发)

每个房间进程内,按 “任务类型” 拆分线程池,避免线程频繁创建销毁:

#include <stdio.h>
#include <pthread.h>
#include <queue>
#include <string.h>
using namespace std;
#define IO_THREAD_NUM 4    // IO线程数(处理网络请求)
#define BUSINESS_THREAD_NUM 8 // 业务线程数(处理房间逻辑)
#define TASK_QUEUE_SIZE 1024 // 任务队列大小
// 任务类型
typedef enum {
    TASK_IO,      // IO任务(如接收信令)
    TASK_BUSINESS // 业务任务(如用户进房)
} TaskType;
// 任务结构体
typedef struct {
    TaskType type;
    char data[128]; // 任务数据(如信令内容、用户ID)
} Task;
// 线程池结构体
typedef struct {
    queue<Task> io_queue;      // IO任务队列
    queue<Task> business_queue;// 业务任务队列
    pthread_mutex_t io_mutex;  // IO队列互斥锁
    pthread_mutex_t business_mutex; // 业务队列互斥锁
    pthread_cond_t io_cond;    // IO队列条件变量
    pthread_cond_t business_cond; // 业务队列条件变量
    int stop; // 线程池停止标记
} ThreadPool;
ThreadPool g_thread_pool;
// IO线程函数:处理IO任务(如接收信令)
void* io_thread_func(void* arg) {
    while (!g_thread_pool.stop) {
        pthread_mutex_lock(&g_thread_pool.io_mutex);
        // 等待IO任务
        while (g_thread_pool.io_queue.empty() && !g_thread_pool.stop) {
            pthread_cond_wait(&g_thread_pool.io_cond, &g_thread_pool.io_mutex);
        }
        if (g_thread_pool.stop) break;
        // 取出任务处理
        Task task = g_thread_pool.io_queue.front();
        g_thread_pool.io_queue.pop();
        pthread_mutex_unlock(&g_thread_pool.io_mutex);
        printf("IO线程%d:处理IO任务:%s\n", (int)pthread_self()%1000, task.data);
        // 实际逻辑:接收信令后,解析并转发给业务队列
        pthread_mutex_lock(&g_thread_pool.business_mutex);
        g_thread_pool.business_queue.push(task);
        pthread_cond_signal(&g_thread_pool.business_cond);
        pthread_mutex_unlock(&g_thread_pool.business_mutex);
    }
    return NULL;
}
// 业务线程函数:处理业务任务(如用户进房)
void* business_thread_func(void* arg) {
    while (!g_thread_pool.stop) {
        pthread_mutex_lock(&g_thread_pool.business_mutex);
        // 等待业务任务
        while (g_thread_pool.business_queue.empty() && !g_thread_pool.stop) {
            pthread_cond_wait(&g_thread_pool.business_cond, &g_thread_pool.business_mutex);
        }
        if (g_thread_pool.stop) break;
        // 取出任务处理
        Task task = g_thread_pool.business_queue.front();
        g_thread_pool.business_queue.pop();
        pthread_mutex_unlock(&g_thread_pool.business_mutex);
        printf("业务线程%d:处理业务任务:%s\n", (int)pthread_self()%1000, task.data);
        // 实际逻辑:处理用户进房、状态更新等
    }
    return NULL;
}
// 初始化线程池
void thread_pool_init() {
    g_thread_pool.stop = 0;
    pthread_mutex_init(&g_thread_pool.io_mutex, NULL);
    pthread_mutex_init(&g_thread_pool.business_mutex, NULL);
    pthread_cond_init(&g_thread_pool.io_cond, NULL);
    pthread_cond_init(&g_thread_pool.business_cond, NULL);
    // 创建IO线程
    pthread_t io_tids[IO_THREAD_NUM];
    for (int i = 0; i < IO_THREAD_NUM; i++) {
        pthread_create(&io_tids[i], NULL, io_thread_func, NULL);
    }
    // 创建业务线程
    pthread_t business_tids[BUSINESS_THREAD_NUM];
    for (int i = 0; i < BUSINESS_THREAD_NUM; i++) {
        pthread_create(&business_tids[i], NULL, business_thread_func, NULL);
    }
}
// 向IO队列添加任务(模拟接收信令)
void add_io_task(const char* data) {
    pthread_mutex_lock(&g_thread_pool.io_mutex);
    if (g_thread_pool.io_queue.size() < TASK_QUEUE_SIZE) {
        Task task = {TASK_IO, {0}};
        strncpy(task.data, data, sizeof(task.data)-1);
        g_thread_pool.io_queue.push(task);
        pthread_cond_signal(&g_thread_pool.io_cond);
    } else {
        printf("IO任务队列满,丢弃任务:%s\n", data);
    }
    pthread_mutex_unlock(&g_thread_pool.io_mutex);
}
int main() {
    thread_pool_init();
    // 模拟添加IO任务(如接收用户进房请求)
    add_io_task("用户123请求进房(房间ID:456)");
    add_io_task("用户456请求切换麦克风(房间ID:456)");
    while (1) { sleep(1); } // 模拟运行
    // 停止线程池(实际开发中需处理)
    g_thread_pool.stop = 1;
    pthread_cond_broadcast(&g_thread_pool.io_cond);
    pthread_cond_broadcast(&g_thread_pool.business_cond);
    return 0;
}

Part6面试延伸

面试官问完 “怎么选、怎么配”,大概率会追问这些问题,咱们提前准备好:

1. “腾讯会议怎么避免多线程死锁?”

死锁的四个条件:互斥、持有等待、不可剥夺、循环等待。只要破坏一个,就能避免死锁。腾讯会议的做法是:

(1)破坏 “循环等待”:按固定顺序申请锁

比如访问 “房间锁” 和 “用户锁” 时,永远先申请 ID 小的锁:

// 房间锁(ID:100)和用户锁(ID:200)
pthread_mutex_t room_mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t user_mutex = PTHREAD_MUTEX_INITIALIZER;
int room_mutex_id = 100;
int user_mutex_id = 200;
// 按锁ID从小到大申请
void lock_two_mutexes(pthread_mutex_t* m1, int id1, pthread_mutex_t* m2, int id2) {
    if (id1 < id2) {
        pthread_mutex_lock(m1);
        pthread_mutex_lock(m2);
    } else {
        pthread_mutex_lock(m2);
        pthread_mutex_lock(m1);
    }
}
// 释放锁(按申请逆序)
void unlock_two_mutexes(pthread_mutex_t* m1, pthread_mutex_t* m2) {
    pthread_mutex_unlock(m2);
    pthread_mutex_unlock(m1);
}
// 业务函数:同时操作房间和用户
void operate_room_and_user() {
    lock_two_mutexes(&room_mutex, room_mutex_id, &user_mutex, user_mutex_id);
    // 处理逻辑...
    unlock_two_mutexes(&room_mutex, &user_mutex);
}

(2)破坏 “持有等待”:一次性申请所有锁

用pthread_mutex_timedlock实现 “超时放弃”,避免持有一个锁等待另一个:

#include <time.h>
// 一次性申请两个锁,超时返回失败
int lock_two_mutexes_timeout(pthread_mutex_t* m1, pthread_mutex_t* m2, struct timespec* timeout) {
    // 1. 尝试申请第一个锁
    if (pthread_mutex_timedlock(m1, timeout) != 0) {
        return -1; // 超时,放弃
    }
    // 2. 尝试申请第二个锁
    if (pthread_mutex_timedlock(m2, timeout) != 0) {
        pthread_mutex_unlock(m1); // 申请失败,释放第一个锁
        return -1;
    }
    return 0;
}
void business_func() {
    struct timespec timeout;
    clock_gettime(CLOCK_REALTIME, &timeout);
    timeout.tv_sec += 1; // 超时时间1秒
    if (lock_two_mutexes_timeout(&room_mutex, &user_mutex, &timeout) != 0) {
        printf("申请锁超时,重试或放弃\n");
        return;
    }
    // 处理逻辑...
    pthread_mutex_unlock(&user_mutex);
    pthread_mutex_unlock(&room_mutex);
}

2. “线程池的核心参数怎么配?”

核心看任务类型,腾讯会议的配置经验:

任务类型

核心线程数

最大线程数

队列大小

腾讯会议场景示例

IO 密集型(信令)

CPU 核心数 × 2

CPU 核心数 × 4

1024~4096

信令接收、网络连接管理

CPU 密集型(解码)

CPU 核心数(如 8 核→8 线程)

CPU 核心数 + 1(如 8 核→9)

256~512

媒体流解码、加密解密

定时任务

2~4(固定)

4(固定)

128

房间心跳、超时清理

代码配置示例

// 根据CPU核心数动态配置线程池
#include <sys/sysinfo.h>
void dynamic_thread_pool_init() {
    int cpu_core = get_nprocs(); // 获取CPU核心数
    int io_core_num = cpu_core * 2;
    int business_core_num = cpu_core;
    printf("CPU核心数:%d,IO线程数:%d,业务线程数:%d\n", cpu_core, io_core_num, business_core_num);
    // 后续按计算出的线程数创建线程池...
}

总结

最后再回到面试题:其实没有 “绝对该用多进程” 或 “绝对该用多线程” 的场景 —— 关键是看业务需要什么。

比如腾讯会议:

  • 要隔离故障,就用多进程拆模块(媒体解码、安全沙箱);
  • 要高效并发,就用多线程做细粒度执行(信令转发、同房间解码);
  • 二者配合,就是 “用多进程搭好安全的架子,用多线程填高效的细节”。

记住:面试官问这题,不是要你背答案,而是要你证明 —— 你懂技术的本质,还能结合业务写出关键代码,落地到实际场景。把 “图示 + 代码” 讲清楚,把 “腾讯会议的场景” 带进去,这题就稳了。

往期文章推荐

为什么很多人劝退学 C++,但大厂核心岗位还是要 C++?

【大厂标准】Linux C/C++ 后端进阶学习路线

音视频流媒体高级开发-学习路线

C++ Qt学习路线一条龙!(桌面开发&嵌入式开发)

Linux内核学习指南,硬核修炼手册

C/C++ 高频八股文面试题1000题(三)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值