Py修行路 python基础 (二十五)线程与进程
操作系统是用户和硬件沟通的桥梁
操作系统,位于底层硬件与应用软件之间的一层
工作方式:向下管理硬件,向上提供接口
操作系统进行切换操作: 把CPU的使用权切换给不同的进程。
1、出现IO操作
2、固定时间
切换过程中就涉及到状态的保存,状态的恢复,资源利用等问题。 线程和进程在多语言之间通用。
二、进程和线程的概念(面试会用到)
1、进程定义:
进程就是一个程序在一个数据集上的一次动态执行过程。进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
2、线程定义:
线程是在进程中执行操作的单元,是进程中的一个实体,作为系统调度和分派的基本单位。线程的出现是为了降低上下文切换的消耗,提高系统的并发性,并突破一个进程只能干一样事的缺陷,使到进程内并发成为可能。
线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。线程是没有自己的系统资源,而是调用所在进程提供的系统资源。
三、进程与线程的关系
1)进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。或者说进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
进程的特征:
1.动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的。
2.并发性:任何进程都可以同其他进程一起并发执行。
3.独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
4.异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
2)线程则是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
线程的性质:
1.线程是进程内的一个相对独立的可执行的单元。若把进程称为任务的话,那么线程则是应用中的一个子任务的执行。
2.由于线程是被调度的基本单元,而进程不是调度单元。所以,每个进程在创建时,至少需要同时为该进程创建一个线程。即进程中至少要有一个或一个以上的线程,否则该进程无法被调度执行。
3.进程是被分给并拥有资源的基本单元。同一进程内的多个线程共享该进程的资源,但线程并不拥有资源,只是使用他们。
4.线程是操作系统中基本调度单元,因此线程中应包含有调度所需要的必要信息,且在生命周期中有状态的变化。
5.由于共享资源【包括数据和文件】,所以线程间需要通信和同步机制,且需要时线程可以创建其他线程,但线程间不存在父子关系。
3)进程和线程的关系:
(1)一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
(2)资源分配给进程,同一进程的所有线程共享该进程的所有资源。
(3)CPU分给线程,即真正在CPU上运行的是线程。
(4)每创建一个进程,就会相应的开辟一块固定的空间,在这个空间中存放程序,数据集,程序控制块,供线程进行调用!
进程:是资源管理单位(容器)!由程序,数据集,进程控制块组成。每个进程之间一定是相互独立的!
线程:是最小执行单位(被CPU执行的) 同一时刻,单核CPU中只能有一个线程在执行!
四、并行和并发
并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。
并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。所以说,并行是并发的子集
并发:多个线程被CPU依次执行
并行:多个CPU同时执行多个线程
在Cpython解释器中,由于有GIL锁,在一个进程中的多线程,每次只能竞争出一个线程由CPU执行!所以每个进程中的多线程是并发执行的,而不同的进程之间是并行执行的!(默认CPU为多核CPU)
五、同步和异步
同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去。
同步例子:打电话!给某人拨电话,你只有在对方接听了的情况下才会通话。在接听之前,你一直处于等待状态!
异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
异步例子:发短息就是异步通信。不管对方在什么状态,你把短信编辑好就可以直接发送过去。同时对方回复消息也是一个道理!
python有个特点,多进程下同一时刻只有一个线程在CPU执行!
python的多线程:由于GIL,导致同一时刻,同一进程只能有一个线程在CPU中被执行!
六、threading 模块
1、线程对象类的创建
我们可以把当前文件执行看作一个主线程,然后在该文件下利用thread方法,实例出了两个子线程,同时在一个进程中执行!
1)Thread类直接创建
import threading import time def tingge(): print("听歌") time.sleep(3) print("听歌结束") def xieboke(): print("写博客") time.sleep(5) print("写博客结束") t1 = threading.Thread(target=tingge) #用类 创建线程 t2 = threading.Thread(target=xieboke) #创建线程 #线程之间没有先后顺序,是竞争关系,谁快谁先执行 t1.start() #执行线程 t2.start() #执行线程 print("ending!")
2)Thread类继承方式创建
import threading import time class MyThread(threading.Thread): #继承类 def __init__(self,num): threading.Thread.__init__(self), self.num = num def run(self): #重写run方法 print("running on number:%s"%self.num) time.sleep(3) print(time.time()-s) s = time.time() t1 = MyThread(1) #实例化一个线程 t2 = MyThread(2) # s = time.time() print("start!") t1.start() #执行线程 time.sleep(2) print("--------------") t2.start() print("ending!")
2、Thread类的实例方法
1)join方法 当前子线程不执行结束,不执行主线程的操作
t.join():线程对象t未执行完,会阻塞你的主线程。
2)t.setDaemon(True) 开启守护线程
守护线程:将子线程的执行与主线程的执行绑定在一起,主线程结束执行完毕,子线程也会随着主线程结束;主线程不结束,子线程会一直在执行!
# join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。 # setDaemon(True): ''' 将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起。 当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成 想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是只要主线程 完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦'''
import threading from time import ctime,sleep import time def Music(name): print ("Begin listening to {name}. {time}".format(name=name,time=ctime())) sleep(3) print("end listening {time}".format(time=ctime())) def Blog(title): print ("Begin recording the {title}. {time}".format(title=title,time=ctime())) sleep(5) print('end recording {time}'.format(time=ctime())) threads = [] t1 = threading.Thread(target=Music,args=('FILL ME',)) t2 = threading.Thread(target=Blog,args=('python',)) threads.append(t1) threads.append(t2) if __name__ == '__main__': #守护线程必须在start之前就设置,分析子线程设置了守护线程,整个程序的执行流程! # t1.setDaemon(True) #守护线程 # t2.setDaemon(True) for t in threads: t.start() #分析子线程添加了join方法,整个程序的执行流程! # for t in threads: # t.join() #与上边for循环的执行结果一致 # t1.start() # t1.join() # t2.start() # t2.join() print ("all over %s" %ctime())
其他方法:
Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
七、GIL(全局解释器锁):
1、只加载在 cpython 解释器的锁,阻止的是多线程并行!
GIL:在一个线程拥有了解释器的访问权之后,其他的所有线程都必须等待它释放解释器的访问权,即使这些线程的下一条指令并不会互相影响。
在调用任何Python C API之前,要先获得GIL
GIL缺点:多处理器退化为单处理器;优点:避免大量的加锁解锁操作
2、GIL的影响:
无论你启多少个线程,你有多少个cpu, Python在执行一个进程的时候会淡定的在同一时刻只允许一个线程运行。所以,python是无法利用多核CPU实现多线程的。
这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
计算密集型:一直在使用CPU
IO:存在大量IO操作
import time def cal(n): #定义一个自加函数 sum=0 for i in range(n): sum+=i s=time.time() #两种执行方式,函数自己调用和利用线程,比较执行时间,查看效率 # cal(50000000) # cal(50000000) import threading t1=threading.Thread(target=cal,args=(50000000,)) t2=threading.Thread(target=cal,args=(50000000,)) t1.start() t2.start() t1.join() t2.join() print("time",time.time()-s)
总结:
对于计算密集型的任务,python的多线程并没有用,但是对于IO密集型的任务:python的多线程是有意义的
python使用多核:开进程,弊端:开销大而且切换复杂
着重点:协程 + 多进程(使用简单,效率高)
方向:IO多路复用
终极思路:换C模块实现多线程
应用:爬虫
八、同步锁(互斥锁)(Lock)、死锁和递归锁(Rlock)
给线程加锁的原因
1)锁通常被用来实现对共享资源的同步访问。为避免多线程之间竞争产生的无序操作,为每一个共享资源创建一个Lock对象,相当于是对每个线程加上一把锁,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁。
注意:同步锁(互斥锁)每次只能为一个对象(子线程)加锁,等待资源执行完成之后,才会释放锁!而从进程竞争出来的其他线程都必须等待锁释放之后才能去获取这个同步锁再执行资源代码。
#同步锁模版 import threading R=threading.Lock() R.acquire() ''' 对公共数据的操作 ''' R.release()
import time import threading lock = threading.Lock() #创建同步锁 def SubNum(): global num # print("ok") lock.acquire() #获取加锁的对象 temp=num #对公共数据进行操作 time.sleep(0.0001) #CPU执行的时间很短很短,加上时间让所有的线程都从进程GIL锁中竞争出来等待加锁, # 这样就人为的构成了一个有序的执行队列! 会明显发现不管时间是多少,代码执行完毕之后结果都是0 num = temp - 1 #对公共变量进行-1操作 lock.release() #释放锁 num = 100 #设定一个共享变量 thread_list = [] for i in range(100): t = threading.Thread(target=SubNum) t.start() #开启100个线程 thread_list.append(t) for t in thread_list: #等待所有线程执行完毕 t.join() #主线程阻塞,所有子线程执行完毕,才会执行主线程 print("Num is:",num)
补充:
1、为什么有了GIL,还需要线程同步?
多线程环境下必须存在资源的竞争,那么如何才能保证同一时刻只有一个线程对共享资源进行存取? 加锁, 对, 加锁可以保证存取操作的唯一性, 从而保证同一时刻只有一个线程对共享数据存取.
2、GIL为什么限定在一个进程上?
你写一个py程序,运行起来本身就是一个进程,这个进程是有解释器来翻译的,所以GIL限定在当前进程;如果又创建了一个子进程,那么两个进程是完全独立的,这个字进程也是有python解释器来运行的,所以这个子进程上也是受GIL影响的
同步锁虽然可以实现进程在用户级别上的可序,但是也会造成死锁的现象!
2)所谓死锁: 是指两个或两个以上的进程或线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
import threading import time #生成了两个同步锁 mutexA = threading.Lock() mutexB = threading.Lock() class MyThread(threading.Thread): #通过继承线程类的方法,创建子线程 def __init__(self): threading.Thread.__init__(self) def run(self): #定义run方法 self.fun1() #线程执行fun1函数 self.fun2() # 线程执行fun1函数 def fun1(self): mutexA.acquire() #1、执行这个函数,就给线程加锁-A ,执行资源 print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) mutexB.acquire() #2、然后再给线程加锁-B,执行资源 print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) #此时就相当于给线程上了两把锁,内层是A,外层是B mutexB.release() #释放B锁,等待其他线程获取 mutexA.release() #释放A锁,等待其他线程获取 def fun2(self): mutexB.acquire() #1、执行这个函数,就给线程加锁-B ,执行资源 print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) # time.sleep(0.2) # 如果锁被占用,则阻塞在这里,等待锁的释放 mutexA.acquire() #2、然后再给线程加锁-A,执行资源 print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) mutexA.release() #释放A锁,等待其他线程获取 mutexB.release() #释放B锁,等待其他线程获取 if __name__ == "__main__": print("start---------------------------%s"%time.time()) for i in range(0, 10): #创建10个线程 my_thread = MyThread() #实例化一个线程 my_thread.start() #开启线程 #执行结果: start---------------------------1494504309.0541146 I am Thread-1 , get res: ResA---1494504309.0541146 I am Thread-1 , get res: ResB---1494504309.0551147 I am Thread-1 , get res: ResB---1494504309.0551147 I am Thread-1 , get res: ResA---1494504309.0551147 I am Thread-2 , get res: ResA---1494504309.0551147 I am Thread-2 , get res: ResB---1494504309.0551147 I am Thread-2 , get res: ResB---1494504309.0551147 I am Thread-3 , get res: ResA---1494504309.0551147 #分析造成这个执行结果的前因后果: #线程与线程之间从等到到获取锁之间是有时间的,虽然很短很短,对于CPU执行而言却是很长,所以线程1执行到这个函数的时候,又把两把锁获取了! #但是cpu对线程的执行是并发的,固定时间下就会去遍历执行一下线程,如果正好敢上一个正在执行的线程把A,B锁释放,正好要执行函数fun2时, # CPU此时触发了另一个待执行的线程,然后触发执行函数fun1,那么A锁就会被这个线程获取执行资源,会碰到加B锁操作,而之前的线程获取B锁执行完资源后, #会碰到加A锁操作!此时明确一点:同步锁只有在没有被占用的情况下才会被获取,而且只能被调用一次!若是占用就会等待被释放才会再去获取。 #而此时这两个线程都是在做加锁操作,A,B锁都在被不同的线程使用而没有释放,导致线程卡死而无法往下执行,此时就造成了死锁状态!
3)在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock(及递归锁)。这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require(加锁状态,每加一次锁计数器就+1,每释放一次计数器就-1)。直到一个线程所有的acquire都被release(即计数器此时的计数为0),其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁:
# import threading # import time # # #生成了两个同步锁 # mutexA = threading.Lock() # mutexB = threading.Lock() # # class MyThread(threading.Thread): #通过继承线程类的方法,创建子线程 # # def __init__(self): # threading.Thread.__init__(self) # # def run(self): #定义run方法 # self.fun1() #线程执行fun1函数 # self.fun2() # 线程执行fun1函数 # # def fun1(self): # # mutexA.acquire() #1、执行这个函数,就给线程加锁-A ,执行资源 # print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) # mutexB.acquire() #2、然后再给线程加锁-B,执行资源 # print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) # #此时就相当于给线程上了两把锁,内层是A,外层是B # mutexB.release() #释放B锁,等待其他线程获取 # mutexA.release() #释放A锁,等待其他线程获取 # # def fun2(self): # # mutexB.acquire() #1、执行这个函数,就给线程加锁-B ,执行资源 # print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) # # time.sleep(0.2) # 如果锁被占用,则阻塞在这里,等待锁的释放 # mutexA.acquire() #2、然后再给线程加锁-A,执行资源 # print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) # mutexA.release() #释放A锁,等待其他线程获取 # mutexB.release() #释放B锁,等待其他线程获取 # # if __name__ == "__main__": # print("start---------------------------%s"%time.time()) # for i in range(0, 10): #创建10个线程 # my_thread = MyThread() #实例化一个线程 # my_thread.start() #开启线程 #执行结果: # start---------------------------1494504309.0541146 # I am Thread-1 , get res: ResA---1494504309.0541146 # I am Thread-1 , get res: ResB---1494504309.0551147 # I am Thread-1 , get res: ResB---1494504309.0551147 # I am Thread-1 , get res: ResA---1494504309.0551147 # I am Thread-2 , get res: ResA---1494504309.0551147 # I am Thread-2 , get res: ResB---1494504309.0551147 # I am Thread-2 , get res: ResB---1494504309.0551147 # I am Thread-3 , get res: ResA---1494504309.0551147 #分析造成这个执行结果的前因后果: #线程与线程之间从等到到获取锁之间是有时间的,虽然很短很短,对于CPU执行而言却是很长,所以线程1执行到这个函数的时候,又把两把锁获取了! #但是cpu对线程的执行是并发的,固定时间下就会去遍历执行一下线程,如果正好敢上一个正在执行的线程把A,B锁释放,正好要执行函数fun2时, # CPU此时触发了另一个待执行的线程,然后触发执行函数fun1,那么A锁就会被这个线程获取执行资源,会碰到加B锁操作,而之前的线程获取B锁执行完资源后, #会碰到加A锁操作!此时明确一点:同步锁只有在没有被占用的情况下才会被获取,而且只能被调用一次!若是占用就会等待被释放才会再去获取。 #而此时这两个线程都是在做加锁操作,A,B锁都在被不同的线程使用而没有释放,导致线程卡死而无法往下执行,此时就造成了死锁状态! import threading import time #生成可重入锁 clock = threading.RLock() # mutexB = threading.Lock() class MyThread(threading.Thread): #通过继承线程类的方法,创建子线程 def __init__(self): threading.Thread.__init__(self) def run(self): #定义run方法 self.fun1() #线程执行fun1函数 self.fun2() # 线程执行fun1函数 def fun1(self): #锁计数器 此时 count = 0 clock.acquire() #1、执行这个函数,就给线程加锁-A ,执行资源 count+1 = 1 print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) clock.acquire() #2、然后再给线程加锁-B,执行资源 count+1 = 2 print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) #此时就相当于给线程上了两把锁,内层是A,外层是B clock.release() #释放B锁,等待其他线程获取 count-1 = 1 clock.release() #释放A锁,等待其他线程获取 count-1 = 0 def fun2(self): #锁计数器 此时 count=0 重归于0 clock.acquire() #1、执行这个函数,就给线程加锁-B ,执行资源 count+1 = 1 print ("I am %s , get res: %s---%s" %(self.name, "ResB",time.time())) # time.sleep(0.2) # 如果锁被占用,则阻塞在这里,等待锁的释放 count+1 = 2 clock.acquire() #2、然后再给线程加锁-A,执行资源 print ("I am %s , get res: %s---%s" %(self.name, "ResA",time.time())) clock.release() #释放A锁,等待其他线程获取 count-1 = 1 clock.release() #释放B锁,等待其他线程获取 count-1 = 0 if __name__ == "__main__": print("start---------------------------%s"%time.time()) for i in range(0, 3): #创建10个线程 my_thread = MyThread() #实例化一个线程 my_thread.start() #开启线程 #执行结果: start---------------------------1494513326.8619041 I am Thread-1 , get res: ResA---1494513326.8629043 I am Thread-1 , get res: ResB---1494513326.8629043 I am Thread-1 , get res: ResB---1494513326.8809052 I am Thread-1 , get res: ResA---1494513326.8809052 I am Thread-2 , get res: ResA---1494513326.980911 I am Thread-2 , get res: ResB---1494513326.981911 I am Thread-2 , get res: ResB---1494513326.981911 I am Thread-2 , get res: ResA---1494513326.981911 I am Thread-3 , get res: ResA---1494513326.9849114 I am Thread-3 , get res: ResB---1494513326.9849114 I am Thread-3 , get res: ResB---1494513326.9849114 I am Thread-3 , get res: ResA---1494513326.9849114
总结如下:
同步锁Lock 不能acquire 和 release 多次!
可重入锁Rlock(内部比同步锁多了个计数器的功能) 可以acquire 和 release 多次!
同步锁或是可重入锁,都是 人为的 对一个进程中的所有线程 又套了一层执行标签,由于加锁的问题,一个进程内的多线程在用户态经过GIL锁全部竞争到内核态,然后等待被CPU调用执行,由于有锁的原因,多线程之间必须等待锁的释放才能被加锁执行(相当于打上了一个执行标签,有才会执行,执行结束之后,这个标签才会释放!没有该标签就没有执行权!)!
九、Event对象 注意:event是线程模块threading的一个方法!方法!方法!
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。
event.isSet():返回event的状态值; event.wait():如果 event.isSet()==False将阻塞线程; event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.clear():恢复event的状态值为False。
threading.Event的wait方法还接受一个超时参数,默认情况下如果事件一致没有发生,wait方法会一直阻塞下去,而加入这个超时参数之后,如果阻塞时间超过这个参数设定的值之后,wait方法会返回。
可以考虑一种应用场景(仅仅作为说明),例如,我们有多个线程从Redis队列中读取数据来处理,这些线程都要尝试去连接Redis的服务,一般情况下,如果Redis连接不成功,在各个线程的代码中,都会去尝试重新连接。如果我们想要在启动时确保Redis服务正常,才让那些工作线程去连接Redis服务器,那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作:主线程中会去尝试连接Redis服务,如果正常的话,触发事件,各工作线程会尝试连接Redis服务。
#event就是一个对象,作用就是操作标志位的方法! import threading import time import logging logging.basicConfig(level=logging.DEBUG, format='(%(threadName)-10s) %(message)s',) #调用日志模块 def worker(event): #线程t1,t2调用这个方法,event = t1/t2 logging.debug('Waiting for redis ready...') while not event.isSet(): #获取当前线程的状态 logging.debug("wait.......") #传入的内容 event.wait(1) # if flag=False就阻塞,等待flag=true继续执行,函数中的参数 数字 表示等待的时间(单位:秒) #时间到了,但是flag未改变,event.isSet()获取的还是False ,循环会再执行一次!直至等到flag = True logging.debug('redis ready, and connect to redis server and do some work [%s]', time.ctime()) #谁先执行还是看线程t1,t2的竞争机制(该函数在线程中执行!),看谁先被CPU调用 time.sleep(1) def main(): readis_ready = threading.Event() #调用threading下的Event方法 初始状态flag=False t1 = threading.Thread(target=worker, args=(readis_ready,), name='t1') #创建一个线程,执行worker函数,参数:readis_ready,命名:t1 t1.start() #启动线程 t2 = threading.Thread(target=worker, args=(readis_ready,), name='t2') # 创建一个线程,执行worker函数,参数:readis_ready,命名:t2 t2.start() #启动线程 logging.debug('first of all, check redis server, make sure it is OK, and then trigger the redis ready event') time.sleep(3) # simulate the check progress readis_ready.set() # flag=Ture if __name__=="__main__": main() #执行主函数
十、Semaphore (信号量)
1、定义: 信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施, 它负责协调各个线程, 以保证它们能够正确、合理的使用公共资源。
2、 什么是信号量(Semaphore0
Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。
以一个停车场是运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这是如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait等待)操作时,它要么通过然后将信号量减一,要么一自等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是应为加操作实际上是释放了由信号量守护的资源。
Semaphore管理一个内置的计数器,每当调用acquire()时内置计数器-1;调用release() 时内置计数器+1;计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。
#实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5): import threading import time semaphore = threading.Semaphore(5) def func(): if semaphore.acquire(): print (threading.currentThread().getName() + ' get semaphore') time.sleep(2) semaphore.release() for i in range(20): t1 = threading.Thread(target=func) t1.start()
应用:连接池
十一、multiprocessing模块(多进程模块)
由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。
multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
# Process类调用 from multiprocessing import Process import time def f(name): print('hello', name,time.ctime()) time.sleep(1) if __name__ == '__main__': p_list=[] for i in range(3): p = Process(target=f, args=('alvin:%s'%i,)) p_list.append(p) p.start() for i in p_list: p.join() print('end') # 继承Process类调用 from multiprocessing import Process import time class MyProcess(Process): def __init__(self): super(MyProcess, self).__init__() # self.name = name def run(self): print ('hello', self.name,time.ctime()) time.sleep(1) if __name__ == '__main__': p_list=[] for i in range(3): p = MyProcess() p.start() p_list.append(p) for p in p_list: p.join() print('end')
process类
构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
group: 线程组,目前还没有实现,库引用中提示必须是None;
target: 要执行的方法;
name: 进程名;
args/kwargs: 要传入方法的参数。
实例方法:
is_alive():返回进程是否在运行。
join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():进程准备就绪,等待CPU调度
run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
terminate():不管任务是否完成,立即停止工作进程
属性:
daemon:和线程的setDeamon功能一样
name:进程名字。
pid:进程号。
通过tasklist(Win)或者ps -elf |grep(linux)命令检测每一个进程号(PID)对应的进程名
from multiprocessing import Process import os import time def info(name): print("name:",name) print('parent process:', os.getppid()) print('process id:', os.getpid()) print("------------------") time.sleep(1) def foo(name): info(name) if __name__ == '__main__': info('main process line') p1 = Process(target=info, args=('alvin',)) p2 = Process(target=foo, args=('egon',)) p1.start() p2.start() p1.join() p2.join() print("ending")
每执行一个.py文件,就相当于开启了一个进程,就会执行一个python.exe解释器文件(pid进程号是不一样的!)
每执行一个py文件,都会执行一个主进程(主进程的父进程是IDE),主进程创建多个子进程的话,子进程之间的pid是不同的,但是父进程的pid与主进程的pid相同!(这就间接的验证了多进程间并行执行的概念)程序执行结束进程就会消失。
用多进程解决由于GIL锁对多线程不能实现并行的方法!
用multiprocessing替代Thread multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。
#coding:utf8 from multiprocessing import Process import time def counter(): i = 0 for _ in range(40000000): i = i + 1 return True def main(): l=[] start_time = time.time() for _ in range(2): t=Process(target=counter) t.start() l.append(t) #t.join() for t in l: t.join() end_time = time.time() print("Total time: {}".format(end_time - start_time)) if __name__ == '__main__': main() ''' py2.7: 串行:6.1565990448 s 并行:3.1639978885 s py3.5: 串行:6.556925058364868 s 并发:3.5378448963165283 s '''
但是multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。
十二、协程
1) yield与协程
在IO操作上有很大的优势!只要遇到IO操作,就让他去切换下一个操作,这样减少等待过程中资源的消耗!
特点:自己来切换,而不再是由操作系统来切换。
协程优点:单线程操作
1、由于是单线程,不存在切换
2、不再有任何锁的概念
函数与一个生成器之间的切换,是并发操作!
import time """ 传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。 如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。 """ # 注意到consumer函数是一个generator(生成器): # 任何包含yield关键字的函数都会自动成为生成器(generator)对象 def consumer(): r = '' while True: # 3、consumer通过yield拿到消息,处理,又通过yield把结果传回; # yield指令具有return关键字的作用。然后函数的堆栈会自动冻结(freeze)在这一行。 # 当函数调用者的下一次利用next()或generator.send()或for-in来再次调用该函数时, # 就会从yield代码的下一行开始,继续执行,再返回下一次迭代结果。通过这种方式,迭代器可以实现无限序列和惰性求值。 # next()寻找到yield获得yield的返回值 # send()寻找到yield给yield传一个值,然后再得到yield的返回值 n = yield r #接收到send传来的值,触发yield,函数从yield当前位置开始继续执行(向下)! # 把得到的这个值赋给n,函数往下执行!拿到r = "200 OK",又循环回到yield这个位置!然后yield把重新拿到的r值, #返回给send(),然后yeild函数就运行到在此处停止,又等待下一次的触发! if not n: return print('[CONSUMER] ←← Consuming %s...' % n) time.sleep(1) r = '200 OK' def produce(c): # 1、首先调用c.next()启动生成器 next(c) #此时函数才真正开始运行,yield返回一个值之后,函数运行到此处停止等待,就在此处等待下一次的触发执行! n = 0 while n < 5: n = n + 1 print('[PRODUCER] →→ Producing %s...' % n) # 2、然后,一旦生产了东西,通过c.send(n)切换到consumer执行; cr = c.send(n) #给yield传值!并接收从 yield 返回的值!拿到值之后赋给左边的变量cr! #注意:必须是生成器自己去执行next()或是send()方法! # 4、produce拿到consumer处理的结果,继续生产下一条消息; print('[PRODUCER] Consumer return: %s' % cr) # 5、produce决定不生产了,通过c.close()关闭consumer,整个过程结束。 c.close() if __name__ == '__main__': # 6、整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。 c = consumer() #执行生成器函数,获取一个生成器函数的对象 produce(c) #c以传值的方式,带入商品函数,执行 ''' result: [PRODUCER] →→ Producing 1... [CONSUMER] ←← Consuming 1... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 2... [CONSUMER] ←← Consuming 2... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 3... [CONSUMER] ←← Consuming 3... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 4... [CONSUMER] ←← Consuming 4... [PRODUCER] Consumer return: 200 OK [PRODUCER] →→ Producing 5... [CONSUMER] ←← Consuming 5... [PRODUCER] Consumer return: 200 OK '''
yield 遇到IO 操作 没办法做到监听,然后进行下一个操作,只能是顺序执行!若想完成这种操作,只能用C去操作!
为了实现这种切换的功能,就要利用别人写的模块greenlet和gevent,在监听遇到IO的情况下,可以实现自动或是手动的切换。greenlet 是一个基础框架,在其基础上进行更深层次的开发出的模块。
2)greenlet
greenlet机制的主要思想是:生成器函数或者协程函数中的yield语句挂起函数的执行,直到稍后使用next()或send()操作进行恢复为止。可以使用一个调度器循环在一组生成器函数之间协作多个任务。greentlet是python中实现我们所谓的"Coroutine(协程)"的一个基础库.
from greenlet import greenlet def test1(): print (12) gr2.switch() print (34) gr2.switch() def test2(): print (56) gr1.switch() print (78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch()
3)基于greenlet的框架开发的gevent模块 实现协程
Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成:
import gevent import time def foo(): print("running in foo") gevent.sleep(2) print("switch to foo again") def bar(): print("switch to bar") gevent.sleep(5) print("switch to bar again") start=time.time() gevent.joinall( [gevent.spawn(foo), gevent.spawn(bar)] ) print(time.time()-start)
当然,实际代码里,我们不会用gevent.sleep()去切换协程,而是在执行到IO操作时,gevent自动切换,以爬虫 代码举例 如下:
from gevent import monkey monkey.patch_all() import gevent from urllib import request import time def f(url): print('GET: %s' % url) resp = request.urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) start=time.time() #并发执行 gevent.joinall([ gevent.spawn(f, 'https://itk.org/'), gevent.spawn(f, 'https://www.github.com/'), gevent.spawn(f, 'https://zhihu.com/'), ]) #直接调用串行方式!顺序实行 # f('https://itk.org/') # f('https://www.github.com/') # f('https://zhihu.com/') print(time.time()-start) #两种执行方式运行时间有明显的差别!对小代码量的网页差别不大
扩展:
gevent是一个基于协程(coroutine)的Python网络函数库,通过使用greenlet提供了一个在libev事件循环顶部的高级别并发API。
主要特性有以下几点:
<1> 基于libev的快速事件循环,Linux上面的是epoll机制
<2> 基于greenlet的轻量级执行单元
<3> API复用了Python标准库里的内容
<4> 支持SSL的协作式sockets
<5> 可通过线程池或c-ares实现DNS查询
<6> 通过monkey patching功能来使得第三方模块变成协作式
gevent.spawn()方法spawn一些jobs,然后通过gevent.joinall将jobs加入到微线程执行队列中等待其完成,设置超时为2秒。执行后的结果通过检查gevent.Greenlet.value值来收集。
十三、IO模型
同步(synchronous) IO和异步(asynchronous) IO,阻塞(blocking) IO和非阻塞(non-blocking)IO分别是什么,到底有什么区别?
本文讨论的背景是Linux环境下的network IO。
Stevens在文章中一共比较了五种IO Model:
blocking IO
nonblocking IO
IO multiplexing
signal driven IO
asynchronous IO
由于signal driven IO在实际中并不常用,所以我这只提及剩下的四种IO Model。
再说一下IO发生时涉及的对象和步骤。
对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
记住这两点很重要,因为这些IO Model的区别就是在两个阶段上各有不同的情况。
blocking IO (阻塞IO)
在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
non-blocking IO(非阻塞IO)
linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程是这个样子:
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,用户进程其实是需要不断的主动询问kernel数据好了没有。
注意:
在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,”非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是可以做其他事情的,
也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
socket套接字中的 setblocking方法,默认flag为True(通信属于阻塞状态),将flag改为False,
并写于listen之后,accept之前,起到循环监听的作用!
优点:能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
缺点:任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
IO multiplexing(IO多路复用)
IO multiplexing这个词可能有点陌生,但是如果我说select,epoll,大概就都能明白了。有些地方也称这种IO方式为event driven IO。我们都知道,select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select/epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
#coding:utf-8 import socket import time import select sock=socket.socket() #默认创建以TCP协议通信的套接字对象 sock.bind(("127.0.0.1",8800)) sock.listen(5) sock.setblocking(False) #取消阻塞 inputs=[sock,] #定义一个列表,专门存放套接字对象 print("sock",sock) while True: r,w,e=select.select(inputs,[],[]) # 注意:监听有变化的套接字 inputs=[sock,conn1,conn2,conn3..] #r代表套接字,w表示写入的值,e代表错误 print("r",r) for obj in r: # for循环遍历r,第一次 [sock,] 第二次 #[conn1,] #为实现并发聊天,一个服务端连接多个客户端,select监听有变化的对象 if obj==sock: # 套接字为sock,监听链接,sock只有在被连接的时候才会发生变化! print('wait.....') conn,addr=obj.accept() #接收链接请求,建立链接! print("conn",conn) inputs.append(conn) # inputs=[sock,conn] else: #监听发送接收的数据 有数据conn就会发生变化!进行聊天 data=obj.recv(1024) print(data.decode("utf8")) send_data=input(">>>") obj.send(send_data.encode("utf8"))
#coding:utf-8 import socket sock=socket.socket() sock.connect(("127.0.0.1",8800)) while True: data=input("input>>>") sock.send(data.encode("utf8")) rece_data=sock.recv(1024) print(rece_data.decode("utf8")) sock.close()
select监听fd变化的过程
用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后,就会发送信号给用户进程数据已到;用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除,这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。
上面的示例中,开启三个客户端,分别连续向server端发送一个内容(中间server端不回应)消息队列会顺序存在服务端的内核态中,只有先回复了第一个客户端发送来的消息,下一个客户端发送过来的消息才会进入。
Asynchronous I/O(异步IO)
linux下的asynchronous IO其实用得很少。先看一下它的流程:
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
IO模型比较分析
到目前为止,已经将四个IO Model都介绍完了。现在回过头来回答最初的那几个问题:blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪。
先回答最简单的这个:blocking vs non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。Stevens给出的定义(其实是POSIX的定义)是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
各个IO Model的比较如图所示:
经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
通过检测IO里边是否有数据,来实现并发效果是通过IO多路复用实现的! 数据存放共有4G的寻址空间,1G给内核态,3G给用户态 每台电脑的内存中,都有一块空间给操作系统使用的内核空间,剩余的一部分 是用户空间。数据之间的传输是在内核态之间完成传输的。数据从内核态转成 用户态供程序调用是由操作系统去执行的。接收到的数据先到内核态,在从内 核态转换到用户态! IO模型 1、阻塞IO (特点:全程阻塞,数据从内核态到不了用户态,用户态会一直等 待!不做其他的操作) socket通信 sock.accept() #在发送系统调用。等待数据的到来。数据到了之后,操作系 统瞬间就把数据从内核态拷贝到用户态。 产生阻塞的位置:数据从内核态到用户态这一段,等待数据和拷贝数据的过程 。 请求某一个数据的时候,数据不返回,就会在当前位置卡住 2、非阻塞IO(特点:发送多次系统调用) 过程:用户发送数据,没有数据服务端就会立刻返回一个信息,与其他的 操作不影响。如果有信息就会向下执行。数据之间的传输不会再发生阻塞状态 。 可以人为的加上控制时间,在等待的过程中,系统去用户态执行其他的操作, 时间到了就去获取一次!拿到数据了就执行操作,没拿到就继续执行其他操作 !这种有个弊端:如果数据是在固定的周期时间内到达了内核态,但是由于监 测数据的操作时间还没到,所以系统就不会操作去从内核态拿取这个数据。知 道周期时间结束。这样导致耗时太大。 socket模块,套接字对象下的sock.setblocking(flag)方法,flag =False 解除或是添加flag = True阻塞。若没正常接收,就会报错!可以做异 常处理。 两个阶段:wait for data:非阻塞 copy data:阻塞 优点:wait for data时无阻塞,也就是不用等待 缺点:系统调用发送太多会造成消耗过太,同时数据不是实时接收的! 3、IO多路复用(最大特点:监听多个链接) 除了socket的accept和recv等待和接收的时候会发起系统调用,由用户态 切换到内核态拿取数据,select这个模块也可以。 由select去完成wait for data 的操作完成对套接字的监听,有数据就会 传输然后向下执行,没有的话就会返回再进行监听。程序获取数据直接就可以 在内核态中拿取。 记住select机制,面试会用到! select 监听多个套接字对象,只监听有变化的对象!!! select机制实现并发聊天的好处:可大量节省IO操作的时间 sock对象fd,对于文件描述符(socket套接字对象): 1、是一个非零整数,不会变 2、收发数据的时候,对于接收端而言,监听的数据先到内核空间,然后copy 到用户空间,同时将内核空间的数据清除, sock 执行过程:监听内核:初始为空,监听到数据发生变化立即copy到用户 态,内核态数据清除变为空,继续监听下一次的数据。 既然IO多路复用这么重要,相比阻塞IO的好处:就是能监听多个对象!对于 select,他有两次系统调用的过程(sock,accept),而阻塞IO只有一次 (accept) 特点: 1、全程(wait for data,copy)阻塞 2、能监听多个文件描述符,实现并发! 4、异步IO(全程无阻塞) 了解,实现太复杂! 5、驱动信号 进程之间的切换时间 > 线程之间的切换时间 > 协程之间的切换时间 总结: (同步IO:如果整个过程中,存在阻塞,或是在任意阶段是阻塞的,异步IO:全 程无阻塞) 同步:阻塞IO,非阻塞IO,IO多路复用 异步:异步IO 记住: 阻塞IO:一直阻塞住对应的进程,直至操作完成,在这期间进程不能在做其他事情的情况,叫做阻塞IO 非阻塞IO:在内核态接收IO还在准备数据的阶段,就已经返回。
selectors模块
IO多路复用的实现机制
win: select
linux: select poll epoll
1)select的缺点:
1、效率低,每次调用select都要将所有的fd(文件描述符)拷贝到内核空间导致效率下降。
2、 遍历所有的fd,是否有数据访问(最重要的问题)。
3、最大连接数(1024),超过预值就不再监听
2)poll:
在实现上和select一样,只是更改了最大连接数。最大连接数没有限制!
3)epoll:由三个函数实现
1、第一个函数:创建epoll句柄:将所有的fd((文件描述符)拷贝到内核空间,但是只需拷贝一次
2、回调函数(前端中常会用到!):某一个函数或者某一个动作成功完成后会自然的触发函数。为所有的fd绑定一个回调函数,一旦有数据访问,触发该回调函数,回调函数将fd放到链表中!(处理完直接反馈)
3、第三个函数 判断链表是否为空。注意:也没有连接上线!链接个数与端口也有关系!1G可以创建十万个左右链接。
4)select机制和epoll有什么不同?
对于数据,select是在大量的进行重复的遍历,相当于是不断的去询问!而epoll是给fd绑定一个回调函数,有数据访问就会触发,直接将信息反馈。大大的提高了效率。
举例说明:一场考试,老师监考学生做题,select相当于是老师一直去逐个问学生谁要交卷?有交卷的就把卷子拿回,然后再继续询问剩下的学生!而epoll则是询问一次之后,给每个学生打上标志进行监听,学生说想交卷了,就拿取试卷就可以!学生自主交卷,老师等着收卷,就不再需要循环的监听了。
------selectors代码举例:
import selectors #基于select模块实现的IO多路复用,建议使用 import socket sock = socket.socket() sock.bind(("127.0.0.1",8888)) sock.listen(5) sock.setblocking(False) #解除阻塞 sel = selectors.DefaultSelector() #根据具体平台自动选择最佳IO多路机制,比喻linux,选择epoll def read(conn,mask): try: data = conn.recv(1024) print(data.decode("utf-8")) data2 = input(">>>:") conn.send(data2.encode("utf-8")) except Exception: sel.unregister(conn) #解除绑定关系 def accept(sock,mask): conn,addr = sock.accept() sel.register(conn,selectors.EVENT_READ,read) sel.register(sock,selectors.EVENT_READ,accept) #绑定注册关系 #要监听谁就把谁放入注册函数 while True: print("wating...") events = sel.select() #监听[(key,mask)] 只监听有变化的数据 for key,mask in events: print(key) #建立链接拿到的SelectorKey(fileobj=<socket.socket fd=272, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888)>, fd=272, events=1, data=<function accept at 0x000000000237CA60>) func = key.data #触发当前绑定的注册函数 print(func) #<function accept at 0x000000000237CA60> obj = key.fileobj #当前触发的对象(套接字对象conn) print(obj) #sock or conn 套接字对象<socket.socket fd=272, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8888)> func(obj,mask) #1 accept(sock,mask) # read(conn,mask) #相比select机制的好处! #通过register将监听列表取消了 #把监听过程中的条件判断取消了,直接去判断然后走不同的函数! #多运行几次就能明白什么意思,时刻注意key,key.data,key.fileobj的变化!
import socket sock = socket.socket() sock.connect(("127.0.0.1",8888)) while True: data = input(">>>:") sock.send(data.encode("utf-8")) re_data = sock.recv(1024) print(re_data.decode("utf-8")) sock.close()
十四、队列
队列(queue):是与线程有关系的!是在多线程编程中有用,用于保证信息安全的切换。保证线程的安全性
是一种数据结构,用于存储数据
get与put方法
''' 创建一个“队列”对象 import Queue q = Queue.Queue(maxsize = 10) Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数 maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。 将一个值放入队列中 q.put(10) 调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值; 第二个block为可选参数,默认为 1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0, put方法将引发Full异常。 将一个值从队列中取出 q.get() 调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且 block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。 '''
join与task_done方法
''' join() 阻塞进程,直到所有任务完成,需要配合另一个方法task_done。 def join(self): with self.all_tasks_done: while self.unfinished_tasks: self.all_tasks_done.wait() task_done() 表示某个任务完成。每一条get语句后需要一条task_done。 '''
其他常用方法
''' 此包中的常用方法(q = Queue.Queue()):
q.qsize() 返回队列的大小 q.empty() 如果队列为空,返回True,反之False q.full() 如果队列满了,返回True,反之False q.full 与 maxsize 大小对应 q.get([block[, timeout]]) 获取队列,timeout等待时间 q.get_nowait() 相当q.get(False)非阻塞
q.put(item) 写入队列,timeout等待时间 q.put_nowait(item) 相当q.put(item, False) q.task_done() 在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号 q.join() 实际上意味着等到队列为空,再执行别的操作 '''
其他模式
''' Python Queue模块有三种队列及构造函数: 1、Python Queue模块的FIFO队列先进先出。 class queue.Queue(maxsize) 2、LIFO类似于堆,即先进后出。 class queue.LifoQueue(maxsize) 3、还有一种是优先级队列级别越低越先出来。 class queue.PriorityQueue(maxsize)
import queue #先进后出 q=queue.LifoQueue() q.put(34) q.put(56) q.put(12) #优先级 q=queue.PriorityQueue() q.put([5,100]) q.put([7,200]) q.put([3,"hello"]) q.put([4,{"name":"alex"}]) while 1: data=q.get() print(data) '''
import queue q=queue.Queue(3) # 默认是 先进先出(FIFO) #设置队列内可以有多少个数据!不写默认为无数 q.put(111) q.put("hello") q.put(222) q.put(223,False) ##队列最大值,如果队列满了有两种情况:block = True,阻塞;block= False就返回错误 # print(q.get()) # print(q.get()) # print(q.get()) q.get(False) # queue 优点: 线程是安全的 # join和task_done #队列调用join方法,任务不完成就会一直阻塞,直到任务每完成一次就调用一次task_done方法。直至所有任务完成才会解除阻塞! #这两个是配套使用!有一个另一个必须出现! q=queue.Queue(5) q.put(111) q.put(222) a=q.get() print(a) q.task_done() b=q.get() print(b) q.task_done() q.join() print("ending") # 先进后出模式 q=queue.LifoQueue() # Lifo last in first out q.put(111) q.put(222) q.put(333) print(q.get()) print(q.get()) #按照优先级操作(数越小优先级越高!) q=queue.PriorityQueue() q.put([4,"hello4"]) q.put([1,"hello"]) q.put([2,"hello2"]) print(q.get()) print(q.get())
import queue q = queue.Queue(5) #默认是先进先出(FIFO)可以设置队列最大值 sum = 0 while not q.full(): q.put(sum) #有个block 阻塞标志 默认为True sum += 1 while not q.empty(): print(q.get()) print("ending!")
生产者消费者模型(主要还是针对于消息模型!)
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
这就像,在餐厅,厨师做好菜,不需要直接和客户交流,而是交给前台,而客户去饭菜也不需要不找厨师,直接去前台领取即可,这也是一个结耦的过程。
import time,random import queue,threading q = queue.Queue() def Producer(name): count = 0 while count <10: print("making........") time.sleep(random.randrange(3)) q.put(count) print('Producer %s has produced %s baozi..' %(name, count)) count +=1 #q.task_done() #q.join() print("ok......") def Consumer(name): count = 0 while count <10: time.sleep(random.randrange(4)) if not q.empty(): data = q.get() #q.task_done() #q.join() print(data) print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data)) else: print("-----no baozi anymore----") count +=1 p1 = threading.Thread(target=Producer, args=('A',)) c1 = threading.Thread(target=Consumer, args=('B',)) # c2 = threading.Thread(target=Consumer, args=('C',)) # c3 = threading.Thread(target=Consumer, args=('D',)) p1.start() c1.start() # c2.start() # c3.start()
原文地址:多线程与多进程