多线程程序中fork导致的一些问题

  最近项目中,在使用多线程和多进程时,遇到了些问题。

  问题描述:在多线程程序中fork出一个新进程,发现新的进程无法正常工作。

  解决办法:将开线程的代码放在fork以后。也就是放在新的子进程中进行创建。

  产生原因:在使用fork时会将原来进程中的所有内存数据复制一份保存在子进程中。但是在拷贝的时候,但是线程是无法被拷贝的。如果在原来线程中加了锁,在使用的时候会造成死锁。以下是具体的例子(转发):

  
  在多线程程序里,在”自身以外的线程存在的状态”下一使用fork的话,就可能引起各种各样的问题.比较典型的例子就是,fork出来的子进程可能会死锁.请不要,在不能把握问题的原委的情况下就在多线程程序里fork子进程.

  能引起什么问题呢?

  那看看实例吧.一执行下面的代码,在子进程的执行开始处调用doit()时,发生死锁的机率会很高.

void* doit(void*) {
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
    pthread_mutex_lock(&mutex);
    struct timespec ts = {10, 0}; nanosleep(&ts, 0); // 10秒寝る
                                                     // 睡10秒
    pthread_mutex_unlock(&mutex);
    return 0;
}

int main(void) {
        pthread_t t;

pthread_create(&t, 0, doit, 0);                                 // 做成并启动子线程
        if (fork() == 0) {
              //子进程
             //在子进程被创建的瞬间,父的子进程在执行nanosleep的场合比较多
              doit(0);

return 0;
        }
        pthread_join(t, 0); //
         // 等待子线程结束
}

以下是说明死锁的理由:
一般的,fork做如下事情
   1. 父进程的内存数据会原封不动的拷贝到子进程中
   2. 子进程在单线程状态下被生成

在内存区域里,静态变量mutex的内存会被拷贝到子进程里.而且,父进程里即使存在多个线程,但它们也不会被继承到子进程里.
fork的这两个特征就是造成死锁的原因.
译者注: 死锁原因的详细解释 ---
   1. 线程里的doit()先执行.
   2. doit执行的时候会给互斥体变量mutex加锁.
   3. mutex变量的内容会原样拷贝到fork出来的子进程中(在此之前,mutex变量的内容已经被线程改写成锁定状态).
   4.子进程再次调用doit的时候,在锁定互斥体mutex的时候会发现它已经被加锁,所以就一直等待,直到拥有该互斥体的进程释放它(实际上没有人拥有这个mutex锁).
   5.线程的doit执行完成之前会把自己的mutex释放,但这是的mutex和子进程里的mutex已经是两份内存.所以即使释放了mutex锁也不会对子进程里的mutex造成什么影响.

例如,请试着考虑下面那样的执行流程,就明白为什么在上面多线程程序里不经意地使用fork就造成死锁了*3.
1.    在fork前的父进程中,启动了线程1和2
2.    线程1调用doit函数
3.    doit函数锁定自己的mutex
4.    线程1执行nanosleep函数睡10秒
5.    在这儿程序处理切换到线程2
6.    线程2调用fork函数
7.    生成子进程
8.    这时,子进程的doit函数用的mutex处于”锁定状态”,而且,解除锁定的线程在子进程里不存在
9.    子进程的处理开始
10.   子进程调用doit函数
11.   子进程再次锁定已经是被锁定状态的mutex,然后就造成死锁

像这里的doit函数那样的,在多线程里因为fork而引起问题的函数,我们把它叫
做”fork-unsafe函数”.反之,不能引起问题的函数叫做”fork-safe函数”.虽然在一些商用的UNIX里,源于OS提供的函数(系统调
用),在文档里有fork-safety的记载,但是在
Linux(glibc)里当然!不会被记载.即使在POSIX里也没有特别的规定,所以那些函数是fork-safe的,几乎不能判别.不明白的话,作
为unsafe考虑的话会比较好一点吧.(2004/9/12追记)Wolfram
Gloger说过,调用异步信号安全函数是规格标准,所以试着调查了一下,在pthread_atforkの这个地方里有” In the
meantime*5, only a short list of async-signal-safe library routines are
promised to be available.”这样的话.好像就是这样.

随便说一下,malloc函数就是一个维持自身固有mutex的典型例子,通常情况下它是fork-unsafe的.依赖于malloc函数的函数有很多,例如printf函数等,也是变成fork-unsafe的.

直到目前为止,已经写上了thread+fork是危险的,但是有一个特例需要告诉大家.”fork后马上调用exec的场合,是作为一个特列不会产生问题的”. 什么原因呢..?exec函数*6一被调用,进程的”内存数据”就被临时重置成非常漂亮的状态.因此,即使在多线程状态的进程里,fork后不马上调用一切危险的函数,只是调用exec函数的话,子进程将不会产生任何的误动作.但是,请注意这里使用的”马上”这个词.即使exec前仅仅只是调用一回printf(“I’m
child process”),也会有死锁的危险.

译者注:exec函数里指明的命令一被执行,该命令的内存映像就会覆盖父进程的内存空间.所以,父进程里的任何数据将不复存在.

本blog的理解:查看前面进程创建中,子进程在创建后,是写时复制的,也就是子进程刚创建时,与父进程一样的副本,当exce后,那么老的地址空间被丢弃,而被新的exec的命令的内存的印像覆盖了进程的内存空间,所以锁的状态无关紧要了。
如何规避灾难呢?
为了在多线程的程序中安全的使用fork,而规避死锁问题的方法有吗?试着考虑几个.

规避方法1:做fork的时候,在它之前让其他的线程完全终止.
在fork之前,让其他的线程完全终止的话,则不会引起问题.但这仅仅是可能的情况.还有,因为一些原因而其他线程不能结束就执行了fork的时候,就会是产生出一些解析困难的不具合的问题.

规避方法2:fork后在子进程中马上调用exec函数

(2004/9/11 追记一些忘了写的东西)
不用使用规避方法1的时候,在fork后不调用任何函数(printf等)就马上调用execl等,exec系列的函数.如果在程序里不使用”没有exec就fork”的话,这应该就是实际的规避方法吧.
译者注:笔者的意思可能是把原本子进程应该做的事情写成一个单独的程序,编译成可执行程序后由exec函数来调用.

规避方法3:”其他线程”中,不做fork-unsafe的处理
除了调用fork的线程,其他的所有线程不要做
fork-unsafe的处理.为了提高数值计算的速度而使用线程的场合*7,这可能是fork-
safe的处理,但是在一般的应用程序里则不是这样的.即使仅仅是把握了那些函数是fork-safe的,做起来还不是很容易的.fork-safe函
数,必须是异步信号安全函数,而他们都是能数的过来的.因此,malloc/new,printf这些函数是不能使用的.
规避方法4:使用pthread_atfork函数,在即将fork之前调用事先准备的回调函数.apue中详细介绍了它
使用pthread_atfork函数,在即将
fork之前调用事先准备的回调函数,在这个回调函数内,协商清除进程的内存数据.但是关于OS提供的函数
(例:malloc),在回调函数里没有清除它的方法.因为malloc里使用的数据结构在外部是看不见的.因此,pthread_atfork函数几乎
是没有什么实用价值的.
规避方法5:在多线程程序里,不使用fork
就是不使用fork的方法.即用pthread_create来代替fork.这跟规避策2一样都是比较实际的方法,值得推荐.

*1:生成子进程的系统调用
*2:全局变量和函数内的静态变量
*3:如果使用Linux的话,查看pthread_atfork函数的man手册比较好.关于这些流程都有一些解释.
*4:Solaris和HP-UX等
*5:从fork后到exec执行的这段时间
*6:≒execve系统调用
*7:仅仅做四则演算的话就是fork-safe的

  总结:在程序正常运行时出现不能正常工作,而在调试时又能正常工作。则可以考虑死锁的情况!

      

时间: 2024-12-26 21:41:35

多线程程序中fork导致的一些问题的相关文章

多线程程序中操作的原子性

[转]http://www.parallellabs.com/2010/04/15/atomic-operation-in-multithreaded-application/ 多线程程序中操作的原子性 0. 背景 原子操作就 是不可再分的操作.在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是一些常见的多线程Bug的源头.本文主要讨论了三 个问题:1. 多线程程序中对变量的读写操作是否是原子的?2. 多线程程序中对Bit field(位域)的读写操作是否是线程安全

多线程程序与fork()

前言:exce调用并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了当前进程的正文.数据.堆和栈段 多线程程序里不准使用fork :为什么??? UNIX上C++程序设计守则3 准则3:多线程程序里不准使用fork 能引起什么问题呢? 那看看实例吧.一执行下面的代码,在子进程的执行开始处调用doit()时,发生死锁的机率会很高. void* doit(void*) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITI

CountDownLatch在多线程程序中的应用

一.CountDownLatch介绍 CountDownLatch是JDK1.5之后引入的,存在于java.util.concurrent包下,能够使一个线程等待其他线程完成动作后再执行.构造方法: 1 public CountDownLatch(int count) { 2 if (count < 0) throw new IllegalArgumentException("count < 0"); 3 this.sync = new Sync(count); 4 } 主

C#OOP之十二 创建多线程程序

线程简介 在讲线程之前,我们先来看几个概念,程序.进程. 程序:为了让计算机执行某些操作或解决某个问题而编写的一系列有序指令的集合. 进程:进程通俗的讲,就是正在执行的程序.一个进程通常就是一个正在执行的应用程序.Windows提供了"任务管理器",如下图所示: 1教室.exe为海天屏幕广播程序的进程:飞球.exe为飞球的进程:sqlserver.exe为Sql Server2005的进程:当我们启动了一个应用程序(或者说一个任务)时通常也就启动了一个进程.从上图可以看出每一个进程占一

[转]极不和谐的 fork 多线程程序

极不和谐的 fork 多线程程序 继续前几天的话题.做梦幻西游服务器优化的事情.以往的代码,定期存盘的工作分两个步骤,把 VM 里的动态数据序列化,然后把序列化后的数据写盘.这两个步骤,序列化工作并没有独立在单独线程/进程里做,而是放在主线程的.IO 部分则在一个独立进程中. 序列化任务是个繁琐的过程.非常耗时(相对于 MMORPG 这个需要对用户请求快速反应的环境).当玩家同时在线人数升高时,一个简便的优化方法是把整个序列化任务分步完成,分摊到多个心跳内.这里虽然有一些数据一致性问题,但也有不

java程序中的多线程(转)

为什么会排队等待? 下面的这个简单的 Java 程序完成四项不相关的任务.这样的程序有单个控制线程,控制在这四个任务之间线性地移动.此外,因为所需的资源 ― 打印机.磁盘.数据库和显示屏 -- 由于硬件和软件的限制都有内在的潜伏时间,所以每项任务都包含明显的等待时间.因此,程序在访问数据库之前必须等待打印机完成打印文件的任务,等等.如果您正在等待程序的完成,则这是对计算资源和您的时间的一种拙劣使用.改进此程序的一种方法是使它成为多线程的. 四项不相关的任务 class myclass { sta

浅谈多线程在java程序中的应用

在一个高并发的网站中,多线程是必不可少的.下面先说一下多线程在程序中的作用.1.提高前端请求的响应速度.当我们执行一个比较耗时的方法时,http请求得不到响应甚至会超时,这时如果业务上允许数据的延迟,我们可以使用多线程来进行处理比较耗时的方法.这样前端发送了请求,后端令开启了一个线程去处理任务,就不会阻塞主线程了.2.减清服务器的压力.包括我们的web容器,如tomcat.jetty等,还有数据库服务器等.因为我们使用了多线程,并且线程池大小有限制,如30,那么同时请求数据库的链接就限制为30了

MO拆分计划行程序中写入PRODUCTIONORDERS表数据出现重复导致报错(BUG)20180502

错误提示:ORA-00001: 违反唯一约束条件 (ABPPMGR.C0248833319_6192)ORA-06512: 在 "STG.FP_MO_SPLIT", line 199  ORA-06512: 在 "STG.FP_MO_SPLIT", line 1318 错误描述:MO拆分计划行程序中写入PRODUCTIONORDERS表数据出现重复导致报错(BUG) 存储过程:FP_MO_SPLIT 解决方法: 原文地址:https://www.cnblogs.co

WPF 线程中异常导致程序崩溃

原文:WPF 线程中异常导致程序崩溃 一般我们WPF中都加全局捕获,避免出现异常导致崩溃. Application.Current.DispatcherUnhandledException += Current_DispatcherUnhandledException; AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException)