关于fork的多线程问题

本文探讨了在多线程环境下使用POSIX标准的fork行为带来的问题,特别是锁的复制导致的死锁风险,并提出了解决方案。

转载自:http://blog.codingnow.com/2011/01/fork_multi_thread.html

 

主要意思:

在 POSIX 标准中,fork 的行为是这样的:复制整个用户空间的数据(通常使用 copy-on-write 的策略,所以可以实现的速度很快)以及所有系统对象,然后仅复制当前线程到子进程。这里:所有父进程中别的线程,到了子进程中都是突然蒸发掉的。

其它线程的突然消失,是一切问题的根源。

极不和谐的 fork 多线程程序

继续前几天的话题。做梦幻西游服务器优化的事情。以往的代码,定期存盘的工作分两个步骤,把 VM 里的动态数据序列化,然后把序列化后的数据写盘。这两个步骤,序列化工作并没有独立在单独线程/进程里做,而是放在主线程的。IO 部分则在一个独立进程中。

序列化任务是个繁琐的过程。非常耗时(相对于 MMORPG 这个需要对用户请求快速反应的环境)。当玩家同时在线人数升高时,一个简便的优化方法是把整个序列化任务分步完成,分摊到多个心跳内。这里虽然有一些数据一致性问题,但也有不同的手段解决。

但是,在线人数达到一定后,序列化过程依然会对系统性能造成较大影响。在做定期存盘时,玩家的输入反应速度明显变大。表现得是游戏服务器周期性的卡。为了缓解这一点,我希望改造系统,把序列化任务分离到独立进程去做。

方法倒是很简单,在定期存盘一刻,调用 fork ,然后在子进程中慢慢的做序列化工作。(可以考虑使用 nice)做完后,再把数据交到 IO 进程写盘。不过鉴于我们前期设计的问题,具体实现中,我需要通过共享内存把序列化结果交还父进程,由父进程送去 IO 进程。

因为 fork 会产生一个内存快照,所以甚至没有数据一致性问题。这应该是一个网络游戏用到的常见模式。

可问题就出在于,经过历史变迁,我们的服务器已经使用了多线程,这使得 fork 子进程的做法变的不那么可靠,需要自己推敲一下。


多进程的多线程程序,听起来多不靠谱。真是闲得淡疼的人才会做此设计。但依旧可以使用万能的推辞:历史造成的。

在 POSIX 标准中,fork 的行为是这样的:复制整个用户空间的数据(通常使用 copy-on-write 的策略,所以可以实现的速度很快)以及所有系统对象,然后仅复制当前线程到子进程。这里:所有父进程中别的线程,到了子进程中都是突然蒸发掉的。

其它线程的突然消失,是一切问题的根源。

我之前从未写过多进程多线程程序,不过公司里有 David Xu 同学(他实现维护着 FreeBSD 的线程库)是这方面的专家,今天跟徐同学讨论了一下午,终于觉得自己搞明白了其中的纠结。嗯,写点东西整理一下思路。

可能产生的最严重的问题是锁的问题。

因为为了性能,大部分系统的锁是实现在用户空间的。所以锁对象会因为 fork 复制到子进程中。

对于锁来说,从 OS 看,每个锁有一个所有者,即最后一次 lock 它的线程。

假设这么一个环境,在 fork 之前,有一个子线程 lock 了某个锁,获得了对锁的所有权。fork 以后,在子进程中,所有的额外线程都人间蒸发了。而锁却被正常复制了,在子进程看来,这个锁没有主人,所以没有任何人可以对它解锁。

当子进程想 lock 这个锁时,不再有任何手段可以解开了。程序发生死锁。

为何,POSIX 指定标准时,会定下这么一个显然不靠谱的规则?允许复制一个完全死掉的锁?答案是历史和性能。因为历史上,把锁实现在用户态是最方便的(今天依旧如此)。 背后可能只需要一条原子操作指令即可。大多数 CPU 都支持的。fork 只管用户空间的复制,不会涉及其中的对象细节。

一般的惯例,多线程程序 fork 前,应该由发起 fork 的线程 lock 所有子进程可能用到的锁,fork 后,把它们一一 unlock 。当然,这样的做法就隐含了锁的次序。如果次序和平时不同,那么就会死锁。

不光是显式的使用锁,许多 CRT 函数也会间接的使用。比如 fprintf 这些文件操作。因为对 FILE * 的操作是依靠锁来达到线程安全的。最常见的问题是在子线程里调用 fprintf 写 log 。

除此之外,就是要小心一些不依赖锁的数据一致性问题了。比如若在父进程里另一个线程中操作一个链表,fork 发生时,因为其它线程的突然消失,这个链表就可能会因为只操作了一半而是不完整的数据。不过这一般不会是问题,或者可以归咎于对锁的处理。(多个线程,访 问同一块数据。比如一条链表。就是需要加锁的)

最后引用讨论中, David Xu 的话 “POSIX这个问题一直是讨论的热门话题。而且双方立场很清楚,一方是使用者,另外一方是实现者,双方互相指责”


突然想到,lua / java 这些 VM 的实现,是不是可以利用 fork 来缓解 gc 造成的停滞呢?只需要在 gc 时,fork 一份出来做扫描。找到不被引用的垃圾,

 

<think>好的,用户想了解在Linux环境下使用C语言实现fork多线程的示例和用法。首先,我需要回忆fork和线程的基本概念。fork是创建子进程的系统调用,而多线程通常使用pthread库来实现。这两个机制在并发编程中都很重要,但有不同的应用场景。 接下来,我应该先分别给出fork和pthread的示例代码,说明它们的基本用法。比如,fork会复制整个进程,而线程共享进程资源。要注意用户可能对这两者的区别不太清楚,所以需要解释清楚。 然后,用户可能想知道如何同时使用fork多线程,这时候需要举一个结合的例子。但要注意,fork和线程一起使用时可能会有潜在的问题,比如子进程中只复制调用线程,这可能导致死锁或其他问题,需要提醒用户注意。 另外,用户提供的引用中有关于pthread_create、pthread_join的使用,以及编译时需要链接线程库的信息。这些都需要在示例中体现出来,比如在编译命令中加入-lpthread,并处理线程的创建和等待。 还要检查示例代码的正确性,确保语法正确,比如头文件的包含、函数参数的正确性。例如,fork的示例需要包含unistd.h,而多线程需要pthread.h,并且在创建线程时传递正确的参数。 用户可能对fork和线程的区别及适用场景有疑问,所以需要解释何时使用进程,何时使用线程。比如,进程间隔离性好,但开销大;线程轻量但需要处理同步问题。 最后,生成的相关问题需要覆盖fork和线程的基础、区别、使用注意事项以及实际应用,帮助用户进一步学习。同时确保引用正确的参考资料,如引用中的示例和注意事项。</think>以下是在Linux环境下使用C语言实现fork多线程的示例与用法说明: ### 一、fork函数基础用法 fork()用于创建子进程,父子进程代码段相同,数据段独立: ```c #include <unistd.h> #include <stdio.h> int main() { pid_t pid = fork(); if (pid == 0) { // 子进程 printf("Child PID: %d\n", getpid()); } else if (pid > 0) { // 父进程 printf("Parent PID: %d\n", getpid()); } else { perror("fork failed"); } return 0; } ``` 父子进程会从fork()处开始并行执行[^1][^2] ### 二、多线程基础示例 使用pthread库实现多线程,注意编译时需加`-lpthread`参数: ```c #include <pthread.h> #include <stdio.h> void* thread_func(void* arg) { int num = *(int*)arg; printf("Thread %d created\n", num); return NULL; } int main() { pthread_t tid; int arg = 10; if (pthread_create(&tid, NULL, thread_func, &arg)) { perror("Thread create failed"); return 1; } pthread_join(tid, NULL); // 等待线程结束[^5] return 0; } ``` ### 三、fork多线程结合示例 ```c #include <unistd.h> #include <pthread.h> #include <stdio.h> void* thread_func(void* arg) { printf("Thread in process %d\n", getpid()); sleep(1); return NULL; } int main() { pthread_t tid; pthread_create(&tid, NULL, thread_func, NULL); pid_t pid = fork(); if (pid == 0) { printf("Child process %d\n", getpid()); } else { printf("Parent process %d\n", getpid()); } pthread_join(tid, NULL); return 0; } ``` 注意:fork()会复制调用线程,可能导致死锁,需谨慎使用[^2][^5]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值