前言

在单核时代,大家所编写的程序都是单进程/单线程程序。随着计算机硬件技术的发展,进入了多核时代后,为了降低响应时间,重复充分利用多核cpu的资源,使用多进程编程的手段逐渐被人们接受和掌握。然而因为创建一个进程代价比较大,多线程编程的手段也就逐渐被人们认可和喜爱了。

记得在我刚刚学习线程进程的时候就想,为什么很少见人把多进程和多线程结合起来使用呢,把二者结合起来不是更好吗?现在想想当初真是too young too simple,后文就主要讨论一下这个问题。

进程与线程模型

进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的context中的。context是由程序正确运行所需的状态组成的,这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器(PC)、环境变量以及打开的文件描述符的集合。

进程主要提供给上层的应用程序两个抽象:

  • 一个独立的逻辑控制流,它提供一个假象,好像我们程序独占的使用处理器。
  • 一个私有的虚拟地址空间,它提供一个假象,好像我们的程序独占的使用存储器系统。

线程,就是运行在进程context中的逻辑流。线程由内核自动调度。每个线程都有它自己的线程context,包括一个唯一的整数线程ID、栈、栈指针、程序计数器(PC)、通用目的寄存器和条件码。每个线程和运行在同一进程内的其他线程一起共享进程context的剩余部分。这包括整个用户虚拟地址空间,它是由只读文本(代码)、读/写数据、堆以及所有的共享库代码和数据区域组成。线程也同样共享打开文件的集合。

即进程是资源管理的最小单位,而线程是程序执行的最小单位。

在linux系统中,posix线程可以“看做”为一种轻量级的进程,pthread_create创建线程和fork创建进程都是在内核中调用__clone函数创建的,只不过创建线程或进程的时候选项不同,比如是否共享虚拟地址空间、文件描述符等。

fork与多线程

我们知道通过fork创建的一个子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份拷贝,包括文本、数据和bss段、堆以及用户栈等。子进程还获得与父进程任何打开文件描述符相同的拷贝,这就意味着子进程可以读写父进程中任何打开的文件,父进程和子进程之间最大的区别在于它们有着不同的PID。

但是有一点需要注意的是,在Linux中,fork的时候只复制当前线程到子进程,在fork(2)-Linux Man Page中有着这样一段相关的描述:

The child process is created with a single thread--the one that called fork(). The entire virtual address space of the parent is replicated in the child, including the states of mutexes, condition variables, and other pthreads objects; the use of pthread_atfork(3) may be helpful for dealing with problems that this can cause.

也就是说除了调用fork的线程外,其他线程在子进程中“蒸发”了。

这就是多线程中fork所带来的一切问题的根源所在了。

互斥锁

互斥锁,就是多线程fork大部分问题的关键部分。

在大多数操作系统上,为了性能的因素,锁基本上都是实现在用户态的而非内核态(因为在用户态实现最方便,基本上就是通过原子操作或者之前文章中提到的memory barrier实现的),所以调用fork的时候,会复制父进程的所有锁到子进程中。

问题就出在这了。从操作系统的角度上看,对于每一个锁都有它的持有者,即对它进行lock操作的线程。假设在fork之前,一个线程对某个锁进行的lock操作,即持有了该锁,然后另外一个线程调用了fork创建子进程。可是在子进程中持有那个锁的线程却"消失"了,从子进程的角度来看,这个锁被“永久”的上锁了,因为它的持有者“蒸发”了。

那么如果子进程中的任何一个线程对这个已经被持有的锁进行lock操作话,就会发生死锁。

当然了有人会说可以在fork之前,让准备调用fork的线程获取所有的锁,然后再在fork出的子进程的中释放每一个锁。先不说现实中的业务逻辑以及其他因素允不允许这样做,这种做法会带来一个问题,那就是隐含了一种上锁的先后顺序,如果次序和平时不同,就会发生死锁。

如果你说自己一定可以按正确的顺序上锁而不出错的话,还有一个隐含的问题是你所不能控制的,那就是库函数。

因为你不能确定你所用到的所有库函数都不会使用共享数据,即他们都是完全线程安全的。有相当一部分线程安全的库函数都是在内部通过持有互斥锁的方式来实现的,比如几乎所有程序都会用到的C/C++标准库函数malloc、printf等等。

比如一个多线程程序在fork之前难免会分配动态内存,这就必然会用到malloc函数;而在fork之后的子进程中也难免要分配动态内存,这也同样要用到malloc,可这却是不安全的,因为有可能malloc内部的锁已经在fork之前被某一个线程所持有了,而那个线程却在子进程中消失了。

exec与文件描述符

按照上文的分析,似乎多线程中在fork出的子进程中立刻调用exec函数是唯一明智的选择了,其实即使这样做还是有一点不足。因为子进程会继承父进程中所有已打开的文件描述符,所以在执行exec之前子进程仍然可以读写父进程中的文件,但如果你不希望子进程能读写父进程里的某个已打开的文件该怎么办?

或许fcntl设置文件属性是一种办法:

1
2
3
4
5
6
int fd = open("file", O_RDWR | O_CREAT);
if (fd < 0)
{
    perror("open");
}
fcntl(fd, F_SETFD, FD_CLOEXEC);

但是如果在open打开file文件之后,调用fcntl设置CLOEXEC属性之前有其他线程fork出了子进程了的话,这个子进程仍然是可以读写file文件。如果用锁的话,就又回到了上文所讨论的情况了。

从Linux 2.6.23版本的内核开始,我们可以在open中设置O_CLOEXEC标志了,相当于“打开文件再设置CLOEXEC”成为了一个原子操作。这样在fork出的子进程执行exec之前就不能读写父进程中已打开的文件了。

pthread_atfork

如果你不幸真的碰到了一个要解决多线程中fork的问题的时候,可以尝试使用pthread_atfork:

1
int pthread_atfork(void (*prepare)(void), void (*parent)void(), void (*child)(void));
  • prepare处理函数由父进程在fork创建子进程前调用,这个函数的任务是获取父进程定义的所有锁。
  • parent处理函数是在fork创建了子进程以后,但在fork返回之前在父进程环境中调用的。它的任务是对prepare获取的所有锁解锁。
  • child处理函数在fork返回之前在子进程环境中调用,与parent处理函数一样,它也必须解锁所有prepare中所获取的锁。

因为子进程继承的是父进程的锁的拷贝,所有上述并不是解锁了两次,而是各自独自解锁。可以多次调用pthread_atfork函数从而设置多套fork处理程序,但是使用多个处理程序的时候。处理程序的调用顺序并不相同。parent和child是以它们注册时的顺序调用的,而prepare的调用顺序与注册顺序相反。这样可以允许多个模块注册它们自己的处理程序并且保持锁的层次(类似于多个RAII对象的构造析构层次)。

需要注意的是pthread_atfork只能清理锁,但不能清理条件变量。在有些系统的实现中条件变量不需要清理。但是在有的系统中,条件变量的实现中包含了锁,这种情况就需要清理。但是目前并没有清理条件变量的接口和方法。

结语

  • 在多线程程序中最好只用fork来执行exec函数,不要对fork出的子进程进行其他任何操作。
  • 如果确定要在多线程中通过fork出的子进程执行exec函数,那么在fork之前打开文件描述符时需要加上CLOEXEC标志。

谨慎使用多线程中的fork 学习!!!!的更多相关文章

  1. 谨慎使用多线程中的fork

    // Upon successful completion, pthread_atfork() shall return a value of zero; otherwise, an error nu ...

  2. 多线程中fork与mutex

    在多线程程序中fork出一个新进程,发现新的进程无法正常工作.因为:在使用fork时会将原来进程中的所有内存数据复制一份保存在子进程中.但是在拷贝的时候,但是线程是无法被拷贝的.如果在原来线程中加了锁 ...

  3. [C#学习]在多线程中如何调用Winform[转]

    问题的产生: 我的WinForm程序中有一个用于更新主窗口的工作线程(worker thread),但文档中却提示我不能在多线程中调用这个form(为什么?),而事实上我在调用时程序常常会崩掉.请问如 ...

  4. PHP中的Libevent学习

    wangbin@2012,1,3 目录 Libevent在php中的应用学习 1.      Libevent介绍 2.      为什么要学习libevent 3.      Php libeven ...

  5. iOS多线程中,队列和执行的排列组合结果分析

    本文是对以往学习的多线程中知识点的一个整理. 多线程中的队列有:串行队列,并发队列,全局队列,主队列. 执行的方法有:同步执行和异步执行.那么两两一组合会有哪些注意事项呢? 如果不是在董铂然博客园看到 ...

  6. Java多线程中易混淆的概念

    概述 最近在看<ThinKing In Java>,看到多线程章节时觉得有一些概念比较容易混淆有必要总结一下,虽然都不是新的东西,不过还是蛮重要,很基本的,在开发或阅读源码中经常会遇到,在 ...

  7. Java多线程中变量的可见性

    之所以写这篇博客, 是因为在csdn上看到一个帖子问的就是这个问题. 废话不多说, 我们先看看他的代码(为了减少代码量, 我将创建线程并启动的部分修改为使用方法引用). 1 2 3 4 5 6 7 8 ...

  8. 认识多线程中start和run方法的区别?

    一.认识多线程中的 start() 和 run() 1.start(): 先来看看Java API中对于该方法的介绍: 使该线程开始执行:Java 虚拟机调用该线程的 run 方法. 结果是两个线程并 ...

  9. Java 多线程中的任务分解机制-ForkJoinPool,以及CompletableFuture

    ForkJoinPool的优势在于,可以充分利用多cpu,多核cpu的优势,把一个任务拆分成多个“小任务”,把多个“小任务”放到多个处理器核心上并行执行:当多个“小任务”执行完成之后,再将这些执行结果 ...

随机推荐

  1. Luogu 3586 [POI2015]LOG

    考虑离散化后开权值线段树. 设序列中不小于$s$的数有$cnt$个,小于$s$的数的和为$sum$. 那么操作Z能成功的充要条件是$sum \geq (c - cnt) * s$. 如果序列中不小于$ ...

  2. Picasso VS Glide

    原文: Introduction to Glide, Image Loader Library for Android, recommended by Google 在泰国举行的谷歌开发者论坛上,谷歌 ...

  3. [译]如何在visual studio中调试Javascript

    本文翻译youtube上的up主kudvenkat的javascript tutorial播放单 源地址在此: https://www.youtube.com/watch?v=PMsVM7rjupU& ...

  4. JDBC行级锁

    行级锁又称为悲观锁 for update 如下(必须要等这个for updaste事务执行完毕以后,剩下的sql语句才可以去执行)

  5. MVC Controller传值到View的几种方式总结

    Controller中的代码如下 var bingo = new Web1.Models.Bingo() { Title = "测试", desc = "嘻嘻" ...

  6. Java从入门到放弃——05.修饰符static,final,权限修饰符

    本文目标 static final: 权限修饰符:public,private,protected,缺省 1.static 静态修饰符,被static修饰的变量或者方法会被加载进静态区内存,不需要创建 ...

  7. kolla出现问题时的定位方式

    前提,对官网问题的一个翻译 Troubleshooting Guide排障手册 1.Failures(失败) If Kolla fails, often it is caused by a CTRL- ...

  8. ubuntu - 安装软件问题

    problem & solution 问题1 - E: 无法定位软件包 @原因(1) - 没有添加相应软件的镜像源(软件源)    解决方案 用 gedit/vi/vim - 在 /etc/a ...

  9. HDU - 1166 敌兵布阵 方法一:(线段树+单点修改,区间查询和) 方法二:利用树状数组

    C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了.A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况.由于 ...

  10. uoj#422. 【集训队作业2018】小Z的礼物(MIn-Max容斥+插头dp)

    题面 传送门 题解 好迷-- 很明显它让我们求的是\(Max(S)\),我们用\(Min-Max\)容斥,因为\(Min(S)\)是很好求的,只要用方案数除以总方案数算出概率,再求出倒数就是期望了 然 ...