进程和线程

并发是指宏观上在一段时间内能同时运行多个程序,而并行则指同一时刻能运行多个指令。

并行需要硬件支持,如多流水线、多核处理器或者分布式计算系统。

操作系统通过引入进程和线程,使得程序能够并发运行。

堆:是大家共有的空间,分全局堆和局部堆。全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。

栈:是个线程独有的,保存其运行状态和局部自动变量的。栈在线程开始的时候初始化,每个线程的栈互相独立,因此,栈是 thread safe的。操作系统在切换线程的时候会自动的切换栈,就是切换 SS/ESP寄存器。栈空间不需要在高级语言里面显式的分配和释放。

进程和线程的区别

by Guide
在这里插入图片描述
在这里插入图片描述

从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈和 本地方法栈。

一个标准的线程由线程ID、程序计数器(pc)、一组寄存器和堆栈组成。通常,一个进程由多个线程组成,每个线程之间共享进程的内存空间(包括代码段、数据段、堆等)及一些进程级的资源(如打开的文件描述符和信号)。
在这里插入图片描述
线程的访问非常自由,它可以访问进程内存里的所有数据,同时线程也拥有自己IDE私有存储空间,包括以下几方面:

  • 1)栈
  • 2)线程局部存储(TLS)。
  • 3)寄存器(包括PC寄存器)

线程私有区,包含以下3类:

  • 程序计数器:记录正在执行的虚拟机字节码的地址;
  • 虚拟机栈:方法执行的内存区,每个方法执行时会在虚拟机栈中创建栈帧;
  • 本地方法栈:虚拟机的Native方法执行的内存区;

在代码中的方法调用过程中,往往需要从一个方法跳转到另一个方法,执行完再返回,那么在跳转之前需要在当前方法的基本信息压入栈中保存再跳转。
在这里插入图片描述
可以看到,在虚拟机栈有一帧帧的 栈帧组成,而栈帧包含局部变量表,操作栈等子项,那么线程在运行的时候,代码在运行时,是通过程序计数器不断执行下一条指令。真正指令运算等操作时通过控制操作栈的操作数入栈和出栈,将操作数在局部变量表和操作栈之间转移。
Java栈是由许多栈帧(frame)组成,一个栈帧包含一个Java方法的调用状态。当现成调用一个Java方法时,JVM压入一个新的栈帧到该线程的Java栈中;当方法返回时,这个栈帧被从Java栈中弹出并抛弃。
如有分别存在局部变量区的a,b,c,要计算c=a+b,则使用4条指令:

  • 读局部变量区[0]的值,压入操作数栈;
  • 读局部变量区[1]的值,压入操作数栈;
  • 弹出操作数栈栈顶值,再弹出操作数栈栈顶值,相加,把结果压入操作数栈;
  • 弹出操作数栈栈顶值,写入局部变量区[2]。

总结: 线程是进程划分成的更小的运行单位, 一个进程在其执行的过程中可以产生多个线程,基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反。进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

进程是资源分配的基本单位。进程控制块 (Process Control Block, PCB) 描述进程的基本信息和运行状态,所谓的创建进程和撤销进程,都是指对 PCB 的操作。

线程是资源调度的基本单位。一个进程中可以有多个线程,它们共享进程资源。QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。

区别
Ⅰ 拥有资源

进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。

Ⅱ 调度

线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程中的线程切换到另一个进程中的线程时,会引起进程切换。

Ⅲ 系统开销

由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程不一样,线程拥有独立的堆栈空间及程序计数器,但是共享数据段,它们彼此之间使用相同的地址空间,共享大部分数据,比进程更节俭,线程切换时只需保存和设置少量寄存器内容,开销比较小,切换速度也比进程快,效率高,但是正由于进程之间独立的特点,使得进程安全性比较高,也因为进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。一个线程死掉就等于整个进程死掉。

Ⅳ 通信方面

体现在通信机制上面,线程间可以通过直接读写同一进程中的共享数据进行通信,但是正因为进程之间互不干扰,相互独立,进程的通信机制相对很复杂,譬如管道,信号,消息队列,共享内存,套接字等通信机制。

V CPU系统

线程使得CPU系统更加有效,因为操作系统会保证当线程数不大于CPU数目时,不同的线程运行于不同的CPU上。

进程有哪几种状态

  1. 创建状态(new) :进程正在被创建,尚未到就绪状态。
  2. 就绪状态(ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行。
  3. 运行状态(running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)。
  4. 阻塞状态(waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行。
  5. 结束状态(terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行。终止一个进程需要两个步骤:先等待操作系统或相关的进程进行善后处理;然后回收占用的资源并被系统删除。

进程间的通信方式

  1. 管道/匿名管道(Pipes) :用于具有亲缘关系的父子进程间或者兄弟进程之间的通信。
  2. 有名管道(Names Pipes) : 匿名管道由于没有名字,只能用于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out)。有名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。
  3. 信号(Signal) :信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生;
  4. 消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道(无名管道:只存在于内存中的文件;命名管道:存在于实际的磁盘介质或者文件系统)不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除一个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不一定要以先进先出的次序读取,也可以按消息的类型读取.比 FIFO 更有优势。消息队列克服了信号承载信息量少,管道只能承载无格式字 节流以及缓冲区大小受限等缺。
  5. 信号量(Semaphores) :信号量是一个计数器,用于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信方式主要用于解决与同步相关的问题并避免竞争条件。
  6. 共享内存(Shared memory) :使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有用的进程间通信方式。
  7. 套接字(Sockets) : 此方法主要用于在客户端和服务器之间通过网络进行通信。套接字是支持 TCP/IP 的网络通信的基本操作单元,可以看做是不同主机之间的进程进行双向通信的端点,简单的说就是通信的两方的一种约定,用套接字中的相关函数来完成通信过程。

线程间的同步的方式

进程同步指两个以上进程基于某个条件来协调它们的活动。一个进程的执行依赖于协作进程的消息或信号,当一个进程没有得到来自于协作进程的消息或信号时需等待,直到消息或信号到达才被唤醒.

线程同步: 指多线程通过特定的设置(如互斥量,事件对象,临界区)来控制线程之间的执行顺序(即所谓的同步)也可以说是在线程之间通过同步建立起执行顺序的关系,如果没有同步,那线程之间是各自运行各自的!

线程间的同步的方式:

  1. 临界区: 通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。它并不是核心对象,不是属于操作系统维护的,而是属于进程维护的。
  2. 互斥量(Mutex): 内核对象,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
  3. 信号量(Semphares) : 内核对象,它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
  4. 事件(Event) : 内核对象,Wait/Notify:通过通知操作的方式来保持多线程同步,事件机制,允许一个线程在处理完一个任务后,主动唤醒另一个线程执行任务,通过通知操作的方式来保持线程的同步。还可以方便的实现多线程优先级的比较操作。

进程的调度算法

确定首先执行哪个进程以及最后执行哪个进程以实现最大 CPU 利用率。

  • 先到先服务(FCFS)调度算法 : 从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
  • 短作业优先(SJF)的调度算法 : 从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度。
  • 时间片轮转调度算法 : 时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin)调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间。
    多级反馈队列调度算法 :前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程 。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业(进程)迅速完成。,因而它是目前被公认的一种较好的进程调度算法,UNIX 操作系统采取的便是这种调度算法。
  • 优先级调度 : 为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推。具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级。

多个线程同时访问一个共享数据

在这里插入图片描述

在许多体系结构上,++i的实现会如下:

1)读取i到某个寄存器X

2)X++

3)将X的内存存储回i

由于线程1和线程2的并发执行,因此两个线程的执行序列可能如下:
在这里插入图片描述
从程序的逻辑看,正确的结果应该是i为0.但是由于执行的序列问题,可能出现的结果有0,1,2。可见,两个线程同时操作一个共享数据会出现意想不到的结果。

很明显,这里出现错误的原因主要在于自增(++)操作被操作系统编译为汇编代码之后不止一条指令,因此在多线程环境下就可能出现执行了一半而被调度系统打断,去执行其他的代码。如果单条指令是原子的,则执行就不会被打断。问题是,尽管原子操作非常方便,但是它仅适用于比较简单的场合。

为了避免多个线程同时读写一个数据而出现不可预料的结果,我们需要将各种线程对同一数据的访问同步。所谓同步,即是指在一个线程访问数据未结束的时候,其他线程不得对同一个数据进行访问。

同步的最常见方法是加锁。锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,访问完后释放锁。

过度优化
有时候过度优化也会造成线程安全问题。
在这里插入图片描述
由于有锁的保护,x++的行为不会被并发所破坏,那么x似乎必然为2.然而,如果编译器为了提高x的访问速度,把x放入了某个寄存器中,那么我们知道不同线程的寄存器是各自独立的,此时就出现线程安全问题,例如:

在这里插入图片描述
可见,现在即使加锁也不能保证结果正确。

我们可以使用volatile关键字试图阻止过度优化。volatile可以阻止两件事情:

1)阻止编译器为了提高速度将一个变量缓存在寄存器内而不写回。

2)阻止编译器调整操作volatile变量的指令。

进程和线程的选择取决于什么

1.需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程的代价是很大的。

2.线程的切换速度快,所以在需要大量计算,切换频繁时使用线程,还有耗时的操作时用使用线程可提高应用程序的响应。

3.因为对CPU系统的效率使用上线程更占优势,所以可能要发展到多机分布的用进程,多核分布用线程。

4.并行操作时用线程,如C/S架构的服务器端并发线程响应用户的请求。

5.需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。

面试题:为什么你的项目中用的是线程?为什么不用进程?如果只有进程,对你这个项目有没有影响?

因为我的项目中需要对数据段的数据共享,可以被多个程序所修改,所以使用线程来完成此操作,无需加入复杂的通信机制,使用进程需要添加复杂的通信机制实现数据段的共享,增加了我的代码的繁琐,而且使用线程开销小,项目运行的速度快,效率高。

如果只用进程的话,虽然安全性高,但是对代码的简洁性不好,程序结构繁琐,开销比较大,还需要加入复杂的通信机制,会使得我的项目代码量大大增加,切换速度会变的很慢,执行效率降低不少。

进程切换

进程控制块

进程映像表示了一个进程在虚拟内存中的结构,包括程序代码、用户数据、栈(保存参数、局部变量、调用地址等)和属性。共享地址空间是每个进程所共享的一块区域,比如操作系统的内核代码。

在这里插入图片描述
进程的属性是由一种称为进程控制块的数据结构来表示的。
进程控制块的信息可以分为3类:
进程标识信息。包括进程的标识符pid,该进程的父进程标识符,用户标识符uid。
处理器状态信息。包括用户可见寄存器(用户模式下可以使用的寄存器),控制和状态寄存器(程序计数器、模式信息),栈指针(栈用于保存参数或过程调用的地址,栈指针指向栈顶)
进程控制信息。包括调度状态信息(进程状态、优先级、等待事件的标识等),资源使用情况(进程所控制的物理资源),存储管理(描述分配给进程的虚拟内存的页表指针)。

进程创建

1)为新进程分配一个唯一的进程标识符pid;
2)为新进程分配空间,包括进程映像中的所有元素;
3)初始化进程控制块。对进程控制块中的各字段值设置初始值,注意子进程会继承父进程的资源;
4)设置进程的链接,比如将一个新进程放入就绪链表中;

进程切换

进程切换指从正在运行的进程中收回处理器,让待运行进程来占有处理器运行。实质上就是被中断运行进程与待运行进程的上下文切换。

模式切换:进程切换必须在操作系统内核模式下完成,这就需要模式切换。模式切换又称处理器切换,即用户模式和内核模式的互相切换。

步骤:
1)保存处理器的上下文信息,包括程序计数器和其他寄存器的值;
2)更新当前进程的进程控制块,包括更改进程状态;
3)根据进程的状态,将该进程的进程控制块加入到相应的队列,比如就绪队列,事件i的阻塞队列等;
4)处理器根据调度算法,选中另一个就绪进程;
5)更改新进程的进程控制块,包括更改进程状态为运行态;
6)更新内存管理数据结构,是否需要更新取决于管理地址转换的方式;
7)将处理器的上下文恢复为新进程上次退出运行时保存的上下文信息。

进程切换何时发生

进程切换一定发生在中断/异常/系统调用处理过程中,常见的有以下情况:
1、阻塞式系统调用、虚拟地址异常。导致被中断进程进入等待态。

2、时间片中断、I/O中断后发现更改优先级进程。导致被中断进程进入就绪态。

3、终止用系统调用、不能继续执行的异常。导致被中断进程进入终止态。

但是并不意味着所有的中断/异常都会引起进程切换。有一些中断/异常不会引起进程状态转换,不会引起进程切换,只是在处理完成后把控制权交还给被中断进程。

以下是处理流程:
1、(中断/异常等触发)正向模式切换并压入PSW/PC (Program Status Word 程序状态字。program counter 程序计数器。指向下一条要执行的指令)。
2、保存被中断进程的现场信息。
3、处理具体中断、异常。
4、恢复被中断进程的现场信息。
5、(中断返回指令触发)逆向模式转换并弹出PSW/PC。

线程切换

线程上下文是指某一时间点 CPU 寄存器和程序计数器的内容,CPU通过时间片分配算法来循环执行任务(线程),因为时间片非常短,所以CPU通过不停地切换线程执行。

CPU切换前把当前任务的状态保存下来,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行。任务的状态保存及再加载, 这段过程就叫做上下文切换。

每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量)堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)

寄存器 是 CPU 内部的数量较少但是速度很快的内存(与之对应的是 CPU 外部相对较慢的 RAM 主内存)。寄存器通过对常用值(通常是运算的中间值)的快速访问来提高计算机程序运行的速度。

程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

线程切换的时候实际上是指线程上下文的切换,是将来用于恢复线程环境必须的信息,包括所有必须保存的寄存器和程序计数器,线程的状态等,切换步骤:

  • 挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处
  • 恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
  • 跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中

线程上下文切换会有什么问题呢?

上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。

直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小

切换查看
Linux系统下可以使用vmstat命令来查看上下文切换的次数, 其中cs列就是指上下文切换的数目(一般情况下, 空闲系统的上下文切换每秒大概在1500以下)
在这里插入图片描述
线程让出cpu的情况:

  • 当前运行线程主动放弃CPU,JVM暂时放弃CPU操作(基于时间片轮转调度的JVM操作系统不会让线程永久放弃CPU,或者说放弃本次时间片的执行权),例如调用yield()方法。
  • 当前运行线程因为某些原因进入阻塞状态,例如阻塞在I/O上
  • 当前运行线程结束,即运行完run()方法里面的任务

引起线程上下文切换的因素

  • 当前执行任务(线程)的时间片用完之后,系统CPU正常调度下一个任务
  • 中断处理,在中断处理中,其他程序”打断”了当前正在运行的程序。当CPU接收到中断请求时,会在正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断和软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
  • 用户态切换,对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
  • 多个任务抢占锁资源,在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换

因此优化手段有:

  • 无锁并发编程,多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据
  • CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁
  • 使用最少线程
  • 协程,单线程里实现多任务的调度,并在单线程里维持多个任务间的切换
  • 合理设置线程数目既可以最大化利用CPU,又可以减少线程切换的开销。

高并发,低耗时的情况,建议少线程。
低并发,高耗时的情况:建议多线程。
高并发高耗时,要分析任务类型、增加排队、加大线程数

进程切换和线程切换的区别

进程切换与线程切换的一个最主要区别就在于进程切换涉及到虚拟地址空间的切换而线程切换则不会。因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。

用户级线程与内核级线程

在多线程环境中,一个进程可以定义多个并发线程,方法是使用用户级线程或内核级线程。

用户级线程:由进程用户空间中运行的线程库来创建并管理,线程库调度线程,类似于内核调度进程,而内核是感知不到线程存在的。优点:切换用户级线程时,进程不需要为了管理线程而切换到内核模式,节省了两次状态转换(即用户模式到内核模式,内核模式到用户模式)的开销。缺点:由于内核不知道线程的存在,内核是以进程为单位进行调度的,所以当进程中的某一个线程阻塞时,就会认为整个进程就阻塞了。并且,内核每次只把一个进程分配给一个处理器,那么一个进程内就无法使用多处理器处理技术。

内核级线程:由内核维护,所以内核可以感知到线程的存在。那么,用户级线程中的缺点可以得到解决,即同一进程的多个进程可以在多处理器上并行执行,且一个线程的阻塞不会阻塞整个进程。但是,用户级线程的优点也变成了内核级线程的缺点,即线程间切换需要进行模式转换。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值