进程
进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
直白地讲,进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。
进程拥有代码和打开的文件资源、数据资源、独立的内存空间。
线程
线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。线程拥有自己的栈空间。

有人给出了很好的归纳:
对操作系统来说,线程是最小的执行单元,进程是最小的资源管理单元。无论进程还是线程,都是由操作系统所管理的。Java中线程具有五种状态:
初始化 可运行 运行中 阻塞 销毁
这五种状态的转化关系如下:

但是,线程不同状态之间的转化是谁来实现的呢?是JVM吗?
并不是。JVM需要通过操作系统内核中的TCB(Thread Control Block)模块来改变线程的状态,这一过程需要耗费一定的CPU资源。
进程和线程的痛点
线程之间是如何进行协作的呢?
最经典的例子就是生产者/消费者模式:
若干个生产者线程向队列中写入数据,若干个消费者线程从队列中消费数据。

如何用java语言实现生产者/消费者模式呢?
让我们来看一看代码:
1 | public class ProducerConsumerTest { |
这段代码做了下面几件事:
定义了一个生产者类,一个消费者类。
生产者类循环100次,向同步队列当中插入数据。
消费者循环监听同步队列,当队列有数据时拉取数据。
如果队列满了(达到5个元素),生产者阻塞。
如果队列空了,消费者阻塞。
上面的代码正确地实现了生产者/消费者模式,但是却并不是一个高性能的实现。为什么性能不高呢?原因如下:
涉及到同步锁。
涉及到线程阻塞状态和可运行状态之间的切换。
涉及到线程上下文的切换。
以上涉及到的任何一点,都是非常耗费性能的操作。
协程
协程,英文Coroutines,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。

最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。
这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。
进程和线程的区别
进程——资源分配的最小单位,
线程——程序执行的最小单位。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程与线程主要区别是它将不再被内核调度,而是交给了程序自己而线程是将自己交给内核调度。
实际意义的区别
(1)一个程序至少有一个进程,一个进程至少有一个线程。线程(Thread)是进程的一个实体,是CPU调度和分派的基本单位;
(2)进程拥有独立的内存单元,而多个线程共享内存。从而线程效率更高;
(3)进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮;
(4)进程切换时,耗费资源较大,效率要差一些;
(5)进程是系统资源分配的基本单位,线程是调度的基本单位。
比较进程线程的优点
(1)易于调度。
(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
(3)开销少。创建线程比创建进程要快,所需开销很少。
(4)利于充分发挥多处理器的功能。
相比进程线程的缺点
(1)线程之间的同步和加锁控制比较麻烦
(2)一个线程的崩溃影响到整个程序的稳定性
(3)线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU
通讯的区别
(1)每个进程有自己的地址空间。两个进程中的地址即使值相同,实际指向的位置也不同。进程间通信一般通过操作系统的公共区进行。 同一进程中的线程因属同一地址空间,可直接通信。
(2)只有进程间需要通信,同一进程的线程share地址空间,没有通信的必要,但要做好同步/互斥mutex,保护共享的全局变量。线程拥有自己的栈。同步/互斥是原语primitives. 而进程间通信无论是信号,管道pipe还是共享内存都是由操作系统保证的,是系统调用.
(3)线程间通信:由于多线程共享地址空间和数据空间,所以多个线程间的通信是一个线程的数据可以直接提供给其他线程使用,而不必通过操作系统(也就是内核的调度)。进程间的通信则不同,它的数据空间的独立性决定了它的通信相对比较复杂,需要通过操作系统。以前进程间的通信只能是单机版的,现在操作系统都继承了基于套接字(socket)的进程间的通信机制。这样进程间的通信就不局限于单台计算机了,实现了网络通信。
切换和调度
线程上下文切换比进程上下文切换要快得多,在多线程程序下,进程不是一个可执行的实体
协程
协程是用户模式下的轻量级线程,最准确的名字应该叫用户空间线程(User Space Thread)
操作系统内核对协程一无所知,协程的调度完全有应用程序来控制,操作系统不管这部分的调度;
一个线程可以包含一个或多个协程,协程拥有自己的寄存器上下文和栈,协程调度切换时,将寄存器上下文和栈保存起来,在切换回来时恢复先前保运的寄存上下文和栈。
协程的优势如下:
- 节省内存,每个线程需要分配一段栈内存,以及内核里的一些资源
- 节省分配线程的开销(创建和销毁线程要各做一次 syscall)
- 节省大量线程切换带来的开销
- 与 NIO 配合实现非阻塞的编程,提高系统的吞吐
进程间的通讯方式
(1)管道:半双工;数据只能单向流动,只能用于具有亲缘关系的进程之间,即用于父子、兄弟之间。
(2)命名管道(FIFO):半双工,允许无亲缘关系的进程
(3)消息队列:消息链表存于内核,每个消息队列由消息队列标识符标识;于管道不同的是,消息队列存放在内核中,只有在内核重启时才能删除一个消息队列;消息队列的大小受限制。
(4)信号量(semophore):信号量是一个计数器,可以用来控制多个进程对于共享资源的访问。作为一种锁机制,防止某进程正在访问共享资源师,其他进程也访问该资源,常用来处理临界资源的访问同步问题。
临界资源:为某一时刻只能由一个进程或线程操作的资源。
(5)共享内存:就是映射一段能被其他进程所访问的内存,这段内存由一个进程创建,但可以多个进程同时访问,可以说是最有用的进程间通信方式,也是最快的IPC形式。 常与其他通讯机制(信号量)配合使用。
(6)套接字:也可用于不同机器之间。
(7)信号(Signal):比较复杂,用于通知接收进程某个事件已经发生
进程的状态
创建状态:程由创建而产生。创建进程是一个非常复杂的过程,一般需要通过多个步骤才能完成:如首先由进程申请一个空白的进程控制块(PCB),并向PCB中填写用于控制和管理进程的信息;然后为该进程分配运行时所必须的资源;最后,把该进程转入就绪状态并插入到就绪队列中。
就绪状态:这是指进程已经准备好运行的状态,即进程已分配到除CPU以外所有的必要资源后,只要再获得CPU,便可立即执行。如果系统中有许多处于就绪状态的进程,通常将它们按照一定的策略排成一个队列,该队列称为就绪队列。有执行资格,没有执行权的进程。
运行状态:这里指进程已经获取CPU,其进程处于正在执行的状态。对任何一个时刻而言,在单处理机的系统中,只有一个进程处于执行状态而在多处理机系统中,有多个进程处于执行状态。既有执行资格,又有执行权的进程。
阻塞状态:这里是指正在执行的进程由于发生某事件(如I/O请求、申请缓冲区失败等)暂时无法继续执行的状态,即进程执行受到阻塞。此时引起进程调度,操作系统把处理机分配给另外一个就绪的进程,而让受阻的进程处于暂停的状态,一般将这个暂停状态称为阻塞状态
终止状态:进程的终止也要通过两个步骤:首先,是等待操作系统进行善后处理,最后将其PCB清零,并将PCB空间返还给系统。当一个进程到达了自然结束点,或是出现了无法克服的错误,或是被操作系统所终结,或是被其他有终止权的进程所终结,它将进入终止状态。进入终止态的进程以后不能在再执行,但是操作系统中任然保留了一个记录,其中保存状态码和一些计时统计数据,供其他进程进行收集。一旦其他进程完成了对其信息的提取之后,操作系统将删除其进程,即将其PCB清零,并将该空白的PCB返回给系统。
线程共享的和独有的内容
线程独有的内容:
线程上下文 包括:线程ID 、栈、栈指针、PC(程序计数器)、通用目的寄存器、条件码、 错误返回码、 线程的信号屏蔽码、 线程的优先级
线程共享的内容:
线程共享的环境包括:进程代码段、进程的公有数据、进程打开的文件描述符、信号的处理器、进程的当前目录、进程用户 ID 与进程组 ID 等
线程同步的方式
线程间的通信目的主要是用于线程同步,所以线程没有像进程通信中的用于数据交换的通信机制。
锁机制:包括互斥锁、条件变量、读写锁
(1)临界区:当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止。
(2) 互斥量(Mutex):提供了以排他方式防止数据结构被并发修改的方法, 互斥对象和临界区对象非常相似,只是其允许在进程间使用,也可在线程间使用,而临界区只限制与同一进程的各个线程之间使用。
(3)条件变量:以原子的方式阻塞进程,直到某个特定条件为真为止,一个线程被挂起,直到某件事件发生。
条件变量始终与互斥锁一起使用。
(4)信号量(semaphore):当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。CSemaphore类对象保存了对当前访问某一个指定资源的线程的计数值,该计数值是当前还可以使用该资源的线程数目。如果这个计数达到了零,则所有对这个CSemaphore类对象所控制的资源的访问尝试都被放入到一个队列中等待,直到超时或计数值不为零为止。 mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
(5)信号:类似进程间的信号处理
(6)事件:允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。
(7)套接字:可用于两个机器之间
进程间通讯方式的区别
共享内存和消息队列,FIFO,管道传递消息的区别:
后者,消息队列,FIFO,管道的消息传递方式一般为
1:服务器得到输入
2:通过管道,消息队列写入数据,通常需要从进程拷贝到内核。
3:客户从内核拷贝到进程
4:然后再从进程中拷贝到输出文件
上述过程通常要经过4次拷贝,才能完成文件的传递。
而共享内存只需要
1:从输入文件到共享内存区
2:从共享内存区输出到文件
上述过程不涉及到内核的拷贝,所以花的时间较少。
多线程编程(线程池),如何确定线程的个数
首先确定应用是CPU密集型 (例如分词,加密等),还是耗时io( 网络,文件操作等)
CPU密集型:最佳线程数等于cpu核心数或稍微小于cpu核心数 Cpu的核数 = 线程数就行,一般我们会设置 Cpu核数+1 防止由于其他因素导致线程阻塞等。
耗时io型:最佳线程数一般会大于cpu核心数很多倍。。一般是io设备延时除以cpu处理延时,得到一个倍数,我的经验数值是20–50倍*cpu核心数。
多核Cpu 最佳线程数 =CPU 核数 * [ 1 +(I/O 耗时 / Cpu 耗时)
最佳线程数量也与机器配置(内存,磁盘速度)有关,如果cpu,内存,磁盘任何一个达到顶点,就需要适当减少线程数。
默认情况下,一个线程的栈要预留1M的内存空间,而一个进程中可用的内存空间只有2G,所以理论上一个进程中最多可以开2048个线程,但是内存当然不可能完全拿来作线程的栈,所以实际数目要比这个值要小。
互斥锁 条件变量 信号量的区别
- 信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
- 使用互斥锁来实现线程间数据的共享和通信,互斥锁一个明显的缺点是它只有两种状态:锁定和非锁定。而条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补了互斥锁的不足,它常和互斥锁一起使用。使用时,条件变量被用来阻塞一个线程,当条件不满足时,线程往往解开相应的互斥锁并等待条件发生变化。一旦其它的某个线程改变了条件变量,它将通知相应的条件变量唤醒一个或多个正被此条件变量阻塞的线程。这些线程将重新锁定互斥锁并重新测试条件是否满足。一般说来,条件变量被用来进行线承间的同步
作用域
信号量: 进程间或线程间(linux仅线程间)
互斥锁: 线程间
上锁时
信号量: 只要信号量的value大于0,其他线程就可以sem_wait成功,成功后信号量的value减一。若value值不大于0,则sem_wait阻塞,直到sem_post释放后value值加一
互斥锁: 只要被锁住,其他任何线程都不可以访问被保护的资源,成功后否则就阻塞
以下是信号灯(量)的一些概念:
信号灯与互斥锁和条件变量的主要不同在于”灯”的概念,灯亮则意味着资源可用,灯灭则意味着不可用。如果说后两中同步方式侧重于”等待”操作,即资源不可用的话,信号灯机制则侧重于点灯,即告知资源可用;没有等待线程的解锁或激发条件都是没有意义的,而没有等待灯亮的线程的点灯操作则有效,且能保持灯亮状态。当然,这样的操作原语也意味着更多的开销。
一个进程(Process)最多可以生成多少个线程(Thread)
默认情况下,一个线程的栈要预留1M的内存空间,而一个进程中可用的内存空间只有2G,所以理论上一个进程中最多可以开2048个线程,但是内存当然不可能完全拿来作线程的栈,所以实际数目要比这个值要小。
进程的并发与并行
并发:在单核 CPU 系统中,系统调度在某一时刻只能让一个进程运行,虽然这种调度机制有多种形式(大多数是时间片轮巡为主),但无论如何,要通过不断切换需要运行的进程让其运行的方式叫并发
并行:在多核 CPU 系统中,可以让两个以上的进程同时运行在不同的物理核心上,这种运行的方式就是并行
区别
并发在微观上不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行,因为 CPU 计算速度很快,从宏观上看,好像这些进程都 在同一个时间点执行
并行是真正的细粒度上的同时进行:既同一时间点上同时运行着多个进程







