进程
进程定义(程序段,相关数据段和PCB构成进程实体,又称为进程映像):
- 进程是程序的一次执行
- 进程是一个程序机器数据在处理机上顺序执行时所发生的活动
- 进城是程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
进程的三种主要状态:
- 就绪态->当进程分配到除CPU以外所有必要资源后,只要再获得CPU,便可立即执行,进程这时候的状态称为就绪状态。在一个系统中处于就绪状态的进程可能有多个,通常将它们排成一个队列,称为就绪队列。
- 执行状态->进程已获得CPU,其程序正在执行。在单处理机系统中,只有一个进程处于执行状态,在多处理机系统中,则有多个进程处于执行状态。
- 阻塞状态->正在执行的进程由于发生某件事请而暂时无法继续执行时,便放弃处理机而处于暂停状态,亦即进程的执行受到阻塞,把这种暂停状态称为阻塞状态,有时也称为等待状态状态或封锁状态。典型事件:请求I/O,申请缓冲空间等。
进程是一个实体。每一个进程都有它自己的地址空间,一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)。文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。
特征
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
线程
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。一个标准的线程由线程ID,当前指令指针(PC),寄存器集合和堆栈组成。另外,线程是进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点儿在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
线程是程序中一个单一的顺序控制流程。进程内有一个相对独立的、可调度的执行单元,是系统独立调度和分派CPU的基本单位指令运行时的程序的调度单位。在单个程序中同时运行多个线程完成不同的工作,称为多线程。
特点
在多线程OS中,通常是在一个进程中包括多个线程,每个线程都是作为利用CPU的基本单位,是花费最小开销的实体。线程具有以下属性。
1)轻型实体
线程中的实体基本上不拥有系统资源,只是有一点必不可少的、能保证独立运行的资源。
线程的实体包括程序、数据和TCB。线程是动态概念,它的动态特性由线程控制块TCB(Thread Control Block)描述。TCB包括以下信息:
(1)线程状态。
(2)当线程不运行时,被保存的现场资源。
(3)一组执行堆栈。
(4)存放每个线程的局部变量主存区。
(5)访问同一个进程中的主存和其它资源。
用于指示被执行指令序列的程序计数器、保留局部变量、少数状态参数和返回地址等的一组寄存器和堆栈。
2)独立调度和分派的基本单位。
在多线程OS中,线程是能独立运行的基本单位,因而也是独立调度和分派的基本单位。由于线程很“轻”,故线程的切换非常迅速且开销小(在同一进程中的)。
3)可并发执行。
在一个进程中的多个线程之间,可以并发执行,甚至允许在一个进程中所有线程都能并发执行;同样,不同进程中的线程也能并发执行,充分利用和发挥了处理机与外围设备并行工作的能力。
4)共享进程资源。
在同一进程中的各个线程,都可以共享该进程所拥有的资源,这首先表现在:所有线程都具有相同的地址空间(进程的地址空间),这意味着,线程可以访问该地址空间的每一个虚地址;此外,还可以访问进程所拥有的已打开文件、定时器、信号量机构等。由于同一个进程内的线程共享内存和文件,所以线程之间互相通信不必调用内核。
进程是资源单位,线程是执行单位
协程
协程 ,又称为微线程,它是实现多任务的另一种方式,只不过是比线程更小的执行单元。因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程。
通俗的理解: 在一个线程中的某个函数中,我们可以在任何地方保存当前函数的一些临时变量等信息,然后切换到另外一个函数中执行,注意不是通过调用函数的方式做到的 ,并且切换的次数以及什么时候再切换到原来的函数都由开发者自己确定。
协程与线程的差异:
在实现多任务时, 线程切换从系统层面远不止保存和恢复CPU上下文这么简单。操作系统为了程序运行的高效性,每个线程都有自己缓存Cache等等数据,操作系统还会帮你做这些数据的恢复操作,所以线程的切换非常耗性能。但是协程的切换只是单纯地操作CPU的上下文,所以一秒钟切换个上百万次系统都抗的住。
协程通过在线程中实现调度,避免了陷入内核级别的上下文切换造成的性能损失,进而突破了线程在IO上的性能瓶颈。协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力
进程,线程,协程三者上下文切换比较
进程 | 线程 | 协程 | |
---|---|---|---|
切换者 | 操作系统 | 操作系统 | 用户(编程者/应用程序) |
切换时机 | 根据操作系统自己的切换策略,用户不感知 | 根据操作系统自己的切换策略,用户不感知 | 用户自己的程序决定 |
切换内容 | 页全局目录,内核栈,硬件上下文 | 内核栈,硬件上下文 | 硬件上下文 |
切换内容的保存 | 保存于内核栈中 | 保存于内核栈中 | 保存在用户自己的变量(用户栈或堆) |
切换过程 | 用户态 - 内核态 - 用户态 | 用户态 - 内核态 - 用户态 | 用户态(没用进入内核态) |
切换效率 | 低 | 中 | 高 |
在python中,yield(生成器)可以很容易的实现上述的功能,从一个函数切换到另外一个函数。
Python的协程源于yield指令。yield有两个功能:
- yield item用于产出一个值,反馈给next()的调用方。
- 作出让步,暂停执行生成器,让调用方继续工作,直到需要使用另一个值时再调用next()。
import time
def task_1():
while True:
print("--This is task 1!--before")
yield
print("--This is task 1!--after")
time.sleep(0.5)
def task_2():
while True:
print("--This is task 2!--before")
yield
print("--This is task 2!--after")
time.sleep(0.5)
if __name__ == "__main__":
t1 = task_1() # 生成器对象
t2 = task_2()
# print(t1, t2)
while True:
next(t1) # 1、唤醒生成器t1,执行到yield后,保存上下文,挂起任务;下次再次唤醒之后,从yield继续往下执行
print("\nThe main thread!\n") # 2、继续往下执行
next(t2) # 3、唤醒生成器t2,....
实现协作式多任务,在Python3.5正式引入了 async/await表达式,使得协程正式在语言层面得到支持和优化,大大简化之前的yield写法。
import asyncio
async def compute(x, y):
print("Compute %s + %s ..." % (x, y))
await asyncio.sleep(x + y)
return x + y
async def print_sum(x, y):
result = await compute(x, y)
print("%s + %s = %s" % (x, y, result))
loop = asyncio.get_event_loop()
tasks = [print_sum(1, 2), print_sum(3, 4)]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()
'''
1、event_loop 事件循环:相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件时,就会调用对应的处理方法。
2、coroutine 协程:协程对象,只一个使用async关键字定义的函数,他的调用不会立即执行函数,而是会返回一个协程对象。协程对象需要注册到事件循环中,由事件循环调用。
3、task 任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程的进一步封装,其中包含任务的各种状态。
4、future:代表将来执行或没有执行的任务结果。它与task没有本质的区别。
5、async/await 关键字:python3.5用于定义协程的关键字,async定义一个协程,await用于挂起阻塞的异步调用接口。
6、run_until_complete:根据传递的参数的不同,返回的结果也有所不同
①、run_until_complete()传递的是一个协程对象或task对象,则返回他们finished的返回结果(前提是他们得 有return的结果,否则返回None)
②、run_until_complete(asyncio.wait(多个协程对象或任务)),函数会返回一个元组包括(done, pending),通过访问done里的task对象,获取返回值
③、run_until_complete(asyncio.gather(多个协程对象或任务)),函数会返回一个列表,列表里面包括各个任 务的的返回结果,按顺序排列
'''
协程是对线程的调度,yield类似惰性求值方式可以视为一种流程控制工具,
线程是内核进行抢占式的调度的,这样就确保了每个线程都有执行的机会。
而 coroutine 运行在同一个线程中,由语言的运行时中的 EventLoop(事件循环)来进行调度。
和大多数语言一样,在 Python 中,协程的调度是非抢占式的,也就是说一个协程必须主动让出执行机会,其他协程才有机会运行。
让出执行的关键字就是 await。也就是说一个协程如果阻塞了,持续不让出 CPU,那么整个线程就卡住了,没有任何并发。
Gevent
-----
Python通过yield
提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
Gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中主要用到的模式是Greenlet,它是以C扩展模块形式接入Python的轻量级协程。Greenlet全部运行在主程序操作系统的内部,被协作式调度。
gevent其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成:
from gevent import monkey; monkey.patch_socket()
import gevent
def f(n):
for i in range(n):
print gevent.getcurrent(), i
g1 = gevent.spawn(f, 5)
g2 = gevent.spawn(f, 5)
g3 = gevent.spawn(f, 5)
g1.join()
g2.join()
g3.join()
from gevent import monkey; monkey.patch_all()
import gevent
import urllib2
def f(url):
print('GET: %s' % url)
resp = urllib2.urlopen(url)
data = resp.read()
print('%d bytes received from %s.' % (len(data), url))
gevent.joinall([
gevent.spawn(f, 'https://www.python.org/'),
gevent.spawn(f, 'https://www.yahoo.com/'),
gevent.spawn(f, 'https://github.com/'),
])
协程的同步写法却能拥有异步的性能
这个问题实际上在上面已经得到解答了。虽然协程的使用是通过同步的写法实现的。如果希望得到性能上的提升,实际上还是通过异步回调的支持,只不过这个异步回调是底层模型上完成的。所以上协程上关于同步异步的性能是建立在底层的异步模型上的。
协程占用的空间可以比线程小
协程是用户定义的,所以在设计协程时,协程的最小空间占用可以比线程小很多。在一个服务器很难创建10W个线程,但是可以轻松的创建10W个协程。
协程切换比线程的切换更轻量
先控制一下变量,不考虑阻塞的问题
假设4核的cpu,100个任务并发执行
(1)创建100个线程:100个线程参与cpu调度。
(2)创建10个线程,每个线程创建10个协程:10个线程参与cpu调度,每个协程内的10个协程由协程库自己进行调度。
理论上,进程拥有100个线程时,每个线程的时间片会比拥有10个线程时短,也就意味着在相同时间里,拥有100个线程时的上下文切换次数比拥有10个线程时多。
所以协程并发模型与多线程同步模型相比,在一定条件下会减少线程切换次数,但是增加了协程切换次数,由于协程的切换是由协程库调度的,所以很难说协程切换的代价比省去的线程切换代价小,合理的方式应该是通过测试工具在具体的业务场景得出一个最好的平衡点。
将task函数封装到Greenlet内部线程的gevent.spawn。 初始化的greenlet列表存放在数组threads中,此数组被传给gevent.joinall 函数,后者阻塞当前流程,并执行所有给定的greenlet。执行流程只会在 所有greenlet执行完后才会继续向下走。