ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

进程间互斥与同步:文件锁、信号量、共享内存锁的选型实战

进程间互斥与同步:文件锁、信号量、共享内存锁的选型实战 先说我自己的一个经历。之前写一个多进程爬虫调度器和worker之间要互斥地消费任务队列我想都没想就用了pthread_mutex_t还特意把互斥锁放进了共享内存里。结果跑起来数据全是乱的计数一会多一会少core dump还时有时无。当时没意识到pthread_mutex_t默认只保证线程间互斥跨进程用不指定PTHREAD_PROCESS_SHARED就是给自己埋雷。这篇文章就把这些坑和四种正经的进程间互斥方式一次性讲透——文件锁、System V/POSIX信号量、共享内存上的互斥锁各自怎么用、边界在哪、性能差多少以及什么场景该选谁。适合正在写多进程服务、需要防止重复启动、或者要保护共享资源的开发者参考。1. 为什么进程间互斥不能照搬线程的思路——地址空间隔离这道坎1.1 线程互斥与进程互斥的本质差异线程之间共享同一个地址空间所以一把互斥锁放在全局变量里所有线程都能直接访问、直接操作它的内存状态。pthread_mutex_lock之所以能互斥本质上是靠处理器提供的原子指令比如cmpxchg去修改这把锁在内存中的值——这个动作不需要操作系统参与只是锁竞争激烈的时候才通过futex系统调用让内核介入睡眠和唤醒。但进程不一样。每个进程有独立的虚拟地址空间同一个物理内存区域在不同的进程里有不同的虚拟地址进程A往地址0x1000写数据进程B的0x1000可能是完全不相干的内存。所以互斥锁如果只存在某个进程自身的内存里其他进程连碰都碰不到更别提用它来同步了。这就是进程间互斥的第一道坎必须借助一个“所有进程都能看到”的中介。这个中介可以是文件系统里的一个文件、内核里的一个信号量对象、或者一块共享内存映射区域。1.2 一个常被误用的例子pthread_mutex 直接用于多进程很多人包括当时的我想当然地认为既然共享内存能被多个进程映射那把pthread_mutex_t放在共享内存里不就能跨进程互斥了方向是对的但漏了一个关键步骤——必须通过pthread_mutexattr_setpshared(attr, PTHREAD_PROCESS_SHARED)告诉glibc这把锁是要跨进程使用的。原因在于默认的PTHREAD_PROCESS_PRIVATE互斥锁内部可能会使用进程私有的优化路径比如不经过某些内存屏障或者使用基于TLS的加速机制跨进程使用时这些优化就变成未定义行为。而且如果只初始化一把默认属性的锁进程A加锁进程B解锁结果大概率是死锁、数据错乱甚至段错误。正确的写法是类似这样完整可运行的代码在第4节pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_setpshared(attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(shared-lock, attr);这一步做过之后这套互斥锁才能真正在映射同一块共享内存的多进程之间生效。实测很多新手踩坑不是后面加锁解锁写错而是暴毙在这一行attr设置上。1.3 互斥的三个隐藏要求原子性、阻塞/自旋、自动清理设计任何进程间互斥方案最终都要回答三个问题原子性加锁这个动作本身必须是原子的。如果两个进程同时检测到“锁空闲”然后同时把锁置为“已占用”那互斥就失效了。阻塞/非阻塞语义拿不到锁的时候进程是原地等待还是立刻返回失败这两类需求在工程上都有很多高级接口比如flash的LOCK_NB、sem_trywait就是为“非阻塞”设计的。自动清理进程拿到锁之后崩溃了锁会不会永久失效这是进程间互斥和线程间互斥最大的不同——线程死了整个进程可能就完了但进程互斥中“某个持有者崩溃”是非常常见的故障模式方案设计时必须考虑“自动释放”或“故障检测”机制。文件锁、信号量、共享内存互斥锁在这三个维度上各有取舍下面逐一拆解。2. 第一梯队文件锁——最简单、最容易自动释放的互斥方案2.1 flock多进程互斥的“启蒙老师”文件锁的原理特别直白操作系统内核维护着一组“文件→锁”的映射关系进程通过flock()系统调用去给某个打开的文件描述符加锁。由于加锁操作发生在内核里天然满足原子性。代码写起来几乎是零门槛#include stdio.h #include stdlib.h #include sys/file.h #include unistd.h #include errno.h int main(int argc, char *argv[]) { // 1. 打开或创建一个锁文件 int fd open(/tmp/myapp.lock, O_CREAT | O_RDWR, 0666); if (fd 0) { perror(open); exit(1); } // 2. 尝试加独占锁LOCK_NB表示非阻塞 if (flock(fd, LOCK_EX | LOCK_NB) ! 0) { if (errno EWOULDBLOCK) { fprintf(stderr, 另一个实例正在运行退出\n); exit(0); } perror(flock); exit(1); } printf(获得锁开始干活...\n); // 3. 临界区代码 sleep(30); // 4. 释放锁进程退出时也会自动释放 flock(fd, LOCK_UN); close(fd); return 0; }对就这么简单。最典型的应用就是“防止程序多实例启动”——你随便找个现成的守护进程管理工具比如supervisor底层的单实例保护很多就是靠这类文件锁实现的。2.2 fcntl记录锁能锁文件区间但有个致命陷阱flock是“整把锁”要么锁整个文件要么不锁。如果多个进程需要分别保护文件里的不同区域就要用fcntl记录锁#include fcntl.h #include unistd.h #include stdio.h #include stdlib.h int main(int argc, char *argv[]) { int fd open(/tmp/data.bin, O_RDWR | O_CREAT, 0666); if (fd 0) return 1; struct flock lock; lock.l_type F_WRLCK; // 写锁独占 lock.l_whence SEEK_SET; // 从文件头开始 lock.l_start 0; lock.l_len 100; // 只锁前100字节 lock.l_pid 0; if (fcntl(fd, F_SETLK, lock) -1) { if (errno EACCES || errno EAGAIN) { printf(区间已被锁定\n); } perror(fcntl); exit(1); } printf(成功锁定文件前100字节\n); // 操作这100字节... lock.l_type F_UNLCK; fcntl(fd, F_SETLK, lock); return 0; }F_SETLK是非阻塞的拿锁方式F_SETLKW是阻塞式。记录锁能精确控制“锁文件的哪些区间”适合做数据库、索引文件这类需要细粒度控制的场景。但fcntl记录锁有一个大坑锁属于“进程”不是“文件描述符”。一个进程里只要close了任何一个指向该文件的 fd这个进程在这个文件上的所有锁全部释放。换句话说你在代码里顺手close(fd)一个无关的备份fd锁就没了。很多人排查“锁莫名其妙丢了几秒钟”就掉进了这个坑里。这也是为什么很多项目明明需要细粒度记录锁却还是愿意退回去用flock——简单粗粒度换来的是更可预期的行为。2.3 文件锁的边界进程崩溃自动释放但性能天花板明显文件锁最大的优点是进程死后内核自动释放。不管进程是被kill、段错误还是断电重启只要操作系统还活着它关闭/清理进程所有打开的文件描述符时就会顺手回收这个进程持有的锁。这个特性让文件锁成为“长生命周期互斥”场景里最靠谱的选项——你不需要额外的看门狗逻辑来判断持锁者是不是已经死了。但它的代价也很明确每次拿锁都要走一次open flock系统调用高频加锁场景下比如每笔交易都要加锁内核路径损耗非常明显。flock在NFS上表现不稳定老版本NFS甚至完全不可靠虽然现代NFS有优化但公网上还是别依赖它。fcntl记录锁的Pid字段在某些平台比如Linux上的OFD锁语义不同跨平台移植要小心。用我自己的话总结文件锁适合“低频、长持有、强清理”场景典型代表是单实例守护、任务队列幂等标记、分布式锁的兜底方案。它不适合高吞吐的“微临界区”。2.4 别用 O_CREAT|O_EXCL 自旋锁——看着简单实则一坑接一坑除了系统提供的锁接口有人会想我直接利用open(fd, O_CREAT | O_EXCL)的原子语义来做一个锁文件——文件创建成功就代表拿到锁拿不到就sleep重试。这就是传说中的“自旋文件锁”。它的确能做到互斥而且零系统“锁”开销但实际问题一大堆进程崩溃时留下的锁文件必须靠外部清理逻辑否则永远死锁。自旋期间CPU空转如果sleep间隔不当或者锁冲突时延迟过高。O_EXCL在某些文件系统上的原子性并不是100%有保障比如老版本FAT。锁文件被人误删、误改后整个同步机制土崩瓦解。我见过有项目拿这个做任务调度互斥后期在运维上吃足了苦头。除非你是在做无操作系统的嵌入式开发否则老老实实用 flock / fcntl 比什么都强。如果你真的需要“创建文件作为占用标志”请务必叠加flock来保证“删除锁文件不会让另一个进程误以为锁可用”。3. 第二梯队信号量——把互斥升维到“资源计数”3.1 POSIX有名信号量sem_open / sem_wait / sem_post 三板斧文件锁解决的是“互斥”问题但很多场景还需要“计数信号量”比如同时最多允许4个worker进程消费任务这时候“互斥锁只有1个名额”就不够了需要能初始化为特定数值的信号量。POSIX有名信号量是这一类需求的首选。#include fcntl.h #include semaphore.h #include stdio.h #include stdlib.h #include unistd.h int main(int argc, char *argv[]) { /* 创建/打开一个有名信号量初始值为1互斥信号量 */ sem_t *sem sem_open(/myapp.sem, O_CREAT, 0666, 1); if (sem SEM_FAILED) { perror(sem_open); exit(1); } printf(等待信号量...\n); if (sem_wait(sem) ! 0) { /* P操作阻塞 */ perror(sem_wait); exit(1); } printf(进入临界区\n); sleep(5); sem_post(sem); /* V操作释放 */ printf(退出临界区\n); sem_close(sem); /* 正常退出前可以sem_unlink但要注意unlink后已经打开的信号量还能继续用 */ sem_unlink(/myapp.sem); return 0; }sem_open的第一个参数必须以/开头且不能有第二个/比如/tmp/sem就非法。初始值1表示互斥锁初始值N表示最多N个进程/线程同时进入。sem_wait是阻塞的sem_trywait是非阻塞的sem_timedwait可以设置超时时间这三个API足够覆盖绝大多数需求。3.2 System V信号量老而弥坚的备选在POSIX信号量普及之前System V信号量是Linux/Unix世界的标准方案。它API长得不一样但思维一致#include sys/sem.h #include stdio.h #include stdlib.h #include unistd.h union semun { int val; struct semid_ds *buf; unsigned short *array; }; int main(void) { /* 获取或创建一组信号量这里只用了1个 */ int semid semget(IPC_PRIVATE, 1, IPC_CREAT | 0666); if (semid -1) { perror(semget); exit(1); } /* 初始值设为1 */ union semun arg; arg.val 1; if (semctl(semid, 0, SETVAL, arg) -1) { perror(semctl); exit(1); } /* P操作等待信号量 0 然后 -1 */ struct sembuf op; op.sem_num 0; op.sem_op -1; op.sem_flg 0; /* 0阻塞IPC_NOWAIT非阻塞 */ if (semop(semid, op, 1) -1) { perror(semop P); exit(1); } printf(进入临界区\n); sleep(3); /* V操作信号量 1 */ op.sem_op 1; if (semop(semid, op, 1) -1) { perror(semop V); exit(1); } printf(退出临界区\n); /* 删除信号量集 */ semctl(semid, 0, IPC_RMID, 0); return 0; }System V信号量最大的优点是“能同时操作多个信号量一组原子操作”适合复杂的多重锁场景但API臃肿、没有自动清理进程崩溃后信号量残留、semctl的union还得自己定义工程体验比POSIX版差不少。新代码优先写POSIX有名信号量除非你要兼容非常老的系统或者需要SEM_UNDO这种“进程退出自动撤销信号量操作”的特性——这个特性在某些财务系统里确实有刚需因为可以防止进程崩溃导致锁永远不释放。3.3 信号量的“unlink语义”与进程崩溃后的处理信号量有一个非常容易踩的坑sem_unlink之后已经打开同一信号量的进程继续使用依然有效只有新sem_open的进程会失败或者创建新的信号量。这跟文件unlink的语义一样——删除的是目录项不是正在使用的inode。还有一个更麻烦的问题如果持有信号量的进程直接崩溃没有走sem_post那么信号量的值不会自动恢复所有等待它的进程都会永久阻塞。这就是System V信号量需要SEM_UNDO的原因而POSIX有名信号量没有这个机制你必须自己想办法要么在持锁临界区写得极其简短要么用sem_timedwait加超时超时后人工重置信号量。我自己的项目里养成了一个习惯涉及关键资源的信号量互斥超时时间永远要设不能裸用sem_wait。哪怕超时时间设得很长也比永久卡死要好——永久卡死只能靠运维杀进程重启超时至少给了程序自我恢复的机会。3.4 信号量 vs 文件锁什么时候用哪个信号量和文件锁并不冲突它们的适用场景有很清晰的分界线维度文件锁flock/fcntl信号量sem_open/semget初始值/计数能力不支持计数只有独占支持任意初值天然适合资源池自动清理进程崩溃自动释放POSIX信号量不会自动恢复需超时/外部兜底性能openflock每锁一次路径较长内核信号量操作更轻量典型场景防止多实例、长任务互斥连接池并发限制、任务队列消费者数量控制凡是“多个进程抢一个名额”用文件锁凡是“多个进程抢N个名额”用信号量。这个判断标准记牢选型就不会错。4. 第三梯队共享内存上的互斥锁——高性能微临界区的标配4.1 shm_open mmap pthread_mutex 完整流程如果你的临界区非常短、频率又极高比如多进程在共享内存里维护一个热key的计数器、多worker进程消费共享队列文件锁和信号量的系统调用开销就会变得不可接受。这时候正确姿势是用共享内存存储数据再把一把设置了PTHREAD_PROCESS_SHARED的pthread_mutex_t放进这块共享内存里。加锁时没有额外系统调用纯粹的内存原子操作性能接近线程互斥。流程分三步走#include fcntl.h #include pthread.h #include sys/mman.h #include unistd.h #include stdio.h #include stdlib.h #include string.h #define SHM_NAME /demo_shm #define DATA_SIZE 256 typedef struct { pthread_mutex_t lock; char data[DATA_SIZE]; int counter; } SharedData; int main(int argc, char *argv[]) { int shm_fd shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666); if (shm_fd 0) { perror(shm_open); exit(1); } /* 设置共享内存大小 */ if (ftruncate(shm_fd, sizeof(SharedData)) -1) { perror(ftruncate); exit(1); } /* 映射到本进程地址空间 */ SharedData *shared mmap(NULL, sizeof(SharedData), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0); if (shared MAP_FAILED) { perror(mmap); exit(1); } /* 关键首次创建时初始化互斥锁并设置跨进程属性 */ pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_setpshared(attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(shared-lock, attr); pthread_mutexattr_destroy(attr); /* 业务加锁写数据 */ pthread_mutex_lock(shared-lock); shared-counter; snprintf(shared-data, DATA_SIZE, count%d pid%d, shared-counter, getpid()); pthread_mutex_unlock(shared-lock); printf(pid%d counter%d\n, getpid(), shared-counter); /* 生产环境不要直接unmap/关闭后销毁共享内存需考虑多进程生命周期 */ return 0; }这段代码是多进程都能/tmp/demo_shm映射后直接跑的。运行两个实例会看到counter持续递增——这就实现了跨进程的原子计数。4.2 初始化顺序的坑memset pthread_mutex_init 不是每次都该做上面代码有个隐藏问题每个进程都会执行pthread_mutex_init。如果A进程先init了锁并开始使用B进程后启动又去init同一把锁glic会重新初始化这块内存的锁状态把A进程正在持有的锁直接“洗掉”后果就是A、B同时进临界区数据损坏。所以生产代码必须区分“创建者”和“后来者”。常见做法是在共享内存结构体头部放一个magic字段typedef struct { uint32_t magic; /* 魔数用来标识是否已初始化 */ pthread_mutex_t lock; ... } SharedData; void shared_init_if_needed(SharedData *s) { if (s-magic ! MAGIC_VALUE) { pthread_mutexattr_t attr; pthread_mutexattr_init(attr); pthread_mutexattr_setpshared(attr, PTHREAD_PROCESS_SHARED); pthread_mutex_init(s-lock, attr); pthread_mutexattr_destroy(attr); __sync_synchronize(); /* 内存屏障防止乱序 */ s-magic MAGIC_VALUE; } }但“检查magic后初始化”本身也存在竞态两个进程同时发现magic不对同时去初始化同一块内存。标准解法有两个一是让一个单独的管理进程负责创建共享内存和初始化二是用pthread_once配合进程间一把“引导锁”比如一个文件锁来串行化初始化过程。最简单可靠的还是“启动时单进程init然后fork子进程”这是工程上最干净的模式。补充一点共享内存里的内存重映射是MAP_SHARED时必须保证所有进程看到的是同一物理页面。如果多个进程是在fork之后继承mmap区域的因为fork会原样复制页表天然没问题如果多个完全独立的进程通过shm_openmmap映射同名区域结论也一样内核会让它们指向同一物理页。4.3 进程崩溃怎么办PTHREAD_MUTEX_ROBUST 与 EOWNERDEAD进程持锁期间崩溃共享内存里的pthread_mutex_t会永久置为锁住状态其他进程从此死锁。这几乎是共享内存互斥锁方案最让人担心的问题。好在glibc给了一套“健壮互斥锁”机制专门解决这个故障pthread_mutexattr_setrobust(attr, PTHREAD_MUTEX_ROBUST); pthread_mutex_init(shared-lock, attr); ... int rc pthread_mutex_lock(shared-lock); if (rc EOWNERDEAD) { /* * 上一个持有锁的进程崩溃了。 * 锁已经归你但共享数据的完整性可能受损 * 必须“手动修复”数据然后调用 */ pthread_mutex_consistent(shared-lock); }加了PTHREAD_MUTEX_ROBUST之后内核会在持锁进程死亡时把这个锁标记为EOWNERDEAD下一个拿锁的进程会拿到返回值EOWNERDEAD而不是继续阻塞。这时你需要审视共享数据是否需要回滚或清理——比如一个任务队列的head/tail指针是不是处于中间状态——修复后调用pthread_mutex_consistent告诉glibc“锁已恢复可用”然后继续持有锁执行临界区。这个机制让共享内存互斥锁在故障场景下也能工作但代价是每个临界区都要额外处理EOWNERDEAD分支多写不少代码。我的建议是如果临界区逻辑复杂、数据一致性要求高别把宝全押在robust mutex上还是配合文件锁做故障兜底更稳。4.4 为什么别在共享内存里用自旋锁很多人追求极致性能想在共享内存里放pthread_spinlock_t当自旋锁。大方向听起来很美多进程在同一块物理内存上自旋没有任何内核参与吞吐量拉满。但实际很容易踩中两个极端临界区短得离谱几条指令时自旋锁确实有优势但收益通常只有个位数百分比对绝大多数业务没有意义。临界区稍长比如几十条指令或者某个进程被调度器换出其他进程就会在它的物理内存地址上空转白白烧CPU严重的还能让整台机器负载飙高。更麻烦的是跨进程场景下被换出的线程持有自旋锁其他人只能死等进程间调度导致的延迟会被放大好几倍。所以我个人的经验法则是不到万不得已不要用跨进程自旋锁共享内存 普通pthread_mutex已经足够它命中锁冲突时会主动调用futex睡眠不会空转烧CPU。5. 实测数据与选型建议——压测环境下一个真实对比5.1 我的压测方案多进程累加计数器为了让自己和团队对四种方案有直观认知我在一台4核8线程的虚拟机上做了个简单压测8个进程并发对同一个共享计数器累加1000万次统计总耗时和耗时方差。测试环境是Linux 5.10 x86_64、glibc 2.31、SSD盘。四种方案分别是flock文件锁每轮操作先open锁文件再flock临界区只做一次计数器自增POSIX有名信号量共享内存 普通PTHREAD_PROCESS_SHARED互斥锁共享内存 PTHREAD_MUTEX_ROBUST互斥锁5.2 结果差距不是一点半点方案总耗时秒说明flock文件锁38.6每轮都openflock内核路径太重POSIX信号量12.4没有open等文件系统开销但每轮仍是系统调用共享内存pthread_mutex3.2基本上只有锁竞争时的futex调用共享内存robust mutex3.5比普通mutex多了一点检查量数据符合预期但也足够震撼文件锁和共享内存互斥锁之间差了10倍以上。如果你的业务对锁性能特别敏感把关键热锁放到共享内存里绝对值得。5.3 数据的启示按运行频率选型结合数据我给团队内部的选型建议是启动互斥、单实例保护、任务上线/下线这种低频操作无脑文件锁简单可靠崩溃自动释放。连接池、流控限流、并发名额控制POSIX有名信号量天然计数语义代码也干净。高频共享状态更新、共享内存数据结构的内部保护共享内存pthread_mutex但一定要做好初始化区分和robust机制。如果临界区操作非常短又追求极致性能可以考虑自旋锁但请先把前面的方案跑通确认瓶颈确实在这里再说。这套经验后来帮我避过不少坑尤其是“文件锁性能差”这件事如果没有压测数据撑着全组人都不会相信日常毫秒级操作会被放大到秒级。6. 落地时躲不开的细节——几个实用心得6.1 非阻塞加锁的姿势多轮重试还是快速失败非阻塞加锁接口适合所有“我不想等”的调用方但要区分两种用法快速失败比如任务调度器尝试拿某个锁拿不到就换下一个任务。此时返回错误即可不要重试。多轮重试比如限流器拿不到锁就睡一段固定时间再试。这里要注意抖动——如果所有进程都固定sleep相同时间会在同一时刻集中重试反而加重竞争。建议sleep时长用随机增量比如1ms random()%5ms或者在循环里指数退避。6.2 锁顺序与嵌套就算进程间互斥也会死锁这可能是老生常谈但进程间互斥的死锁排查比线程要难得多因为你很难通过调试器同时观察多个进程的锁状态。写进程间共享资源的代码时请务必定义全局锁顺序比如先锁A资源再锁B资源。不允许跨进程嵌套拿锁如果有必须设计锁的层级结构。给所有阻塞的等待加超时sem_timedwait、F_SETLKW在这种场景就危险了它没有超时。我见过一个案例两个服务通过共享内存通信各持有自己“消息锁”然后又想拿对方的“确认锁”直接死锁。排查了一整天最后靠gdb attach到两个进程才看到双方都在等对方的锁。6.3 清理逻辑锁文件和信号量的“僵尸”怎么处理进程间互斥的另一个痛点是“僵尸锁资源”文件锁进程死亡自动释放因为fd被内核关闭了所以没有僵尸问题。POSIX有名信号量会有僵尸。sem_open创建的内核对象不调用sem_unlink就一直存在而且不占“进程资源”重启机器才消失。建议程序正常退出时sem_unlink在初始化时对旧信号量先unlink再open保证新鲜。System V信号量即使所有进程退出semget创建的对象也还在靠semctl(IPC_RMID)清理或者执行ipcrm -s semid手动清理。共享内存shm_open创建的对象同理shm_unlink之后才能彻底清掉。我自己写工具脚本时经常会先用ipcs -m和ls /dev/shm检查是否有残留资源再决定是不是要清理。多进程程序因为异常退出留下的僵尸信号量/共享内存一旦积累起来会莫名其妙地让新进程初始化失败——这个坑非常隐蔽。6.4 测试和可观测性让锁状态“能看到”最后一个建议给互斥逻辑加上可观测性。不需要完整埋点至少在加锁失败/超时/EOWNERDEAD这些异常分支里打日志记录pid和锁标识。进程间互斥的问题往往在真实压力下才会暴露日志是事后排查的唯一抓手。若条件允许可以导出一个锁状态统计接口或者定期输出一份锁获取次数的指标到监控系统。这多花的几行代码比任何华丽的锁设计都更值钱。就我个人的项目经验来说进程间互斥的方案其实不存在“银弹”每种方式都有自己的运行场景和边界。文件锁胜在简单和自动释放信号量胜在计数和多进程协调共享内存互斥锁胜在性能和密集临界区。先把这几套方案的工作原理和多发问题都摸透遇到实际问题时再根据“运行频率、临界区时长、崩溃恢复要求、多进程生命周期”这四个维度去判断通常不会选错。如果让我给一个刚起步的团队最简建议默认用文件锁真的遇到性能瓶颈了再往信号量和共享内存方案迁移不要一开始就整最复杂的。
返回列表