进程与线程的区别

在开发工作中,尤其是对负载较大的服务端程序的开发,为充分发挥处理器多核性能,提高硬件资源利用率,增加系统吞吐量,少不了并发编程。并发编程一般通过多进程和多线程的方式实现。

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配的基本单位。程序是静态实体,进程则是动态的运行实体。操作系统为了使多个程序并发执行,提高CPU利用率,故引入进程对程序进行管理。一个程序通常有多个功能模块,假设一个应用程序由两部分组成,计算部分和I/O部分,在未引入进程之前,计算部分和I/O部分,不能并发执行,更不能并行执行,即运行计算部分,需要I/O部分执行完成,反之亦然,执行I/O部分,需要计算部分执行完成。这样的运行模式是对资源的极大浪费,因为I/O部分在运行时,CPU是空闲的,在计算部分运行时,I/O设备是空闲。为了提高硬件资源的利用率和系统性能,可以使用进程来管理计算部分和I/O部分,分别称之为计算进程和I/O进程,那么此时计算进程和I/O进程可以同时运行,并行操作,极大地提高了系统性能和硬件资源利用率。在单个程序中同时运行多个进程完成不同的工作,称为多进程。

上面使用进程来管理单个程序不同功能模块,使单个程序的不同功能模块可以并行执行。使用进程来管理程序,也可以使多个程序之间并发执行。程序是指令、数据及其组织形式的描述,进程是程序能够独立运行的活动实体,由一组机器指令、数据和堆栈等组成。进程拥有三种状态,就绪状态(Ready State)、运行状态(Running)和阻塞状态状态(Blocked State)。就绪状态指进程已获得除处理器外的所需资源,等待分配处理器资源,只要分配了处理器就可执行。就绪进程可以按多个优先级来划分队列,高优先级队列中排队的进程将优先获得处理器资源,进入运行状态。运行状态指进程占用处理器资源处于执行状态,处于此状态的进程数目小于等于处理器的数目。阻塞状态指进程等待某种条件(如I/O操作或进程同步),在条件满足之前,即使把处理器资源分配给该进程,也无法运行。

线程(Thread)是进程中的一个实体,是系统中独立运行和调度的基本单位,亦被称为轻量级进程(Light Weight Process,LWP)。因进程拥有系统资源,在不同进程之间切换和调度,付出的开销较大,所以提出了比进程更小、更轻量的单位线程,作为操作系统执行和调度的基本单位。由于线程自己不拥有系统资源,只拥有在运行中必不可少的少部分资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源(比如CPU、堆栈等),所以调度起来付出的开销更小。线程也有就绪、运行和阻塞三种基本状态。在单个进程中同时运行多个线程完成不同的工作,称为多线程。 进程和线程都是程序运行时衍生的概念,容易混淆,下面说一下具体的区别。 (1)定义不同。进程是系统分配资源的独立单元,而线程是执行和调度的基本单元; (2)所属不同。进程属于程序,线程属于进程。进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程。 (3)通信机制不同。进程间不共享资源,通信需要特殊手段,比如管道、FIFO、信号等,线程间共享进程资源,直接通信。由于多个线程共享进程资源,对临界资源访问时,往往涉及到线程间的同步问题。 (4)创建方式不同。Linux中,进程的创建调用fork或者vfork,而线程的创建调用pthread_create。 (5)安全性不同。因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径,一个线程死掉,整个进程也会死掉。所以进程的安全性会高于线程。

下面演示Linux环境下,分别使用多进程和多线程方式将两部分标准输出并行化。首先看一下串行程序。

#include <stdio.h>

int main(int argc,char* argv[])
{
    //第一部分标准输出,从0输出到9
    for(int i=0;i<10;++i)
    {
        printf("part one %d\n",i);
    }

    //第二部分标准输出,从0输出到9
    for(int i=0;i<10;++i)
    {
        printf("part two %d\n",i);
    }
}

多进程方式:

#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#include <stdio.h>
#include <stdlib.h>

int main(int argc,char* argv[])
{
     pid_t pid=0;

    //创建子进程,程序开始分叉,分为父子进程
    pid=fork();

    //一次fork()返回两次,父进程中返回子进程ID,子进程返回0,出错返回-1
    if(0==pid)
    {
        //子进程进行第一部分标准输出,从0输出到9
        for(int i=0;i<10;++i)
        {
            printf("in child process part one %d\n",i);
        }
        exit(0);
    }
    else if(pid>0)
    {
        //父进程中进行第二部分标准输出,从0输出到9
        for(int i=0;i<10;++i)
        {
            printf("in parent process part two %d\n",i);
        }

        int childStatus=0;
        //父进程中阻塞式等待子进程结束并回收子进程资源。若子进程已经结束,则立即返回,若子进程未结束,则阻塞等待,直到有信号来到或子进程结束。
        //成功返回子进程ID,失败返回-1
        int ret=waitpid(pid,&childStatus,0);
        if(ret==pid)
        {
            printf("wait child process %d successfully!\n",ret);
        }
    }
    else
    {
        perror("fork");
    }
}

多进程方式输出结果:

in parent process part two 0
in parent process part two 1
in parent process part two 2
in child process part one 0
in parent process part two 3
in child process part one 1
in parent process part two 4
in child process part one 2
in child process part one 3
in child process part one 4
in parent process part two 5
in parent process part two 6
in parent process part two 7
in parent process part two 8
in parent process part two 9
in child process part one 5
in child process part one 6
in child process part one 7
in child process part one 8
in child process part one 9
wait child process 3194 successfully!

从输出结果可以看出,两部分输出出现交叉的情况,表明输出实现了并行。 下面看一下多线程实现方式:

#include <pthread.h>

#include <stdio.h>

//线程函数1。函数结束,线程结束
void* threadFunc1(void* args)
{
    //线程1进行第一部分标准输出,从0输出到iterateNum
    int iterateNum=*(int*)args;
    for(int i=0;i<iterateNum;++i)
    {
        printf("in thread1 part one %d\n",i);
    }
}

//线程函数2。函数结束,线程结束
void* threadFunc2(void* args)
{
    //线程1进行第一部分标准输出,从0输出到iterateNum
    int iterateNum=*(int*)args;
    for(int i=0;i<iterateNum;++i)
    {
        printf("in thread2 part two %d\n",i);
    }
}

int main(int argc,char* argv[])
{
    int args = 10;
    pthread_t thread1ID=0,thread2ID=0;

    //创建线程1
    pthread_create(&thread1ID,NULL,threadFunc1,&args);
    //创建线程2
    pthread_create(&thread2ID,NULL,threadFunc2,&args);

    //阻塞等待线程1结束并回收资源
    int ret1=pthread_join(thread1ID,NULL);
    if(0==ret1)
    {
        printf("thread1 %zu finished\n",thread1ID);
    }
    //阻塞等待线程2结束并回收资源
    int ret2=pthread_join(thread2ID,NULL);
    if(0==ret2)
    {
        printf("thread2 %zu finished\n",thread2ID);
    }
}

输出结果:

in thread1 part one 0
in thread1 part one 1
in thread1 part one 2
in thread1 part one 3
in thread1 part one 4
in thread1 part one 5
in thread2 part two 0
in thread2 part two 1
in thread2 part two 2
in thread1 part one 6
in thread1 part one 7
in thread1 part one 8
in thread1 part one 9
in thread2 part two 3
in thread2 part two 4
in thread2 part two 5
in thread2 part two 6
in thread2 part two 7
in thread2 part two 8
in thread2 part two 9
thread1 139932212193024 finished
thread2 139932203800320 finished

同样地,从数据结果的交叉情况可以看出,两部分输出实现了并行。

上面在介绍进程与线程的区别时,多次提及并发(Concurrency)与并行(Parallelism)的概念,二者虽很相似但有着本质的区别,下面简单地介绍一下二者的概念和区别。

并行指两个或者多个事件在同一时刻发生,并发指两个或多个事件在同一时间间隔内发生。在多道程序环境下,并发性是指在一段时间内宏观上有多个程序在同时运行,但在单处理机系统中,每一时刻却仅能有一道程序执行,故微观上这些程序只能是分时地交替执行。倘若在计算机系统中有多个处理机,则这些可以并发执行的程序便可被分配到多个处理机上,即利用每个处理机来处理单个程序,这样,多个程序便可以同时执行,这样就实现了实现并行执行。 这里引用Erlang之父Joe Armstrong对并发与并行区别的形象描述。首先看一下下面这张图。

并发是两个等待队列中的人同时去竞争一台咖啡机,谁先竞争到咖啡机谁使用;而并行是每个队列拥有自己的咖啡机,两个队列之间没有竞争的关系。因此,并发意味着多个执行实体(人)需要竞争资源(咖啡机),就不可避免带来竞争和同步的问题;而并行则是不同的执行实体拥有各自的资源,相互之间互不干扰。如果是串行执行的话,一个队列使用一台咖啡机,那么哪怕最前面的人便秘了去厕所呆半天,后面的人也只能等着他回来才能去接咖啡,这效率无疑是最低的。所以,现在操作系统中会引入并发与并行的机制来提高系统效率。可以用一句话总结并行与并发的区别:并发是逻辑上的同时发生,并行是物理上的同时发生。

说到并发与并行,为提高系统效率,计算机在不同层次上使用了并行技术,按系统层次结构由低到高主要有: 位级并行(Bit-level Parallelism)。基于处理器的字长,比如64位的CPU能在同一时间内处理字长为64位的二进制数据,比32位字长的CPU多处理一倍的数据。

数据级并行(Data-level Parallelism)。单指令多数据流(SIMD)是一种实现数据级并行的技术。SIMD表示一条指令可以同时完成多个操作。以加法指令为例,单指令单数据流(SISD)型CPU一条指令只能完成一对操作数相加,SIMD一条指令可以同时完成多对操作数相加。

指令级并行(Instruction-level Parallelism)。计算机处理问题是通过指令实现的,当指令之间不存在相关时,它们在流水线中是可以重叠起来并行执行。 指令级并行基于流水线(Pipeline)技术,将一条指令所需的活动划分成不同的步骤,每个步骤交由不同的硬件处理,不同指令的相同步骤并行执行,达到指令级并行。

线程级并行(Thread-level Parallelism)是上文中通过多线程并行执行,来达到提高系统效率,硬件基础是超线程与多核处理器。超线程允许一个单核CPU同时执行多个控制流(线程),多核处理器的不同核心能够同时执行线程。举例来说,4核的Intel Core i7处理器,配合超线程技术,可以并行执行8个线程。

任务级并行(Task Parallelism)。将作业分解为可并行处理的多个任务,每个任务则被分配分布式计算系统的各个计算节点中完成。


参考文献

[1]进程和线程的区别 [2]计算机操作系统.汤晓丹 [3]并发.百度百科 [4]并发与并行的区别.百家号

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏行者悟空

Hadoop之RPC机制

13010
来自专栏Flutter入门到实战

一行代码快速解耦Application逻辑,让Application更简洁好维护

版权声明:本文为博主原创文章,未经博主允许不得转载。https://www.jianshu.com/p/23b9ba9b685d

9430
来自专栏Jed的技术阶梯

Kafka 新版消费者 API(一):订阅主题

说明:这两个参数分别指定了 TCP socket 接收和发送数据包的缓冲区大小。如果它们被设为 -1,就使用操作系统的默认值。如果生产者或消费者与 broker...

96320
来自专栏企鹅号快讯

Java Web 模板代码生成器的设计与实现

起因 项目中需要根据数据库表写很多Meta、Dao、Service代码,其中很多代码都是重复而繁琐的。因此如果有一个模板代码的生成器,就可以一定程度提高开发效率...

296100
来自专栏mini188

基于 Asp.Net的 Comet 技术解析

Comet技术原理 来自维基百科:Comet是一种用于web的技术,能使服务器能实时地将更新的信息传送到客户端,而无须客户端发出请求,目前有两种实现方式,长轮询...

26480
来自专栏蜉蝣禅修之道

获取pdf文档属性的方法

22040
来自专栏程序小工

【实战】Tp5+小程序(三)--微信登录与令牌

ThinkPHP5 从入门到深入学习,结合实战项目深入理解 ThinkPHP5 的特性和使用方法。深入学习 api 开发,学习微信登录和令牌的相关知识,并理解微...

2.5K30
来自专栏专栏

跨环境测试框架介绍-pytest的高级用法

本文将介绍针对测试和生产等不同测试环境下,维护一套可读性,追溯性强的测试用例的工具-pytest。

64240
来自专栏程序员阿凯

一条大河波浪宽 -- 数据库连接池实现

12440
来自专栏Java技术栈

单点登录终极方案之 CAS 应用及原理

Cookie的单点登录的实现方式很简单,但是也问题颇多。例如:用户名密码不停传送,增加了被盗号的可能。另外,不能跨域!

34820

扫码关注云+社区

领取腾讯云代金券