一 背景知识
进程是对正在运行程序的一个抽象,进程的概念起源于操作系统,是操作系统最核心的概念,操作系统的其他所有内容都是围绕进程的概念展开的。所以想要真正了解进程,必须事先了解操作系统。
进程是操作系统提供的最古老也是最重要的抽象概念之一。即使可以利用的cpu只有一个(早期的计算机确实如此),也能保证支持(伪)并发的能力。将一个单独的cpu变成多个虚拟的cpu(多道技术:时间多路复用和空间多路复用+硬件上支持隔离),没有进程的抽象,现代计算机将不复存在。
**为什么要有操作系统?
操作系统,位于底层硬件与应用软件之间的一层
工作方式:向下管理硬件 向上提供接口
二 进程
1.什么是进程(进程与程序)
程序仅仅只是一堆代码而已,而进程指的是程序的运行过程。
进程一般有程序、数据集、进程控制块三部分组成。
(1)程序:程序的运行过程就是一个进程。
(2)数据集:是程序在执行过程中所需要使用的资源,硬件资源和软件资源。
(3)进程控制块:用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志
操作系统进程切换有两种情况:
1 出现IO操作,就是某个线程处于等待状态时。
2 固定时间,系统内部规定的。
举例:
想象一位有一手好厨艺的计算机科学家正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、糖、香草汁等。在这个比喻中,做蛋糕的食谱就是程序(即用适当形式描述的算法)计算机科学家就是处理器(cpu),而做蛋糕的各种原料就是输入数据。进程就是厨师阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和。
现在假设计算机科学家的儿子哭着跑了进来,说他的头被一只蜜蜂蛰了。计算机科学家就记录下他照着食谱做到哪儿了(保存进程的当前状态),然后拿出一本急救手册,按照其中的指示处理蛰伤。这里,我们看到处理机从一个进程(做蛋糕)切换到另一个高优先级的进程(实施医疗救治),每个进程拥有各自的程序(食谱和急救手册)。当蜜蜂蛰伤处理完之后,这位计算机科学家又回来继续做蛋糕,从他离开时的那一步继续坐下来。
需要强调的是:同一个程序执行两次,那也是两个进程,比如打开暴风影音,虽然都是同一个软件,但是一个可以播放苍井空,一个可以播放饭岛爱。(这是老司机egon的举例,真乃是醍醐灌顶,让人茅塞顿开呐!)
考虑一个场景:浏览器,网易云音乐以及notepad++ 三个软件只能顺序执行是怎样一种场景呢?另外,假如有两个程序A和B,程序A在执行到一半的过程中,需要读取大量的数据输入(I/O操作),而此时CPU只能静静地等待任务A读取完数据才能继续执行,这样就白白浪费了CPU资源。你是不是已经想到在程序A读取数据的过程中,让程序B去执行,当程序A读取完数据之后,让程序B暂停。聪明,这当然没问题,但这里有一个关键词:切换。
既然是切换,那么这就涉及到了状态的保存,状态的恢复,加上程序A与程序B所需要的系统资源(内存,硬盘,键盘等等)是不一样的。自然而然的就需要有一个东西去记录程序A和程序B分别需要什么资源,怎样去识别程序A和程序B等等(比如读书)。
进程定义:
进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
三 线程
1.什么是线程
在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程
多线程(即多个控制线程)的概念是,在一个进程中存在多个控制线程,控制该进程的地址空间
进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位,例如,北京地铁与上海地铁是不同的进程,而北京地铁里的13号线是一个线程,北京地铁所有的线路共享北京地铁所有的资源,比如所有的乘客可以被所有线路拉。
线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。 假设,一个文本程序,需要接受键盘输入,将内容显示在屏幕上,还需要保存信息到硬盘中。若只有一个进程,势必造成同一时间只能干一样事的尴尬(当保存时,就不能通过键盘输入内容)。若有多个进程,每个进程负责一个任务,进程A负责接收键盘输入的任务,进程B负责将内容显示在屏幕上的任务,进程C负责保存内容到硬盘中的任务。这里进程A,B,C间的协作涉及到了进程通信问题,而且有共同都需要拥有的东西——-文本内容,不停的切换造成性能上的损失。若有一种机制,可以使任务A,B,C共享资源,这样上下文切换所需要保存和恢复的内容就少了,同时又可以减少通信所带来的性能损耗,那就好了。是的,这种机制就是线程。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程没有自己的系统资源。
四 进程与线程的关系
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。或者说进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
线程则是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)CPU分给线程,即真正在CPU上运行的是线程。
**加深理解
进程: 资源管理单位 (容器)
线程: 最小执行单位
提示:Python的多线程: 由于GIL,导致同一时刻,同一进程只能有一个线程被运行。所以如果想开多个线程的话可以通过开多个进程来实现多个线程的运行。
五 并发与并行
无论是并行还是并发,在用户看来都是'同时'运行的,而一个cpu同一时刻只能执行一个任务
并行:同时运行,只有具备多个cpu才能实现并行
并发:是伪并行,即看起来是同时运行,单个cpu+多道技术
所有现代计算机经常会在同一时间做很多件事,一个用户的PC(无论是单cpu还是多cpu),都可以同时运行多个任务(一个任务可以理解为一个进程)。当启动系统时,会秘密启动许多进程:
启动一个进程来杀毒(360软件)
启动一个进程来看电影(暴风影音)
启动一个进程来聊天(腾讯QQ)
所有的这些进程都需被管理,于是一个支持多进程的多道程序系统是至关重要的
多道技术:内存中同时存入多道(多个)程序,cpu从一个进程快速切换到另外一个,使每个进程各自运行几十或几百毫秒,这样,虽然在某一个瞬间,一个cpu只能执行一个任务,但在1秒内,cpu却可以运行多个进程,这就给人产生了并行的错觉,即伪并发,以此来区分多处理器操作系统的真正硬件并行(多个cpu共享同一个物理内存)
六 同步与异步
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
举个例子,打电话时就是同步通信,发短息时就是异步通信
python并发编程之开多线程的方法
七 threading模块
一 、线程对象的创建
第一种方法:Thread类直接创建
import threading #导入threading模块 import time s=time.time() def listen(): #定义某个线程要运行的函数 print("listenning") time.sleep(2) print("listenning over!") print(time.time()-s) def write(): print("writting") time.sleep(5) print("writting over!") print(time.time()-s) t1=threading.Thread(target=listen) #生成一个线程实例 t2=threading.Thread(target=write) #生成第二个实例 print(time.time()-s) t1.start() #启动线程 t2.start() print("我是主线程")#主线程
第二种方法:Thread类继承式创建
import threading import time class Mythread(threading.Thread):#自定义一个类继承threading模块下的Thread类 def __init__(self,num): threading.Thread.__init__(self) self.num=num def run(self):#这个run函数属性是写死的,必须有个run函数! print("now is %s"%(self.num)) time.sleep(3) print(time.time()-s) t1=Mythread(111) t2=Mythread(222) s=time.time() t1.start() t2.start() print("我是主线程") print(time.time()-s)
二、Thread类的实例化方法
join()和setDaemon()
join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。
setDaemon(True):将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起.
当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就兵分两路,分别运行,
那么当主线程完成想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。
但是有时候我们需要的是只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法了。
import threading from time import ctime,sleep import time def Music(name): print ("线程一 {name}. {time}".format(name=name,time=ctime())) sleep(5) print("线程一最后 {time}".format(time=ctime())) def Blog(title): print ("线程二 {title}. {time}".format(title=title,time=ctime())) sleep(3) print('线程二最后 {time}'.format(time=ctime())) threads = [] t1 = threading.Thread(target=Music,args=('',)) t2 = threading.Thread(target=Blog,args=('',)) threads.append(t1) threads.append(t2) if __name__ == '__main__': t1.setDaemon(True) #注意:一定在start之前设置 t1.start() # t1.join() # t2.setDaemon(True) t2.start() # t2.join() #试试将join放到其他位置的结果 print ("主线程 %s" %ctime())
八 GIL(全局解释器锁)
''' 定义: In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.) '''
Python中的线程是操作系统的原生线程,Python虚拟机使用一个全局解释器锁(Global Interpreter Lock)来互斥线程对Python虚拟机的使用。为了支持多线程机制,一个基本的要求就是需要实现不同线程对共享资源访问的互斥,所以引入了GIL。
GIL:在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。
在调用任何Python C API之前,要先获得GIL
GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作
GIL的早期设计
Python支持多线程,而解决多线程之间数据完整性和状态同步的最简单方法自然就是加锁。 于是有了GIL这把超级大锁,而当越来越多的代码库开发者接受了这种设定后,他们开始大量依赖这种特性(即默认python内部对象是thread-safe的,无需在实现时考虑额外的内存锁和同步操作)。慢慢的这种实现方式被发现是蛋疼且低效的。但当大家试图去拆分和去除GIL的时候,发现大量库代码开发者已经重度依赖GIL而非常难以去除了。有多难?做个类比,像MySQL这样的“小项目”为了把Buffer Pool Mutex这把大锁拆分成各个小锁也花了从5.5到5.6再到5.7多个大版为期近5年的时间,并且仍在继续。MySQL这个背后有公司支持且有固定开发团队的产品走的如此艰难,那又更何况Python这样核心开发和代码贡献者高度社区化的团队呢?
GIL的影响
无论你启多少个线程,你有多少个cpu, Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行。
所以,python是无法利用多核CPU实现多线程的。
这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
import time def cal(n): sum=0 for i in range(n): sum+=i s=time.time() #上面的执行过程为串行的方式,就一个线程,CPU没有切换。 import threading t1=threading.Thread(target=cal,args=(50000000,)) t2=threading.Thread(target=cal,args=(50000000,)) t1.start() t2.start() t1.join() t2.join() #而这种开两个线程的并发方式,中间存在大量的CPU切换,耗时反而会加大。 # cal(50000000) # cal(50000000) print("time",time.time()-s)
GIL(全局解释器锁): 加在cpython解释器上; 计算密集型: 一直在使用CPU IO:存在大量IO操作 总结: 对于计算密集型任务:Python的多线程并没有用 对于IO密集型任务:Python的多线程是有意义的 python使用多核:开进程,弊端:开销大而且切换复杂 着重点:协程+多进程 方向:IO多路复用 终极思路:换C模块实现多线程