精华内容
下载资源
问答
  • 进程(process)和线程(thread)是操作系统的基本概念,但是...vs 线程 :进程可以包含多个线程" title="进程 vs 线程 :进程可以包含多个线程" style="border:0px; max-width:602px; height:auto; ma

    进程(process)和线程(thread)是操作系统的基本概念,但是它们比较抽象,不容易掌握。

    最近,我读到一篇材料,发现有一个很好的类比,可以把它们解释地清晰易懂。

    1.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。

    2.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。

    3.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。

    4.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    一个车间里,可以有很多工人。他们协同完成一个任务。

    5.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    线程就好比车间里的工人。一个进程可以包括多个线程。

    6.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

    7.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

    8.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

    9.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。

    10.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做"信号量"(Semaphore),用来保证多个线程不会互相冲突。

    不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。

    11.

    进程 <wbr>vs <wbr>线程 <wbr>:一个进程可以包含多个线程

    操作系统的设计,因此可以归结为三点:

    (1)以多进程形式,允许多个任务同时运行;

    (2)以多线程形式,允许单个任务分成不同的部分运行;

    (3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

    (完)

    展开全文
  • 一个进程都有它自己的地址空间,一般情况,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。 第二,进程一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序...

    进程-线程-多线程

    1、进程(process)
    狭义定义:进程就是一段程序的执行过程
    简单的来讲进程的概念主要有两点:
    第一,进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。
    第二,进程是一个“执行中的程序”。程序是一个没有生命的实体,只有处理器赋予程序生命时,它才能成为一个活动的实体,我们称其为进程。
    进程状态:进程有三个状态,就绪、运行、阻塞。

    511遇见易语言多线程大漠多线程


    2、线程(thread)
    通常在一个进程中可以包含若干个线程,当然一个进程中至少有一个线程,不然没有存在的意义。线程可以利用进程所拥有的资源。
    3、进程与线程的区别:
    每个进程都有私有的虚拟地址空间,进程的所有线程共享同一地址空间。每个线程被CPU分配一个时间片,一旦被激活,它正常运行直到时间片耗尽并被挂起,此时,操作系统选择另一个线程进行运行。
    简而言之,一个进程至少有一个线程.
    线程的划分尺度小于进程,使得多线程程序的并发性高。
    1)线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
    2)一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
    3)进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
    4)调度和切换:线程上下文切换比进程上下文切换要快得多。
    4、什么是单线程和多线程?
    单线程,顾名思义即是只有一条线程在执行任务
    多线程,创建多条线程同时执行任务
    5、并行和并发的区别,
    并发:交替做不同事的能力
    并行:同时做不同事的能力
    行话解释:
    并发:不同代码块交替执行的性能
    并行:不同代码块同时执行的性能
    6、多线程在多核上是并发还是并行?
    对于单核,多线程的多任务是在单cpu交替执行,属于并发;
    对于多核,多线程的任务如果能够分布在各个cpu,上(线程数少许核心数),那么就是并行。
    7、拓展
    同步和异步 ----- 异步的反义词是同步
    顺序和并发 ----- 并发的反义词是顺序
    串行和并行 ----- 并行的反义词是串行
    简单的说:并行是多线程的一种形式,多线程是并发的一种形式。异步也是并发的一种形式。
    8、线程的生命周期
    9、线程的安全
    10、线程池

    展开全文
  • 进程线程

    万次阅读 多人点赞 2021-03-17 22:50:20
    进程线程 1 进程 1.1 进程的概念 进程就是正在运行的程序,它代表了程序所占用的内存区域 1.2 进程的特点 独立性 进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的...多个进程可以在

    进程与线程

    1 进程

    1.1 进程的概念

    进程就是正在运行的程序,它会占用对应的内存区域,由CPU进行执行与计算。

    1.2 进程的特点

    • 独立性
      进程是系统中独立存在的实体,它可以拥有自己独立的资源,每个进程都拥有自己私有的地址空间,在没有经过进程本身允许的情况下,一个用户进程不可以直接访问其他进程的地址空间
    • 动态性
      进程与程序的区别在于,程序只是一个静态的指令集合,而进程是一个正在系统中活动的指令集合,程序加入了时间的概念以后,称为进程,具有自己的生命周期和各种不同的状态,这些概念都是程序所不具备的.
    • 并发性
      多个进程可以在单个处理器CPU上并发执行,多个进程之间不会互相影响.

    2 线程

    2.1 线程的概念

    线程是操作系统OS能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位.
    一个进程可以开启多个线程,其中有一个主线程来调用本进程中的其他线程。
    我们看到的进程的切换,切换的也是不同进程的主线程
    多线程可以让同一个进程同时并发处理多个任务,相当于扩展了进程的功能。

    2.2 进程与线程的关系

    一个操作系统中可以有多个进程,一个进程中可以包含一个线程(单线程程序),也可以包含多个线程(多线程程序)
    进程与线程的关系
    每个线程在共享同一个进程中的内存的同时,又有自己独立的内存空间.
    所以想使用线程技术,得先有进程,进程的创建是OS操作系统来创建的,一般都是C或者C++完成
    进程与线程的关系

    3 多线程的特性

    3.1 随机性

    我们宏观上觉得多个进程是同时运行的,但实际的微观层面上,一个CPU【单核】只能执行一个进程中的一个线程。
    那为什么看起来像是多个进程同时执行呢?
    是因为CPU以纳秒级别甚至是更快的速度高效切换着,超过了人的反应速度,这使得各个进程从看起来是同时进行的,也就是说,宏观层面上,所有的进程看似并行【同时运行】,但是微观层面上是串行的【同一时刻,一个CPU只能处理一件事】。
    线程切换

    串行与并行

    串行是指同一时刻一个CPU只能处理一件事,类似于单车道
    并行是指同一时刻多个CPU可以处理多件事,类似于多车道
    在这里插入图片描述
    在这里插入图片描述

    3.2 CPU分时调度

    时间片,即CPU分配给各个线程的一个时间段,称作它的时间片,即该线程被允许运行的时间,如果在时间片用完时线程还在执行,那CPU将被剥夺并分配给另一个线程,将当前线程挂起,如果线程在时间片用完之前阻塞或结束,则CPU当即进行切换,从而避免CPU资源浪费,当再次切换到之前挂起的线程,恢复现场,继续执行。
    注意:我们无法控制OS选择执行哪些线程,OS底层有自己规则,如:

    1. FCFS(First Come First Service 先来先服务算法)
    2. SJS(Short Job Service短服务算法)

    CPU分片

    3.3 线程的状态

    由于线程状态比较复杂,我们由易到难,先学习线程的三种基础状态及其转换,简称”三态模型” :

    • 就绪(可运行)状态:线程已经准备好运行,只要获得CPU,就可立即执行
    • 执行(运行)状态:线程已经获得CPU,其程序正在运行的状态
    • 阻塞状态:正在运行的线程由于某些事件(I/O请求等)暂时无法执行的状态,即线程执行阻塞
      线程的3种状态

    就绪 → 执行:为就绪线程分配CPU即可变为执行状态"
    执行 → 就绪:正在执行的线程由于时间片用完被剥夺CPU暂停执行,就变为就绪状态
    执行 → 阻塞:由于发生某事件,使正在执行的线程受阻,无法执行,则由执行变为阻塞
    (例如线程正在访问临界资源,而资源正在被其他线程访问)
    反之,如果获得了之前需要的资源,则由阻塞变为就绪状态,等待分配CPU再次执行

    我们可以再添加两种状态:

    • 创建状态:线程的创建比较复杂,需要先申请PCB,然后为该线程运行分配必须的资源,并将该线程转为就绪状态插入到就绪队列中
    • 终止状态:等待OS进行善后处理,最后将PCB清零,并将PCB返回给系统
      线程的5种状态

    PCB(Process Control Block):为了保证参与并发执行的每个线程都能独立运行,OS配置了特有的数据结构PCB来描述线程的基本情况和活动过程,进而控制和管理线程

    3.4 线程状态与代码对照

    线程状态与代码对照
    线程生命周期,主要有五种状态:

    1. 新建状态(New) : 当线程对象创建后就进入了新建状态.如:Thread t = new MyThread();
    2. 就绪状态(Runnable):当调用线程对象的start()方法,线程即为进入就绪状态.
      处于就绪(可运行)状态的线程,只是说明线程已经做好准备,随时等待CPU调度执行,并不是执行了t.start()此线程立即就会执行
    3. 运行状态(Running):当CPU调度了处于就绪状态的线程时,此线程才是真正的执行,即进入到运行状态
      就绪状态是进入运行状态的唯一入口,也就是线程想要进入运行状态状态执行,先得处于就绪状态
    4. 阻塞状态(Blocked):处于运状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入就绪状态才有机会被CPU选中再次执行.
      根据阻塞状态产生的原因不同,阻塞状态又可以细分成三种:
      等待阻塞:运行状态中的线程执行wait()方法,本线程进入到等待阻塞状态
      同步阻塞:线程在获取synchronized同步锁失败(因为锁被其他线程占用),它会进入同步阻塞状态
      其他阻塞:调用线程的sleep()或者join()或发出了I/O请求时,线程会进入到阻塞状态.当sleep()状态超时.join()等待线程终止或者超时或者I/O处理完毕时线程重新转入就绪状态
    5. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期

    4 多线程代码创建方式1:继承Thread

    4.1 概述

    Thread类本质上是实现了Runnable接口的一个实例,代表一个线程的实例
    启动线程的唯一方法就是通过Thread类的start()实例方法
    start()方法是一native方法,它将通知底层操作系统,.最终由操作系统启动一个新线程,操作系统将执行run()
    这种方式实现的多线程很简单,通过自己的类直接extends Thread,并重写run()方法,就可以自动启动新线程并执行自己定义的run()方法
    模拟开启多个线程,每个线程调用run()方法.

    4.2 常用方法

    构造方法

    Thread() 分配新的Thread对象
    Thread(String name) 分配新的Thread对象
    Thread(Runnable target) 分配新的Thread对象
    Thread(Runnable target,String name) 分配新的Thread对象

    普通方法

    static Thread currentThread( )
    返回对当前正在执行的线程对象的引用
    long getId()
    返回该线程的标识
    String getName()
    返回该线程的名称
    void run()
    如果该线程是使用独立的 Runnable 运行对象构造的,则调用该 Runnable 对象的 run 方法
    static void sleep(long millions)
    在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)
    void start()
    使该线程开始执行:Java虚拟机调用该线程的run()

    4.3 测试多线程的创建方式1

    创建包: cn.tedu.thread
    创建类: TestThread1.java

    package cn.tedu.thread;
    /*本类用于多线程编程实现方案一:继承Thread类来完成*/
    public class TestThread1 {
        public static void main(String[] args) {
            //4.创建线程对象进行测试
            /*4.new对应的是线程的新建状态
            * 5.要想模拟多线程,至少得启动2个线程,如果只启动1个,是单线程程序*/
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            MyThread t3 = new MyThread();
            MyThread t4 = new MyThread();
            /*6.这个run()如果直接这样调用,是没有多线程抢占执行的效果的
            * 只是把这两句话看作普通方法的调用,谁先写,就先执行谁*/
            //t1.run();
            //t2.run();
            /*7.start()对应的状态就是就绪状态,会把刚刚新建好的线程加入到就绪队列之中
            * 至于什么时候执行,就是多线程执行的效果,需要等待OS选中分配CPU
            * 8.执行的时候start()底层会自动调用我们重写的run()种的业务
            * 9.线程的执行具有随机性,也就是说t1-t4具体怎么执行
            * 取决于CPU的调度时间片的分配,我们是决定不了的*/
            t1.start();//以多线程的方式启动线程1,将当前线程变为就绪状态
            t2.start();//以多线程的方式启动线程2,将当前线程变为就绪状态
            t3.start();//以多线程的方式启动线程3,将当前线程变为就绪状态
            t4.start();//以多线程的方式启动线程4,将当前线程变为就绪状态
        }
    }
    
    //1.自定义一个多线程类,然后让这个类继承Thread
    class MyThread extends Thread{
        /*1.多线程编程实现的方案1:通过继承Thread类并重写run()来完成的 */
        //2.重写run(),run()里是我们自己的业务
        @Override
        public void run() {
            /*2.super.run()表示的是调用父类的业务,我们现在要用自己的业务,所以注释掉*/
            //super.run();
            //3.完成业务:打印10次当前正在执行的线程的名称
            for (int i = 0; i < 10; i++) {
                /*3.getName()表示可以获取当前正在执行的线程名称
                * 由于本类继承了Thread类,所以可以直接使用这个方法*/
                System.out.println(i+"="+getName());
            }
        }
    }
    

    5 多线程代码创建方式2:实现Runnable接口

    5.1 概述

    如果自己的类已经extends另一个类,就无法多继承,此时,可以实现一个Runnable接口

    5.2 常用方法

    void run()使用实现接口Runnable的对象创建线程时,启动该线程将导致在独立执行的线程中调用对象的run()方法

    5.3 练习2:测试多线程的创建方式2

    创建包: cn.tedu.thread
    创建类: Thread2.java

    package cn.tedu.thread;
    /*本类用于多线程编程实现方案二:实现Runnable接口来完成*/
    public class TestThread2 {
        public static void main(String[] args) {
            //5.创建自定义类的对象--目标业务类对象
            MyRunnable target = new MyRunnable();
            //6.如何启动线程?自己没有,需要与Thread建立关系
            Thread t1 = new Thread(target);
            Thread t2 = new Thread(target);
            Thread t3 = new Thread(target);
            Thread t4 = new Thread(target);
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }
    
    //1.自定义多线程类
    class MyRunnable implements Runnable{
        //2.添加父接口中的抽象方法run(),里面是自己的业务
        @Override
        public void run() {
            //3.写业务,打印10次当前正在执行的线程名称
            for (int i = 0; i < 10; i++) {
                /*问题:自定义类与父接口Runnable中都没有获取名字的方法
                * 所以还需要从Thread中找:
                * currentThread():静态方法,获取当前正在执行的线程对象
                * getName():获取当前线程的名称*/
                System.out.println(i+"="+Thread.currentThread().getName());
            }
        }
    }
    

    5.4 两种实现方式的比较

    • 继承Thread类
      优点: 编写简单,如果需要访问当前线程,无需使用Thread.currentThread()方法,直接使用this即可获得当前线程
      缺点: 自定义的线程类已继承了Thread类,所以后续无法再继承其他的类
    • 实现Runnable接口
      优点: 自定义的线程类只是实现了Runnable接口或Callable接口,后续还可以继承其他类,在这种方式下,多个线程可以共享同一个target对象,所以非常适合多个相同线程来处理同一份资源的情况,从而可以将CPU、代码、还有数据分开(解耦),形成清晰的模型,较好地体现了面向对象的思想
      缺点: 编程稍微复杂,如想访问当前线程,则需使用Thread.currentThread()方法

    6 售票案例

    需求:设计4个售票窗口,总计售票100张。用多线程的程序设计并写出代码

    6.1 方案1:继承Thread

    创建包: cn.tedu.tickets
    创建类: TestThread.java

    package cn.tedu.tickets;
    /*需求:设计多线程编程模型,4个窗口共计售票100张
    * 本方案使用多线程编程方案1,继承Thread类的方式来完成*/
    public class TestThread {
        public static void main(String[] args) {
            //5.创建多个线程对象
            TicketThread t1 = new TicketThread();
            TicketThread t2 = new TicketThread();
            TicketThread t3 = new TicketThread();
            TicketThread t4 = new TicketThread();
            //6.以多线程的方式启动
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }
    
    //1.自定义多线程售票类,继承Thread
    class TicketThread extends Thread{
        //3.定义变量,保存要售卖的票数
        /*问题:4个线程对象共计售票400张,原因是创建了4次对象,各自操作各自的成员变量
        * 解决:让所有对象共享同一个数据,票数需要设置为静态*/
        static int tickets = 100;
        //2.重写父类的run(),里面是我们的业务
        @Override
        public void run() {
            //4.1循环卖票
            while(true){
                try {
                    //7.让每个线程经历休眠,增加线程状态切换的频率与出错的概率
                    //问题1:产生了重卖的现象:同一张票卖了多个人
                    //问题2:产生了超卖的现象:超出了规定的票数100,出现了0 -1 -2这样的票
                    Thread.sleep(10);//让当前线程休眠10ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //4.2打印当前正在卖票的线程名称,并且票数-1
                System.out.println(getName()+"="+tickets--);
                //4.3做判断,如果没有票了,就退出死循环
                if(tickets <= 0) break;//注意,死循环一定要设置出口
            }
        }
    }
    

    6.2 方案2:实现Runnable

    创建包: cn.tedu.tickets
    创建类: TestRunnable.java

    package cn.tedu.tickets;
    /*需求:设计多线程编程模型,4个窗口共计售票100张
     * 本方案使用多线程编程方案2,实现Runnable接口的方式来完成*/
    public class TestRunnable {
        public static void main(String[] args) {
            //5.创建Runnable接口的实现类对象,作为目标业务对象
            TicketRunnable target = new TicketRunnable();
            //6.创建多个Thread类线程对象,并将target业务对象交给多个线程对象来处理
            Thread t1 = new Thread(target);
            Thread t2 = new Thread(target);
            Thread t3 = new Thread(target);
            Thread t4 = new Thread(target);
            //7.以多线程的方式启动多个线程对象
            t1.start();
            t2.start();
            t3.start();
            t4.start();
        }
    }
    
    //1.自定义多线程类实现Runnable接口
    class TicketRunnable implements Runnable{
        //3.定义一个成员变量,用来保存票数100
        /*由于自定义类对象只创建了一次,所以票数被所有线程对象Thread类的对象共享*/
        int tickets = 100;
        //2.添加接口中未实现的方法,方法里是我们的业务
        @Override
        public void run() {
            //4.1循环卖票
            while(true){
                //8.让线程休眠10ms,增加线程状态切换的概率和出错的概率
                try {
                    Thread.sleep(10);//让当前线程休眠10ms
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //4.2打印当前正在售票的线程名称 & 票数-1
                System.out.println(Thread.currentThread().getName()+"="+tickets--);
                //4.3设置死循环的出口,没票了就停止卖票
                if(tickets <=0 ) break;
            }
        }
    }
    

    6.3 问题

    1. 每次创建线程对象,都会生成一个tickets变量值是100,创建4次对象就生成了400张票了。不符合需求,怎么解决呢?能不能把tickets变量在每个对象间共享,就保证多少个对象都是卖这100张票。
      解决方案: 用静态修饰
    2. 产生超卖,0 张 、-1张、-2张。
    3. 产生重卖,同一张票卖给多人。
    4. 多线程安全问题是如何出现的?常见情况是由于线程的随机性+访问延迟。
    5. 以后如何判断程序有没有线程安全问题?
      在多线程程序中 + 有共享数据 + 多条语句操作共享数据
      解决方案:下一节 同步锁点这里
    展开全文
  • 多进程多线程区别

    万次阅读 多人点赞 2016-04-21 10:01:16
    Apache是采用多进程的(perfork模式,每客户连接对应进程,每进程中只存在唯一一个执行线程), Java的Web容器Tomcat、Websphere等都是线程的(每客户连接对应一个线程,所有线程都在进程中)。...

    在Unix上编程采用多线程还是多进程的争执由来已久,这种争执最常见到在C/S通讯中服务端并发技术 的选型上,比如WEB服务器技术中,Apache是采用多进程的(perfork模式,每客户连接对应一个进程,每进程中只存在唯一一个执行线程), Java的Web容器Tomcat、Websphere等都是多线程的(每客户连接对应一个线程,所有线程都在一个进程中)。

    从Unix发展历史看,伴随着Unix的诞生进程就出现了,而线程很晚才被系统支持,例如Linux直到内核2.6,才支持符合Posix规范的NPTL线程库。进程和线程的特点,也就是各自的优缺点如下:

    进程优点:编程、调试简单,可靠性较高。
    进程缺点:创建、销毁、切换速度慢,内存、资源占用大。
    线程优点:创建、销毁、切换速度快,内存、资源占用小。
    线程缺点:编程、调试复杂,可靠性较差。

    上面的对比可以归结为一句话:“线程快而进程可靠性高”。线程有个别名叫“轻量级进程”,在有的书籍资料上介绍线程可以十倍、百倍的效率快于进程; 而进程之间不共享数据,没有锁问题,结构简单,一个进程崩溃不像线程那样影响全局,因此比较可靠。我相信这个观点可以被大部分人所接受,因为和我们所接受 的知识概念是相符的。

    在写这篇文章前,我也属于这“大部分人”,这两年在用C语言编写的几个C/S通讯程序中,因时间紧总是采用多进程并发技术,而且是比较简单的现场为 每客户fork()一个进程,当时总是担心并发量增大时负荷能否承受,盘算着等时间充裕了将它改为多线程形式,或者改为预先创建进程的形式,直到最近在网 上看到了一篇论文《Linux系统下多线程与多进程性能分析》作者“周丽 焦程波 兰巨龙”,才认真思考这个问题,我自己也做了实验,结论和论文作者的相似,但对大部分人可以说是颠覆性的。

    下面是得出结论的实验步骤和过程,结论究竟是怎样的? 感兴趣就一起看看吧。

     

    实验代码使用周丽论文中的代码样例,我做了少量修改,值得注意的是这样的区别:

    论文实验和我的实验时间不同,论文所处的年代linux内核是2.4,我的实验linux内核是2.6,2.6使用的线程库是NPTL,2.4使用的是老的Linux线程库(用进程模拟线程的那个LinuxThread)。

    论文实验和我用的机器不同,论文描述了使用的环境:单 cpu 机器基本配置为:celeron 2.0 GZ, 256M, Linux 9.2,内核 2.4.8。我的环境是我的工作本本:单cpu单核celeron(R) M 1.5 GZ,1.5G内存,ubuntu10.04 desktop,内核2.6.32。

    进程实验代码(fork.c):

    1. #include <stdlib.h>
    2. #include <stdio.h>
    3. #include <signal.h>
    4.  
    5. #define P_NUMBER 255    /* 并发进程数量 */
    6. #define COUNT 100       /* 每进程打印字符串次数 */
    7. #define TEST_LOGFILE "logFile.log"
    8. FILE *logFile =  NULL;
    9.  
    10. char *s =  "hello linux\0";
    11.  
    12. int main ( )
    13. {
    14.      int i =  0,j =  0;
    15.     logFile = fopen (TEST_LOGFILE,  "a+" )/* 打开日志文件 */
    16.      for (i =  0; i < P_NUMBER; i++ )
    17.      {
    18.          if (fork ( ) ==  0 )  /* 创建子进程,if(fork() == 0){}这段代码是子进程运行区间 */
    19.          {
    20.              for (j =  0;j < COUNT; j++ )
    21.              {
    22.                  printf ( "[%d]%s\n", j, s )/* 向控制台输出 */
    23.                 fprintf (logFile, "[%d]%s\n", j, s )/* 向日志文件输出 */
    24.              }
    25.             exit ( 0 )/* 子进程结束 */
    26.          }
    27.      }
    28.  
    29.      for (i =  0; i < P_NUMBER; i++ )  /* 回收子进程 */
    30.      {
    31.         wait ( 0 );
    32.      }
    33.  
    34.      printf ( "OK\n" );
    35.      return  0;
    36. }

    进程实验代码(thread.c):

    1. #include <pthread.h>
    2. #include <unistd.h>
    3. #include <stdlib.h>
    4. #include <stdio.h>
    5.  
    6. #define P_NUMBER 255    /* 并发线程数量 */
    7. #define COUNT 100       /* 每线程打印字符串次数 */
    8. #define Test_Log "logFIle.log"
    9. FILE *logFile =  NULL;
    10.  
    11. char *s =  "hello linux\0";
    12.  
    13. print_hello_linux ( )  /* 线程执行的函数 */
    14. {
    15.      int i =  0;
    16.      for (i =  0; i < COUNT; i++ )
    17.      {
    18.          printf ( "[%d]%s\n", i, s )/* 向控制台输出 */
    19.         fprintf (logFile,  "[%d]%s\n", i, s )/* 向日志文件输出 */
    20.      }
    21.     pthread_exit ( 0 )/* 线程结束 */
    22. }
    23.  
    24. int main ( )
    25. {
    26.      int i =  0;
    27.     pthread_t pid [P_NUMBER ]/* 线程数组 */
    28.     logFile = fopen (Test_Log,  "a+" )/* 打开日志文件 */
    29.  
    30.      for (i =  0; i < P_NUMBER; i++ )
    31.         pthread_create (&pid [i ]NULL( void * )print_hello_linux,  NULL )/* 创建线程 */
    32.  
    33.      for (i =  0; i < P_NUMBER; i++ )
    34.         pthread_join (pid [i ], NULL )/* 回收线程 */
    35.  
    36.      printf ( "OK\n" );
    37.      return  0;
    38. }

    两段程序做的事情是一样的,都是创建“若干”个进程/线程,每个创建出的进程/线程打印“若干”条“hello linux”字符串到控制台和日志文件,两个“若干”由两个宏 P_NUMBER和COUNT分别定义,程序编译指令如下:

    diaoyf@ali:~/tmp1$ gcc -o fork fork.c
    diaoyf@ali:~/tmp1$ gcc -lpthread -o thread thread.c

    实验通过time指令执行两个程序,抄录time输出的挂钟时间(real时间):

    time ./fork
    time ./thread

    每批次的实验通过改动宏 P_NUMBER和COUNT来调整进程/线程数量和打印次数,每批次测试五轮,得到的结果如下:

    一、重复周丽论文实验步骤

     第1次第2次第3次第4次第5次平均
    多进程0m1.277s0m1.175s0m1.227s0m1.245s0m1.228s0m1.230s
    多线程0m1.150s0m1.192s0m1.095s0m1.128s0m1.177s0m1.148s

    进程线程数:255 / 打印次数:100

     第1次第2次第3次第4次第5次平均
    多进程0m6.341s0m6.121s0m5.966s0m6.005s0m6.143s0m6.115s
    多线程0m6.082s0m6.144s0m6.026s0m5.979s0m6.012s0m6.048s

    进程线程数:255 / 打印次数:500

     第1次第2次第3次第4次第5次平均
    多进程0m12.155s0m12.057s0m12.433s0m12.327s0m11.986s0m12.184s
    多线程0m12.241s0m11.956s0m11.829s0m12.103s0m11.928s0m12.011s

    进程线程数:255 / 打印次数:1000

     第1次第2次第3次第4次第5次平均
    多进程1m2.182s1m2.635s1m2.683s1m2.751s1m2.694s1m2.589s
    多线程1m2.622s1m2.384s1m2.442s1m2.458s1m3.263s1m2.614s

    进程线程数:255 / 打印次数:5000

    本轮实验是为了和周丽论文作对比,因此将进程/线程数量限制在255个,论文也是测试了255个进程/线程分别进行10 次,50 次,100 次,200 次……900 次打印的用时,论文得出的结果是:任务量较大时,多进程比多线程效率高;而完成的任务量较小时,多线程比多进程要快,重复打印 600 次时,多进程与多线程所耗费的时间相同。

    虽然我的实验直到5000打印次数时,多进程才开始领先,但考虑到使用的是NPTL线程库的缘故,从而可以证实了论文的观点。从我的实验数据看,多线程和多进程两组数据非常接近,考虑到数据的提取具有瞬间性,因此可以认为他们的速度是相同的。

    当前的网络环境中,我们更看中高并发、高负荷下的性能,纵观前面的实验步骤,最长的实验周期不过1分钟多一点,因此下面的实验将向两个方向延伸,第一,增加并发数量,第二,增加每进程/线程的工作强度。

    二、增加并发数量的实验

    下面的实验打印次数不变,而进程/线程数量逐渐增加。在实验过程中多线程程序在后三组(线程数500,800,1000)的测试中都出现了“段错误”,出现错误的原因和线程栈的大小有关。

    实验中的计算机CPU是32位的赛扬,寻址最大范围是4GB(2的32次方),Linux是按照3GB/1GB的方式来分配内存,其中1GB属于所 有进程共享的内核空间,3GB属于用户空间(进程虚拟内存空间),对于进程而言只有一个栈,这个栈可以用尽这3GB空间(计算时需要排除程序文本、数据、 共享库等占用的空间),所以它的大小通常不是问题。但对线程而言每个线程有一个线程栈,这3GB空间会被所有线程栈摊分,线程数量太多时,线程栈累计的大 小将超过进程虚拟内存空间大小,这就是实验中出现的“段错误”的原因。

    Linux2.6的默认线程栈大小是8M,可以通过 ulimit -s 命令查看或修改,我们可以计算出线程数的最大上线: (1024*1024*1024*3) / (1024*1024*8) = 384,实际数字应该略小与384,因为还要计算程序文本、数据、共享库等占用的空间。在当今的稍显繁忙的WEB服务器上,突破384的并发访问并不是稀 罕的事情,要继续下面的实验需要将默认线程栈的大小减小,但这样做有一定的风险,比如线程中的函数分配了大量的自动变量或者函数涉及很深的栈帧(典型的是 递归调用),线程栈就可能不够用了。可以配合使用POSIX.1规定的两个线程属性guardsize和stackaddr来解决线程栈溢出问题, guardsize控制着线程栈末尾之后的一篇内存区域,一旦线程栈在使用中溢出并到达了这片内存,程序可以捕获系统内核发出的告警信号,然后使用 malloc获取另外的内存,并通过stackaddr改变线程栈的位置,以获得额外的栈空间,这个动态扩展栈空间办法需要手工编程,而且非常麻烦。

    有两种方法可以改变线程栈的大小,使用 ulimit -s 命令改变系统默认线程栈的大小,或者在代码中创建线程时通过pthread_attr_setstacksize函数改变栈尺寸,在实验中使用的是第一 种,在程序运行前先执行ulimit指令将默认线程栈大小改为1M:

    diaoyf@ali:~/tmp1$ ulimit -s 1024
    diaoyf@ali:~/tmp1$ time ./thread

     第1次第2次第3次第4次第5次平均
    多进程0m4.958s0m5.032s0m5.181s0m4.951s0m5.032s0m5.031s
    多线程0m4.994s0m5.040s0m5.071s0m5.113s0m5.079s0m5.059s

    进程线程数:100 / 打印次数:1000

     第1次第2次第3次第4次第5次平均
    多进程0m12.155s0m12.057s0m12.433s0m12.327s0m11.986s0m12.184s
    多线程0m12.241s0m11.956s0m11.829s0m12.103s0m11.928s0m12.011s

    进程线程数:255 / 打印次数:1000 (这里使用了第一次的实验数据)

     第1次第2次第3次第4次第5次平均
    多进程0m17.686s0m17.569s0m17.609s0m17.663s0m17.784s0m17.662s
    多线程0m17.694s0m17.976s0m17.884s0m17.785s0m18.261s0m17.920s

    进程线程数:350 / 打印次数:1000

     第1次第2次第3次第4次第5次平均
    多进程0m23.638s0m23.543s0m24.135s0m23.981s0m23.507s0m23.761s
    多线程0m23.634s0m23.326s0m23.408s0m23.294s0m23.980s0m23.528s

    进程线程数:500 / 打印次数:1000 (线程栈大小更改为1M)

     第1次第2次第3次第4次第5次平均
    多进程0m38.517s0m38.133s0m38.872s0m37.971s0m38.005s0m38.230s
    多线程0m38.011s0m38.049s0m37.635s0m38.219s0m37.861s0m37.995s

    进程线程数:800 / 打印次数:1000 (线程栈大小更改为1M)

     第1次第2次第3次第4次第5次平均
    多进程0m48.157s0m47.921s0m48.124s0m48.081s0m48.126s0m48.082s
    多线程0m47.513s0m47.914s0m48.073s0m47.920s0m48.652s0m48.014s

    进程线程数:1000 / 打印次数:1000 (线程栈大小更改为1M)

    出现了线程栈的问题,让我特别关心Java线程是怎样处理的,因此用Java语言写了同样的实验程序,Java程序加载虚拟机环境比较耗时,所以没 有用time提取测试时间,而直接将测时写入代码。对Linux上的C编程不熟悉的Java程序员也可以用这个程序去对比理解上面的C语言试验程序。

    1. import java.io.File;
    2. import java.io.FileNotFoundException;
    3. import java.io.FileOutputStream;
    4. import java.io.IOException;
    5.  
    6. public  class MyThread  extends  Thread
    7. {
    8.      static  int P_NUMBER =  1000;      /* 并发线程数量 */
    9.      static  int COUNT =  1000;         /* 每线程打印字符串次数 */
    10.  
    11.      static  String s =  "hello linux\n";
    12.        
    13.      static  FileOutputStream out =  null/* 文件输出流 */
    14.     @Override
    15.      public  void run ( )
    16.      {
    17.          for  ( int i =  0; i < COUNT; i++ )
    18.          {
    19.              System. out. printf ( "[%d]%s", i, s )/* 向控制台输出 */
    20.            
    21.             StringBuilder sb =  new StringBuilder ( 16 );
    22.             sb. append ( "[" ). append (i ). append ( "]" ). append (s );
    23.              try
    24.              {
    25.                 out. write (sb. toString ( ). getBytes ( ) ); /* 向日志文件输出 */
    26.              }
    27.              catch  ( IOException e )
    28.              {
    29.                 e. printStackTrace ( );
    30.              }
    31.          }
    32.      }
    33.  
    34.      public  static  void main ( String [ ] args )  throws  FileNotFoundExceptionInterruptedException
    35.      {
    36.         MyThread [ ] threads =  new MyThread [P_NUMBER ]/* 线程数组 */
    37.        
    38.          File file =  new  File ( "Javalogfile.log" );
    39.         out =  new  FileOutputStream (file,  true );   /* 日志文件输出流 */
    40.        
    41.          System. out. println ( "开始运行" );
    42.          long start =  System. currentTimeMillis ( );
    43.  
    44.          for  ( int i =  0; i < P_NUMBER; i++ )  //创建线程
    45.          {
    46.             threads [i ] =  new MyThread ( );
    47.             threads [i ]. start ( );
    48.          }
    49.  
    50.          for  ( int i =  0; i < P_NUMBER; i++ )  //回收线程
    51.          {
    52.             threads [i ]. join ( );
    53.          }
    54.        
    55.          System. out. println ( "用时:" +  ( System. currentTimeMillis ( ) – start ) +  " 毫秒" );
    56.          return;
    57.      }
    58.  
    59. }
     第1次第2次第3次第4次第5次平均
    Java65664 毫秒66269 毫秒65546 毫秒65931 毫秒66540 毫秒65990 毫秒

    线程数:1000 / 打印次数:1000

    Java程序比C程序慢一些在情理之中,但Java程序并没有出现线程栈问题,5次测试都平稳完成,可以用下面的ps指令获得java进程中线程的数量:

    diaoyf@ali:~$ ps -eLf | grep MyThread | wc -l
    1010

    用ps测试线程数在1010上维持了很长时间,多出的10个线程应该是jvm内部的管理线程,比如用于GC。我不知道Java创建线程时默认栈的大 小是多少,很多资料说法不统一,于是下载了Java的源码jdk-6u21-fcs-src-b07-jrl-17_jul_2010.jar(实验环境 安装的是 SUN jdk 1.6.0_20-b02),但没能从中找到需要的信息。对于jvm的运行,java提供了控制参数,因此再次测试时,通过下面的参数将Java线程栈大 小定义在8192k,和Linux的默认大小一致:

    diaoyf@ali:~/tmp1$ java -Xss8192k MyThread

    出乎意料的是并没有出现想象中的异常,但用ps侦测线程数最高到达337,我判断程序在创建线程时在栈到达可用内存的上线时就停止继续创建了,程序 运行的时间远小于估计值也证明了这个判断。程序虽然没有抛出异常,但运行的并不正常,另一个问题是最后并没有打印出“用时 xxx毫秒”信息。

    这次测试更加深了我的一个长期的猜测:Java的Web容器不稳定。因为我是多年编写B/S的Java程序员,WEB服务不稳定常常挂掉也是司空见 惯的,除了自己或项目组成员水平不高,代码编写太烂的原因之外,我一直猜测还有更深层的原因,如果就是线程原因的话,这颠覆性可比本篇文章的多进程性能颠 覆性要大得多,想想世界上有多少Tomcat、Jboss、Websphere、weblogic在跑着,嘿嘿。

    这次测试还打破了以前的一个说法:单CPU上并发超过6、7百,线程或进程间的切换就会占用大量CPU时间,造成服务器效率会急剧下降。但从上面的实验来看,进程/线程数到1000时(这差不多是非常繁忙的WEB服务器了),仍具有很好的线性。

    三、增加每进程/线程的工作强度的实验

    这次将程序打印数据增大,原来打印字符串为:

    1. char *s =  "hello linux\0";

    现在修改为每次打印256个字节数据:

    1. char *s =  "1234567890abcdef\
    2. 1234567890abcdef\
    3. 1234567890abcdef\
    4. 1234567890abcdef\
    5. 1234567890abcdef\
    6. 1234567890abcdef\
    7. 1234567890abcdef\
    8. 1234567890abcdef\
    9. 1234567890abcdef\
    10. 1234567890abcdef\
    11. 1234567890abcdef\
    12. 1234567890abcdef\
    13. 1234567890abcdef\
    14. 1234567890abcdef\
    15. 1234567890abcdef\
    16. 1234567890abcdef\0";
     第1次第2次第3次第4次第5次平均
    多进程0m28.149s0m27.993s0m28.094s0m27.657s0m28.016s0m27.982s
    多线程0m28.171s0m27.764s0m27.865s0m28.041s0m27.780s0m27.924s

    进程线程数:255 / 打印次数:100

     第1次第2次第3次第4次第5次平均
    多进程2m20.357s2m19.740s2m19.965s2m19.788s2m19.796s2m19.929s
    多线程2m20.061s2m20.462s2m19.789s2m19.514s2m19.479s2m19.861s

    进程线程数:255 / 打印次数:500

     第1次第2次
    多进程9m39s9m17s
    多线程9m31s9m22s

    进程线程数:255 / 打印次数:2000 (实验太耗时,因此只进行了2轮比对)

    【实验结论】

    从上面的实验比对结果看,即使Linux2.6使用了新的NPTL线程库(据说比原线程库性能提高了很多,唉,又是据说!),多线程比较多进程在效率上没有任何的优势,在线程数增大时多线程程序还出现了运行错误,实验可以得出下面的结论:

    在Linux2.6上,多线程并不比多进程速度快,考虑到线程栈的问题,多进程在并发上有优势。

    四、多进程和多线程在创建和销毁上的效率比较

    预先创建进程或线程可以节省进程或线程的创建、销毁时间,在实际的应用中很多程序使用了这样的策略,比如Apapche预先创建进程、Tomcat 预先创建线程,通常叫做进程池或线程池。在大部分人的概念中,进程或线程的创建、销毁是比较耗时的,在stevesn的著作《Unix网络编程》中有这样 的对比图(第一卷 第三版 30章 客户/服务器程序设计范式):

    行号服务器描述进程控制CPU时间(秒,与基准之差)
    Solaris2.5.1Digital Unix4.0bBSD/OS3.0
    0迭代服务器(基准测试,无进程控制)0.00.00.0
    1简单并发服务,为每个客户请求fork一个进程504.2168.929.6
    2预先派生子进程,每个子进程调用accept 6.21.8
    3预先派生子进程,用文件锁保护accept25.210.02.7
    4预先派生子进程,用线程互斥锁保护accept21.5  
    5预先派生子进程,由父进程向子进程传递套接字36.710.96.1
    6并发服务,为每个客户请求创建一个线程18.74.7 
    7预先创建线程,用互斥锁保护accept8.63.5 
    8预先创建线程,由主线程调用accept14.55.0 

    stevens已驾鹤西去多年,但《Unix网络编程》一书仍具有巨大的影响力,上表中stevens比较了三种服务器上多进程和多线程的执行效 率,因为三种服务器所用计算机不同,表中数据只能纵向比较,而横向无可比性,stevens在书中提供了这些测试程序的源码(也可以在网上下载)。书中介 绍了测试环境,两台与服务器处于同一子网的客户机,每个客户并发5个进程(服务器同一时间最多10个连接),每个客户请求从服务器获取4000字节数据, 预先派生子进程或线程的数量是15个。

    第0行是迭代模式的基准测试程序,服务器程序只有一个进程在运行(同一时间只能处理一个客户请求),因为没有进程或线程的调度切换,因此它的速度是 最快的,表中其他服务模式的运行数值是比迭代模式多出的差值。迭代模式很少用到,在现有的互联网服务中,DNS、NTP服务有它的影子。第1~5行是多进 程服务模式,期中第1行使用现场fork子进程,2~5行都是预先创建15个子进程模式,在多进程程序中套接字传递不太容易(相对于多线程), stevens在这里提供了4个不同的处理accept的方法。6~8行是多线程服务模式,第6行是现场为客户请求创建子线程,7~8行是预先创建15个 线程。表中有的格子是空白的,是因为这个系统不支持此种模式,比如当年的BSD不支持线程,因此BSD上多线程的数据都是空白的。

    从数据的比对看,现场为每客户fork一个进程的方式是最慢的,差不多有20倍的速度差异,Solaris上的现场fork和预先创建子进程的最大差别是504.2 :21.5,但我们不能理解为预先创建模式比现场fork快20倍,原因有两个:

    1. stevens的测试已是十几年前的了,现在的OS和CPU已起了翻天覆地的变化,表中的数值需要重新测试。

    2. stevens没有提供服务器程序整体的运行计时,我们无法理解504.2 :21.5的实际运行效率,有可能是1504.2 : 1021.5,也可能是100503.2 : 100021.5,20倍的差异可能很大,也可能可以忽略。

    因此我写了下面的实验程序,来计算在Linux2.6上创建、销毁10万个进程/线程的绝对用时。

    创建10万个进程(forkcreat.c):

    1. #include <stdlib.h>
    2. #include <signal.h>
    3. #include <stdio.h>
    4. #include <unistd.h>
    5. #include <sys/stat.h>
    6. #include <fcntl.h>
    7. #include <sys/types.h>
    8. #include <sys/wait.h>
    9.  
    10. int count;   /* 子进程创建成功数量 */
    11. int fcount;  /* 子进程创建失败数量 */
    12. int scount;  /* 子进程回收数量 */
    13.  
    14. /* 信号处理函数–子进程关闭收集 */
    15. void sig_chld ( int signo )
    16. {
    17.    
    18.     pid_t chldpid;  /* 子进程id */
    19.      int stat;  /* 子进程的终止状态 */
    20.  
    21.      /* 子进程回收,避免出现僵尸进程 */
    22.      while  ( (chldpid = wait (&stat ) ) >  0 )
    23.      {
    24.         scount++;
    25.      }
    26. }
    27.  
    28. int main ( )
    29. {
    30.      /* 注册子进程回收信号处理函数 */
    31.     signal (SIGCHLD, sig_chld );
    32.  
    33.      int i;
    34.      for  (i =  0; i <  100000; i++ )  //fork()10万个子进程
    35.      {
    36.         pid_t pid = fork ( );
    37.          if  (pid ==  -1 )  //子进程创建失败
    38.          {
    39.             fcount++;
    40.          }
    41.          else  if  (pid >  0 )  //子进程创建成功
    42.          {
    43.             count++;
    44.          }
    45.          else  if  (pid ==  0 )  //子进程执行过程
    46.          {
    47.             exit ( 0 );
    48.          }
    49.      }
    50.  
    51.      printf ( "count: %d fcount: %d scount: %d\n", count, fcount, scount );
    52. }

    创建10万个线程(pthreadcreat.c):

    1. #include <stdio.h>
    2. #include <pthread.h>
    3.  
    4. int count =  0/* 成功创建线程数量 */
    5.  
    6. void thread ( void )
    7. {
    8.      /* 线程啥也不做 */
    9. }
    10.  
    11. int main ( void )
    12. {
    13.     pthread_t id;  /* 线程id */
    14.      int i,ret;
    15.  
    16.      for  (i =  0; i <  100000; i++ )  /* 创建10万个线程 */
    17.      {
    18.         ret = pthread_create (&id,  NULL( void * )thread,  NULL );
    19.          if (ret !=  0 )
    20.          {
    21.              printf  ( "Create pthread error!\n" );
    22.              return  ( 1 );
    23.          }
    24.  
    25.         count++;
    26.  
    27.         pthread_join (id,  NULL );
    28.      }
    29.    
    30.      printf ( "count: %d\n", count );
    31.  
    32. }

    创建10万个线程的Java程序:

    1. public  class ThreadTest
    2. {
    3.      public  static  void main ( String [ ] ags )  throws  InterruptedException
    4.      {
    5.          System. out. println ( "开始运行" );
    6.          long start =  System. currentTimeMillis ( );
    7.          for ( int i =  0; i <  100000; i++ )  //创建10万个线程
    8.          {
    9.              Thread athread =  new  Thread ( );   //创建线程对象
    10.             athread. start ( );                 //启动线程
    11.             athread. join ( );                  //等待该线程停止
    12.          }
    13.        
    14.          System. out. println ( "用时:" +  ( System. currentTimeMillis ( ) – start ) +  " 毫秒" );
    15.      }
    16. }

    在我的赛扬1.5G的CPU上测试结果如下(仍采用测试5次后计算平均值):

    创建销毁10万个进程创建销毁10万个线程创建销毁10万个线程(Java)
    0m18.201s0m3.159s12286毫秒

    从数据可以看出,多线程比多进程在效率上有5~6倍的优势,但不能让我们在使用那种并发模式上定性,这让我想起多年前政治课上的一个场景:在讲到优 越性时,面对着几个对此发表质疑评论的调皮男生,我们的政治老师发表了高见,“不能只横向地和当今的发达国家比,你应该纵向地和过去中国几十年的发展历史 比”。政治老师的话套用在当前简直就是真理,我们看看,即使是在赛扬CPU上,创建、销毁进程/线程的速度都是空前的,可以说是有质的飞跃的,平均创建销 毁一个进程的速度是0.18毫秒,对于当前服务器几百、几千的并发量,还有预先派生子进程/线程的必要吗?

    预先派生子进程/线程比现场创建子进程/线程要复杂很多,不仅要对池中进程/线程数量进行动态管理,还要解决多进程/多线程对accept的“抢” 问题,在stevens的测试程序中,使用了“惊群”和“锁”技术。即使stevens的数据表格中,预先派生线程也不见得比现场创建线程快,在 《Unix网络编程》第三版中,新作者参照stevens的测试也提供了一组数据,在这组数据中,现场创建线程模式比预先派生线程模式已有了效率上的优 势。因此我对这一节实验下的结论是:

    预先派生进程/线程的模式(进程池、线程池)技术,不仅复杂,在效率上也无优势,在新的应用中可以放心大胆地为客户连接请求去现场创建进程和线程。

    我想,这是fork迷们最愿意看到的结论了。

    五、并发服务的不可测性

    看到这里,你会感觉到我有挺进程、贬线程的论调,实际上对于现实中的并发服务具有不可测性,前面的实验和结论只可做参考,而不可定性。对于不可测性,我举个生活中的例子。

    这几年在大都市生活的朋友都感觉城市交通状况越来越差,到处堵车,从好的方面想这不正反应了我国GDP的高速发展。如果你7、8年前来到西安市,穿 过南二环上的一些十字路口时,会发现一个奇怪的U型弯的交通管制,为了更好的说明,我画了两张图来说明,第一张图是采用U型弯之前的,第二张是采用U型弯 之后的。

     

    南二环交通图一

     

    南二环交通图二

    为了讲述的方便,我们不考虑十字路口左拐的情况,在图一中东西向和南北向的车辆交汇在十字路口,用红绿灯控制同一时间只能东西向或南北向通行,一般 的十字路口都是这样管控的。随着车辆的增多,十字路口的堵塞越来越严重,尤其是上下班时间经常出现堵死现象。于是交通部门在不动用过多经费的情况下而采用 了图二的交通管制,东西向车辆行进方式不变,而南北向车辆不能直行,需要右拐到下一个路口拐一个超大的U型弯,这样的措施避免了因车辆交错而引发堵死的次 数,从而提高了车辆的通过效率。我曾经问一个每天上下班乘公交经过此路口的同事,他说这样的改动不一定每次上下班时间都能缩短,但上班时间有保障了,从而 迟到次数减少了。如果今天你去西安市的南二环已经见不到U型弯了,东西向建设了高架桥,车辆分流后下层的十字路口已恢复为图一方式。

    从效率的角度分析,在图一中等一个红灯45秒,远远小于图二拐那个U型弯用去的时间,但实际情况正好相反。我们可以设想一下,如果路上的所有运行车 辆都是同一型号(比如说全是QQ3微型车),所有的司机都遵守交规,具有同样的心情和性格,那么图一的通行效率肯定比图二高。现实中就不一样了,首先车辆 不统一,有大车、小车、快车、慢车,其次司机的品行不一,有特别遵守交规的,有想耍点小聪明的,有性子慢的,也有的性子急,时不时还有三轮摩托逆行一下, 十字路口的“死锁”也就难免了。

    那么在什么情况下图二优于图一,是否能拿出一个科学分析数据来呢?以现在的科学技术水平是拿不出来的,就像长期的天气预报不可预测一样,西安市的交管部门肯定不是分析各种车辆的运行规律、速度,再进行复杂的社会学、心理学分析做出U型弯的决定的,这就是要说的不可测性。

    现实中的程序亦然如此,比如WEB服务器,有的客户在快车道(宽带),有的在慢车道(窄带),有的性子慢(等待半分钟也无所谓),有的性子急(拼命 的进行浏览器刷新),时不时还有一两个黑客混入其中,这种情况每个服务器都不一样,既是是同一服务器每时每刻的变化也不一样,因此说不具有可测性。开发者 和维护者能做的,不论是前面的这种实验测试,还是对具体网站进行的压力测试,最多也就能模拟相当于QQ3通过十字路口的场景。

    结束语

    本篇文章比较了Linux系统上多线程和多进程的运行效率,在实际应用时还有其他因素的影响,比如网络通讯时采用长连接还是短连接,是否采用 select、poll,java中称为nio的机制,还有使用的编程语言,例如Java不能使用多进程,PHP不能使用多线程,这些都可能影响到并发模 式的选型。

    展开全文
  • 进程线程的区别(超详细)

    万次阅读 多人点赞 2019-10-03 21:57:46
    每个进程都有自己独立的块内存空间,进程可以有多个线程,比如在Windows系统中,个运行的xx.exe就是进程。 线程 进程中的个执行任务(控制单元),负责当前进程中程序的执行。进程至少有个线程...
  • 线程进程多线程多进程任务 小结

    千次阅读 多人点赞 2019-04-20 11:59:56
    3 多进程 4 多线程 5 线程进程的关系 6 线程进程的区别 7 进程的优缺点 7.1 进程的优点 7.2 进程的缺点 8 线程的优缺点 8.1 线程的优点 8.2 线程的缺点 9 多线程的优缺点 9.1 多线程的优点 9.2 ...
  • python多进程多线程,多个程序同时运行

    千次阅读 多人点赞 2021-04-08 13:47:15
    python 多线程进程同时运行 多任务要求 python 基础语法 python 文件目录操作 python 模块应用 开发工具 pycharm ...在段时间内交替执行多个任务, 例如单核cpu 处理多任务, 操作系统让各个任务交
  • Linux下多个进程线程同时对个文件进行写操作  Linux下多个进程线程同时对个文件进行写操作,如何解决冲突? 使用flock(锁定文件或解除锁定),简单可行! 先介绍一下flock函数吧 头文件 #...
  • 线程占有的都是不共享的,其中包括:栈、寄存器、状态、程序计数器 ...线程共享的内容包括: 进程 代码段 进程 数据段 进程打开的文件描述符、 信号的处理器、 进程的当前目录和 进程用户 ID 与进程组 ID ...
  • 默认情况linux环境下一进程最多能有多少个线程?: 主线程+自己的线程382 = 383 这我自己已经验证过,测试程序在后面。
  • Linux下一进程究竟会有多少个线程

    千次阅读 2017-10-10 14:53:46
    进程下可以启动最大线程个
  • 大家好,我用CreateProcess创建了一个进程进程的句柄可以获取到, 请问能否通过此句柄创建一个进程的子线程? 如果可以,如何实现? 多谢。
  • 进程线程多进程多线程

    千次阅读 2019-05-08 10:23:05
    什么是进程进程即正在进行中(运行中)的程序。在以前,进程是CPU执行的最小单元。...那么就可以说这些子进程是播放器进程下线程。如今计算机CPU的最小执行单元是线程。 什么是多进程多进程即为任务...
  • 线程VS进程

    万次阅读 2021-03-10 16:44:19
    比如说QQ是是进程,如果你在和A朋友语音聊天的同时和B朋友打字聊天,同时还在QQ群下载图片,这三个操作就相当于开启了三个线程,可以说有了线程之后我们设计的程序就可以一边执行A操作,一边执行B操作了。...
  • Python 多进程下多线程满负荷工作

    千次阅读 2018-08-28 16:58:51
    本文只是写个例子,实现了开多个进程,每个进程下面开多线程,每个进程执行单独个任务,并行执行,进程下的多线程执行任务,并发执行,有效利用CPU资源. import multiprocessing import threading import time def m...
  • 进程包括由操作系统分配的内存空间,包含个或多个线程个线程不能独立的存在,它必须是进程部分。进程一直运行,直到所有的非守护线程都结束运行后才能结束。 线程  又称其为轻量级进程...
  • 线程进程都是现在电脑概念里比较时髦的用语,什么是多线程,什么是多进程?本文详细的给您介绍一下,希望能增进您对当代电脑技术的了解,有不到之处,还往高手予以更正。 . 进程线程进程(英语:Process,中国...
  • 一个进程能运行多少线程

    千次阅读 2019-04-11 15:22:41
    也知道线程的概念,它是程序执行的最小单元,是进程中的个实体用来执行程序,进程中有多个线程。(个cpu内核只能运行进程/线程,台8核cpu的服务器只能同时运行8个进程/线程。只是每个进...
  • 本文介绍种在PHP中解决多进程多线程同时读写同一个文件的问题。
  • 一个进程下线程共享哪些资源

    千次阅读 2015-10-19 21:04:36
    同一进程中的线程究竟共享哪些资源  转自:http://blog.chinaunix.net/uid-12461657-id-3182841.html 线程共享的环境包括:进程代码段、进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)、...
  • Python 多进程多线程启动

    万次阅读 2021-02-22 12:52:49
    Python 多进程启动 def main(self, num): """ 多进程启动 ValueError: Pool not running:这问题的根源在于:pool.close()提前生效,关闭了pool。所以提示pool没有运行。 解决:多层循环的情况,将pool....
  • 多进程 VS 多线程

    千次阅读 2015-12-12 22:05:00
    在Linux编程多用多进程编程少用多线程编程。  IBM有家伙做了测试,发现切换线程context的时候,windows比linux快。进出最快的锁(windows2k的 critical section和linux的pthread_mutex),windows比...
  • 多线程():创建线程线程的常用方法

    万次阅读 多人点赞 2018-09-01 19:14:23
    了解并发编程:实际工作中很少写多线程的代码,这部分代码一般都被人封装起来了,在业务中使用多线程的机会也不是很(看具体项目),但是作为一个高级程序员如果不会多线程是说不过去的。 二:进程线程 ...
  • Java中main方法启动的是一个线程也是进程个java程序启动后它就是进程进程相当于个空盒,它只提供资源装载的空间,具体的调度并不是由进程来完成的,而是由线程来完成的。个java程序从main开始之后...
  • 多线程多进程

    千次阅读 2019-02-27 10:26:49
    多线程共享进程数据:共享简单;同步复杂 各有优势 内存、CPU 占用内存,切换复杂,CPU利用率低 占用内存少,切换简单,CPU利用率高 线程占优 创建销毁、切换 创建销毁、切换复杂,速度慢 创建...
  • 进程(process)和线程(thread)是操作系统的基本概念 进程是具有一定独立功能的程序关于某个数据集合上的次运行活动,进程是系统进行资源分配和调度的...同进程中的多个线程之间可以并发执行进程和线程的区别在
  • 进程最多创建多少个线程

    千次阅读 2018-10-10 23:02:38
    最近,在做个关于聊天服务器的项目,其中遇到了个问题,那就是进程可以产生多少个线程呢? 开始各种想象,会和不同平台,不同系统相关,网上很大佬说是1024个,也有256个。 与其无端猜测,不如动手测试...
  • python 彻底解读多线程多进程

    万次阅读 多人点赞 2019-03-26 14:20:34
    title: 多线程多进程 copyright: true top: 0 date: 2019-03-03 16:16:41 tags: 多线程多进程 categories: Python高阶笔记 permalink: password: keywords: description: 对python的多线程多进程进一步刨析。 真是...
  • 线程是什么?要理解这个概念,须要先了解一下操作系统的一些相关概念。大部分操作系统(如...这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个
  • 进程 线程 多线程 并发 同步异步

    千次阅读 2014-11-21 13:43:06
    进程 线程 多线程 并发 同步异步 很多人对进程,线程,多线程,并发,同步,异步等概念感到困惑,这都是大学没好好听课的缘故啊.咱在这里帮感到概念给感到困惑的同学复习.

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 734,025
精华内容 293,610
关键字:

一个进程下的多个线程