如果你面过腾讯会议的后台开发岗,大概率会遇到这道题:“什么时候用多进程?什么时候用多线程?二者该怎么配合?”
其实这题不只是考你背概念 —— 面试官真正想知道的是:你能不能结合 “高并发、低延迟、高可用” 的会议场景,把技术选型讲明白。
今天咱们就从 “快递站类比” 入手,一步步拆透进程与线程的本质,再结合腾讯会议的实际业务,聊聊怎么选、怎么配,让你不仅能答好面试题,还能搞懂背后的工程逻辑。
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);
// 后续按计算出的线程数创建线程池...
}
总结
最后再回到面试题:其实没有 “绝对该用多进程” 或 “绝对该用多线程” 的场景 —— 关键是看业务需要什么。
比如腾讯会议:
- 要隔离故障,就用多进程拆模块(媒体解码、安全沙箱);
- 要高效并发,就用多线程做细粒度执行(信令转发、同房间解码);
- 二者配合,就是 “用多进程搭好安全的架子,用多线程填高效的细节”。
记住:面试官问这题,不是要你背答案,而是要你证明 —— 你懂技术的本质,还能结合业务写出关键代码,落地到实际场景。把 “图示 + 代码” 讲清楚,把 “腾讯会议的场景” 带进去,这题就稳了。
1457

被折叠的 条评论
为什么被折叠?



