第九章 并发编程
并发编程
1.1、操作系统
什么是操作系统:
精简的说的话,操作系统就是一个协调、管理和控制计算机硬件资源和软件资源的控制程序。
1.2 多进程
1.2.1 进程理论
1、什么是进程:正在进行的一个多过程或者说一个任务。而负责执行任务的是cpu
在操作系统中的唯一标识符 :pid
2、进程和程序的区别:程序只是一堆代码,进程指的是程序的执行过程。
- 程序只是一个文件
- 进程是这个文件被CPU运行起来了
3、并行和并发:
一 并发:是伪并行,即看起来是同时运行。单个cpu+多道技术就可以实现并发,切换+保存状态
二 并行:同时运行,只有具备多个cpu才能实现并行
4、进程的状态
tail -f access.log |grep '404' 执行程序tail,开启一个子进程,执行程序grep,开启另外一个子进程,两个进程之间基于管道'|'通讯,将tail的结果作为grep的输入。 进程grep在等待输入(即I/O)时的状态称为阻塞,此时grep命令都无法运行 其实在两种情况下会导致一个进程在逻辑上不能运行, 进程挂起是自身原因,遇到I/O阻塞,便要让出CPU让其他进程去执行,这样保证CPU一直在工作 与进程无关,是操作系统层面,可能会因为一个进程占用时间过多,或者优先级等原因,而调用其他的进程去使用CPU。 因而一个进程由三种状态
5、子进程、父进程
- 在父进程中创建子进程
- 在pycharm里启动的所有py程序都是pycharm的子进程
os模块
- os.getpid() #获取子进程的pid
- os.getppid() #获取父进程的pid
1.2.2 开启进程的两种方式及Process类的介绍
1、multiprocessing模块介绍
Python提供了multiprocessing。 multiprocessing模块用来开启子进程,该模块与多线程模块threading的编程接口类似。multiprocessing模块的功能众多:支持子进程、通信和共享数据、执行不同形式的同步,>提供了Process、Queue、Pipe、Lock等组件。
2、Process类的介绍
创建进程的类:
Process([group [, target [, name [, args [, kwargs]]]]]),由该类实例化得到的对象,可用来开启一个子进程 强调: 1. 需要使用关键字的方式来指定参数 2. args指定的为传给target函数的位置参数,是一个元组形式,必须有逗号
参数介绍:
group参数未使用,值始终为None target表示调用对象,即子进程要执行的任务 args表示调用对象的位置参数元组,args=(1,2,'egon',) kwargs表示调用对象的字典,kwargs={'name':'egon','age':18} name为子进程的名称
方法介绍:
p.start():启动进程,并调用该子进程中的p.run() p.run():进程启动时运行的方法,正是它去调用target指定的函数,我们自定义类的类中一定要实现该方法 p.terminate():强制终止进程p,不会进行任何清理操作,如果p创建了子进程,该子进程就成了僵尸进程,使用该方法需要特别小心这种情况。如果p还保存了一个锁那么也将不会被释放,进而导致死锁 p.is_alive():如果p仍然运行,返回True p.join([timeout]):主线程等待p终止(强调:是主线程处于等的状态,而p是处于运行的状态)。timeout是可选的超时时间。
属性介绍:
p.daemon:默认值为False,如果设为True,代表p为后台运行的守护进程,当p的父进程终止时,p也随之终止,并且设定为True后,p不能创建自己的新进程,必须在p.start()之前设置 p.name:进程的名称 p.pid:进程的pid
3、Process的使用
注意:在windows中Process()必须放到# if name == 'main':下
- 进程开启的过程中windows和 linux / ios之间的区别
- windows 通过(模块导入)再一次执行父进程文件中的代码来获取父进程中的数据
- 所以只要是不希望被子进程执行的代码,就写在if name == 'main'下
- 因为在进行导入的时候父进程文件中的__name__ != 'main'
- linux/ios
- 正常的写就可以,没有if name == 'main'这件事情了
- windows 通过(模块导入)再一次执行父进程文件中的代码来获取父进程中的数据
- 补充:
if __name__ == '__main__': # 控制当这个py文件被当作脚本直接执行的时候,就执行这里面的代码 # 当这个py文件被当作模块导入的时候,就不执行这里面的代码 print('hello hello') __name__ == '__main__' # 执行的文件就是__name__所在的文件 __name__ == '文件名' # __name__所在的文件被导入执行的时候
#创建并开启子进程的方式一 from multiprocessing import Process import time def task(name): print('%s is running' %name) time.sleep(3) print('%s is done' % name) if __name__ == '__main__': p=Process(target=task,args=('子进程',)) p.start() #只是向操作系统发信号,并不执行 print('主进程')
#创建并开启子进程的方式二 from multiprocessing import Process import time class Myprocess(Process): def __init__(self,name): super().__init__() #福永process类的init方法 self.name = name def run(self): #这里必须要写run print('%s is running'%self.name) time.sleep(3) print('%s is done'%self.name) if __name__ == '__main__': p=Myprocess('子进程1') p.start() #这里调用的就是run方法 print('主进程')
进程之间的内存空间是隔离的:
from multiprocessing import Process n=100 #在windows系统中应该把全局变量定义在if __name__ == '__main__'之上就可以了 def work(): global n n=0 print('子进程内: ',n) #0 if __name__ == '__main__': p=Process(target=work) p.start() print('主进程内: ',n)#100
1.2.3 进程模块 - join方法
ps:主进程和子进程的关系:
主进程的结束逻辑:
- 主进程的代码结束
- 所有的子进程结束
- 给子进程回收资源
- 主进程结束
为了回收子进程的资源,父进程会等待着所有的子进程结束之后才结束
# 示例: import os import time from multiprocessing import Process def func(): print('start',os.getpid()) time.sleep(10) print('end',os.getpid()) if __name__ == '__main__': p = Process(target=func) p.start() # 异步 调用开启进程的方法 但是并不等待这个进程真的开启 print('main :',os.getpid()) # 主进程没结束 :等待子进程结束 # 主进程负责回收子进程的资源 # 如果子进程执行结束,父进程没有回收资源,那么这个子进程会变成一个僵尸进程
主进程怎么知道子进程结束了的呢?
- 基于网络、文件
- join方法 :阻塞,直到子进程结束就结束
- 把一个进程的结束事件封装成一个join方法
- 执行join方法的效果就是阻塞,直到这个子进程执行结束就结束阻塞
from multiprocessing import Process import time,os def task(name): print('%s is running' %name) time.sleep(3) print('%s is done' % name) if __name__ == '__main__': p1=Process(target=task,args=('子进程1',)) p2=Process(target=task,args=('子进程2',)) p3=Process(target=task,args=('子进程3',)) p1.start() #只是向操作系统发信号,等待操作系统执行 p2.start() p3.start() p1.join() #让主进程等待 p2.join() p3.join() print('主进程')
1.2.4 守护进程
1、方式:有一个参数可以把进程设置成守护进程。
p.daemon = True
守护进程的意思?
其一:守护进程会在主进程代码执行结束后就终止 (这里是主进程代码结束,就算结束了,主进程代码结束并不意味着主进程结束,还要等待其他子进程执行结束,回收资源)
其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemonic processes are not allowed to have children
应用场景:生产者消费者模型
from multiprocessing import Process import time import random def task(name): print('%s is piaoing' %name) time.sleep(random.randrange(1,3)) print('%s is piao end' %name) if __name__ == '__main__': p=Process(target=task,args=('egon',)) p.daemon=True #一定要在p.start()前设置,设置p为守护进程,禁止p创建子进程,并且父进程代码执行结束,p即终止运行 p.start() print('主') #只要终端打印出这一行内容,那么守护进程p也就跟着结束掉了
####练习题 from multiprocessing import Process import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': p1=Process(target=foo) p2=Process(target=bar) p1.daemon=True p1.start() p2.start() print("main-------") ##只要终端打印出这一行内容,那么守护进程p1也就跟着结束掉了 #main------- #456 #end456
1.2.5 互斥锁 Lock
只能lock.acquire() 一次 lock.release() 释放后,才能再次lock.acquire()
1、如果在一个并发的场景下,涉及到某部分内容需要修改一些所有进程共享数据资源,需要加锁来维护数据安全
互斥锁的意思就是互相排斥,如果把多个进程比喻为多个人,互斥锁的工作原理就是多个人都要去争抢同一个资源:卫生间,一个人抢到卫生间后上一把锁,其他人都要等着,等到这个完成任务后释放锁,其他人才有可能有一个抢到......所以互斥锁的原理,就是把并发改成穿行,降低了效率,但保证了数据安全不错乱
2、在数据安全的基础上,才考虑效率问题
3、存在的意义:数据的安全性
4、方式:
- 在主进程中实例化 lock = Lock() 创建一把锁
- 把这把锁传递给子进程
- 在子进程对 需要加锁的代码 进行with lock:
- with lock 相当于
- lock.acquire() #加锁
- lock.release() #解锁
- with lock 相当于
5、应用场景:(在进程中需要加锁的场景)
- 共享的数据资源(文件、数据库)
- 对资源进行修改、删除操作
6、加锁后能保证数据的安全,但是降低了程序的执行效率,从并行到串行
#模拟抢票 import time import json from multiprocessing import Process,Lock def search(user): with open('ticket_count') as f: #json存储数据{"count": 2} dic = json.load(f) print('%s查询结果 : %s张余票' % (user, dic['count'])) def buy(user,lock): time.sleep(1) # lock.acquire()# 给这段代码加上一把锁 with open('ticket_count') as f: dic = json.load(f) if dic['count'] > 0: print('%s买到票了'%user) dic['count'] -= 1 else: print('%s没买到票' % (user)) time.sleep(0.2) with open('ticket_count','w') as f: json.dump(dic,f) # lock.release() # 给这段代码解锁 def task(user, lock): search(user) with lock: buy(user, lock) # buy(user, lock) if __name__ == '__main__': lock = Lock() for i in range(10): p = Process(target=task,args=('路人%s'%i,lock)) p.start()
互斥锁与join区别:
发现使用join将并发改成穿行,确实能保证数据安全,但问题是连查票操作也变成只能一个一个人去查了,很明显大家查票时应该是并发地去查询而无需考虑数据准确与否,此时join与互斥锁的区别就显而易见了,join是将一个任务整体串行,而互斥锁的好处则是可以将一个任务中的某一段代码串行,比如只让task函数中的get任务串行。
总结:
虽然可以用文件共享数据实现进程间通信,但问题是: 1、效率低(共享数据基于文件,而文件是硬盘上的数据) 2、需要自己加锁处理 因此我们最好找寻一种解决方案能够兼顾: 1、效率高(多个进程共享一块内存的数据) 2、帮我们处理好锁问题。 这就是mutiprocessing模块为我们提供的基于消息的IPC通信机制:队列和管道 队列和管道都是将数据存放于内存中,而队列又是基于(管道+锁)实现的,可以让我们从复杂的锁问题中解脱出来,因而队列才是进程间通信的最佳选择。 我们应该尽量避免使用共享数据,尽可能使用消息传递和队列,避免处理复杂的同步和锁问题,而且在进程数目增多时,往往可以获得更好的可获展性。
1.2.6 队列
进程彼此之间互相隔离,要实现进程间通信(IPC)(inter process communication),multiprocessing模块支持两种形式:队列和管道,这两种方式都是使用消息传递的
- Queue(队列):进程之间数据安全
- 天生就是数据安全的
- 基于文件家族的socket pickle lock
- pipe(管道):进程之间数据不安全
- 不安全的
- 基于文件家族的socket pickle
- 队列 = 管道 + 锁
创建队列的类(底层就是以管道和锁定的方式实现):
Queue([maxsize]):创建共享的进程队列,Queue是多进程安全的队列,可以使用Queue实现多进程之间的数据传递。
参数介绍:
maxsize是队列中允许最大项数,省略则无大小限制。 但需要明确: 1、队列内存放的是消息而非大数据 2、队列占用的是内存空间,因而maxsize即便是无大小限制也受限于内存大小
主要方法介绍:
q.put() #插入数据到队列中 q.get() #可以从队列读取并且删除一个元素。 #先进先出
队列的使用
from multiprocessing import Process,Queue q = Queue(3) q.put('1') q.put('2') q.put('3') print(q.full()) q.put(4) #再放就阻塞住了,卡住了 q.get() q.get() q.get() print(q.empty()) print(q.get()) #再取就阻塞住了,卡主 不会取空
1.2.7 生产者消费者模型
为什么要使用生产者消费者模型
生产者指的是生产数据的任务,消费者指的是处理数据的任务,在并发编程中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者和消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。 这个阻塞队列就是用来给生产者和消费者解耦的
生产者消费者模型实现
from multiprocessing import Process,Queue import time,random,os def producer(q,name,food): #生产者 for i in range(3): time.sleep(random.randint(1,3)) res = '%s%s'%(food,i) q.put(res) print('\033[45m%s 生产了 %s\033[0m' %(name,res)) def consumer(q,name): #消费者 while 1: res = q.get() if res is None:break time.sleep(random.randint(1,3)) print('\033[43m%s 吃 %s\033[0m' %(name,res)) if __name__ == '__main__': q = Queue() p1 = Process(target=producer,args=(q,'zhang','包子')) c1 = Process(target=consumer,args=(q,'alex')) p1.start() c1.start() p1.join() q.put(None) # 生产者这行完 加一个None print('主') 此时的问题是主进程永远不会结束,原因是:生产者p在生产完后就结束了,但是消费者c在取空了q之后,则一直处于死循环中且卡在q.get()这一步。 解决方式无非是让生产者在生产完毕后,往队列中再发一个结束信号,这样消费者在接收到结束信号后就可以break出死循环 但上述解决方式,在有多个生产者和多个消费者时,我们则需要用一个很low的方式去解决,有几个生产者就需要发送几次结束信号:相当low,例如
其实我们的思路无非是发送结束信号而已,有另外一种队列提供了这种机制
JoinableQueue([maxsize])
这就像是一个Queue对象,但队列允许项目的使用者通知生成者项目已经被成功处理。通知进程是使用共享的信号和条件变量来实现的。
参数介绍
maxsize是队列中允许最大项数,省略则无大小限制。
方法介绍
JoinableQueue的实例p除了与Queue对象相同的方法之外还具有: q.task_done():使用者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发ValueError异常 q.join():生产者调用此方法进行阻塞,直到队列中所有的项目均被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
基于JoinableQueue实现生产者消费者模型
from multiprocessing import Process,JoinableQueue import time,random,os def consumer(q,name): while True: res=q.get() time.sleep(random.randint(1,3)) print('\033[43m%s 吃 %s\033[0m' %(name,res)) q.task_done() #发送信号给q.join(),说明已经从队列中取走一个数据并处理完毕了 def producer(q,name,food): for i in range(3): time.sleep(random.randint(1,3)) res='%s%s' %(food,i) q.put(res) print('\033[45m%s 生产了 %s\033[0m' %(name,res)) q.join() #等到消费者把自己放入队列中的所有的数据都取走之后,生产者才结束 if __name__ == '__main__': q=JoinableQueue() #使用JoinableQueue() #生产者们:即厨师们 p1=Process(target=producer,args=(q,'egon1','包子')) p2=Process(target=producer,args=(q,'egon2','骨头')) p3=Process(target=producer,args=(q,'egon3','泔水')) #消费者们:即吃货们 c1=Process(target=consumer,args=(q,'alex1')) c2=Process(target=consumer,args=(q,'alex2')) c1.daemon=True c2.daemon=True #开始 p1.start() p2.start() p3.start() c1.start() c2.start() p1.join() p2.join() p3.join() #1、主进程等生产者p1、p2、p3结束 #2、而p1、p2、p3是在消费者把所有数据都取干净之后才会结束 #3、所以一旦p1、p2、p3结束了,证明消费者也没必要存在了,应该随着主进程一块死掉,因而需要将生产者们设置成守护进程 print('主')
1.3 多线程
1.3.1 线程理论
什么是线程:在传统操作系统中,每个进程有一个地址空间,而且默认就有一个控制线程。在pycharm运行一个py文件,就有一个进程和默认的一个控制线程。
所以,进程只是用来把资源集中到一起(进程只是一个资源单位,或者说资源集合),而线程才是cpu上的执行单位。
1.3.2 开启线程的两种方式
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s',name) if __name__ == '__main__': t=Thread(target=sayhi,args=('zhangyang',)) t.start() print('zhu')
#面向对象 from threading import Thread import time class Sayhi(Thread): def __init__(self,name): super().__init__() self.name = name def run(self): time.sleep(2) print('%s'%self.name) if __name__ == '__main__': p = Sayhi('zhangyang') p.start() print('zhu')
1.3.3 多线程与多进程的区别
一 谁的开启速度快?
#1、在主进程下开启线程 from threading import Thread def work(): print('hello') if __name__ == '__main__': t=Thread(target=work) t.start() print('主线程/主进程') #hello #主线程/主进程 #执行的时候,几乎是t.start()的同时就将线程开启了,然后先打印了hello,说明线程的创建开销极小。相当于开辟一条流水线。 #2、在主进程下开启子进程 from multiprocessing import Process def work(): print('hello') if __name__ == '__main__': #在主进程下开启子进程 p=Process(target=work) p.start() print('主线程/主进程') #p.start()将信号发给操作系统,操作系统要申请空间,好拷贝父进程的地址空间到子进程,开销远大于开启线程。相当于新建一个工厂
二 瞅一瞅 ?
#1、在主进程下开启多个线程,每个线程都跟主进程的pid一样 from threading import Thread import os def work(): print('hello',os.getpid()) if __name__ == '__main__': t1 = Thread(target = work) t2 = Thread(target = work) t1.start() t2.start() print('主线程/主进程pid',os.getpid()) #hello 7939 #hello 7939 #主线程/主进程 7939 #2、开多个进程,每个进程都有不同的pid from threading import Process import os def work(): print('hello',os.getpid()) if __name__ == '__main__': p1 = Process(target = work) p2 = Process(target = work) p1.start() p2.start() print('主线程/主进程pid',os.getpid()) #主线程/主进程 7951 #hello 7952 #hello 7953
三 同一进程内的线程共享该进程的数据?
#1、进程之间地址空间是隔离的 from multiprocessing import Process import os def work(): global n n = 0 if __name__ == '__main__': n = 100 p = Process(target = work) p1.start() p1.join() print('主',n) #主 100 #子进程p已经将自己的全局的n改成了0,但是全局的n依然为100 #2、同一进程内开启的多个线程是共享改进程地址空间 from threading import Thread import os def work(): global n n = 0 if __name__ == '__main__': n = 100 t = Thread(target = work) t1.start() t1.join() print('主',n) #主 0 #因为同一进程内的线程之间共享进程内的数据
1.3.4 Thread对象的其他属性或方法
介绍
from threading import Thread 实例化Thread Thread实例对象的方法 # isAlive(): 返回线程是否活动的。 # getName(): 返回线程名。 # setName(): 设置线程名。 threading模块提供的一些方法: import threading # threading.currentThread(): 返回当前的线程变量。 # threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。 # threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
#验证 from threading import Thread import threading from multiprocessing import Process import os def work(): import time time.sleep(3) print(threading.current_thread().getName()) if __name__ == '__main__': #在主进程下开启线程 t=Thread(target=work) t.start() print(threading.current_thread().getName()) print(threading.current_thread()) #主线程 print(threading.enumerate()) #连同主线程在内有两个运行的线程 print(threading.active_count()) print('主线程/主进程') ''' MainThread <_MainThread(MainThread, started 140735268892672)> [<_MainThread(MainThread, started 140735268892672)>, <Thread(Thread-1, started 123145307557888)>] 主线程/主进程 Thread-1'''
主线程等待子线程结束
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.start() t.join() #join和进程一样,这里也是等待(子线程)执行完毕,主线程才执行 print('主线程') print(t.is_alive()) ''' egon say hello 主线程 False '''
1.3.5 守护线程
无论是进程还是线程,都遵循:守护xx会等待主xx运行完毕后被销毁。伴随着主进程、主线程死而死。
需要强调的是:运行完毕并非终止运行
1、对主进程来说,运行完毕指的是主进程代码运行完毕 2、对主线程来说,运行完毕指的是主线程所在的进程内所有非守护线程统统运行完毕,主线程才算运行完毕
详细解释:
1、主进程在他的代码结束后已经算是运行完毕了(守护进程在此时被回收),然后主进程会一直等待非守护的子进程都运行完毕后回收子进程的资源(否则会产生僵尸进程),才会结束。 ps:进程之间是隔离的,是2个进程 2、主线程在其他非守护线程运行完毕后才算执行完毕(守护线程在此时被回收)。因为主线程的结束意味着进程的结束,进程整体的资源都将被回收,所以进程必须保证所有的非守护线程运行完毕才能结束。
from threading import Thread import time def sayhi(name): time.sleep(2) print('%s say hello' %name) if __name__ == '__main__': t=Thread(target=sayhi,args=('egon',)) t.setDaemon(True) #必须在t.start()之前设置 t.start() print('主线程') print(t.is_alive()) ''' 主线程 True 如果没有非守护线程,那就随着主线程死就死掉了 '''
练习
from threading import Thread import time def foo(): print(123) time.sleep(1) print("end123") def bar(): print(456) time.sleep(3) print("end456") if __name__ == '__main__': t1=Thread(target=foo) t2=Thread(target=bar) t1.daemon=True t1.start() t2.start() print("main-------") ''' 123 456 main------- end123 end456 '''
1.3.6 GIL全局解释锁
GIL介绍
GIL本质就是一把互斥锁,所有互斥锁的本质都一样,都是将并行运行变成串行,从此来控制同一时间内的共享数据只能被一个任务所修改。进而保证数据安全。 保护不同的数据的安全,就要加不同的锁。保护解释器层的用GIL锁,保护代码层的数据用自定义Lock锁。
ps:运行一个test.py文件 在一个python进程中,不仅有tesy.py的主线程或者该主线程的其他线程,还有解释器开启的垃圾回收等解释器级别的线程(定时回收),所有的线程都运行在这一个进程内。 1、所有数据都是共享的,这其中,代码作为一种数据也是被所有线程共享的(test.py的所有代码以及Cpython解释器的所有代码) 例如:test.py定义一个函数work(代码内容如下图),在进程内所有线程都能访问到work的代码,于是我们可以开启三个线程然后target都指向该代码,能访问到意味着就是可以执行。 2、所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。
综上:
多线程先先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去运行。
GIL与Lock
GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock. 多线程先抢GIL锁,线程1拿到执行权限,在执行自己的代码,拿到lock.acquire(),遇到time.sleep(0.5)IO堵塞,被操作系统剥夺执行权限,下一个线程开始抢GIL锁,在执行自己的代码,反复,直到线程1重新抢到GIL
from threading import Thread,Lock import os,time def work(): global n lock.acquire() temp=n time.sleep(0.1) n=temp-1 lock.release() if __name__ == '__main__': lock=Lock() n=100 l=[] for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #结果肯定为0,由原来的并发执行变成串行,牺牲了执行效率保证了数据安全,不加锁则结果可能为99
GIL与多线程
有了GIL的存在,同一时刻同一进程中只有一个线程被执行 听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势. 结论: 1、对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用,只能一个一个来 2、2、当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地
结论:python多线程还是有用的,处理IO密集型任务
现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
补充多线程性能测试
#如果并发的多个任务是计算密集型:多进程效率高 from multiprocessing import Process from threading import Thread import os,time def work(): res=0 for i in range(100000000): res*=i if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(4): p=Process(target=work) #耗时5s多 p=Thread(target=work) #耗时18s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start)) #如果并发的多个任务是I/O密集型:多线程效率高 from multiprocessing import Process from threading import Thread import threading import os,time def work(): time.sleep(2) print('===>') if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为4核 start=time.time() for i in range(400): # p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上 p=Thread(target=work) #耗时2s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
应用:
多线程用于IO密集型,如socket,爬虫,web 多进程用于计算密集型,如金融分析
1.3.7 死锁现象与递归锁
一 死锁现象
死锁是指两个以上的进程或线程,因争夺资源造成的一种互相等待的现象,若么有外力,无法进行下去。此时系统处于死锁状态,这些永远在互相等待的进程称为死锁进程,如下就是死锁。
from threading import Thread,Lock import time mutexA=Lock() #2把锁 mutexB=Lock() class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A锁\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B锁\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B锁\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A锁\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start() ''' Thread-1 拿到A锁 Thread-1 拿到B锁 Thread-1 拿到B锁 Thread-2 拿到A锁 #出现死锁,整个程序阻塞住 ''' # 线程1卡到 mutexA.acquire(),线程2卡到mutexB.acquire()
二 递归锁
解决方法,递归锁,在Python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock。 ps: 这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次。
总结:递归锁可以连续acquire多次,没acquire一次,count+1,只能当count为0时,才能被其他线程抢到。
from threading import Thread,Lock,RLock import time #一把锁 mutexB = mutexA =RLock()#一个线程拿到锁,count+1,碰到acquire,count+1,直到锁释放掉,count=0时,其他进程才能抢到锁 class MyThread(Thread): def run(self): self.func1() self.func2() def func1(self): mutexA.acquire() print('\033[41m%s 拿到A锁\033[0m' %self.name) mutexB.acquire() print('\033[42m%s 拿到B锁\033[0m' %self.name) mutexB.release() mutexA.release() def func2(self): mutexB.acquire() print('\033[43m%s 拿到B锁\033[0m' %self.name) time.sleep(2) mutexA.acquire() print('\033[44m%s 拿到A锁\033[0m' %self.name) mutexA.release() mutexB.release() if __name__ == '__main__': for i in range(10): t=MyThread() t.start()
1.3.8 信号量 Event 定时器
一 信号量
信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小
from threading import Thread,Semaphore import threading import time def func(): sm.acquire() print('%s get sm' %threading.current_thread().getName()) time.sleep(3) sm.release() if __name__ == '__main__': sm=Semaphore(5) for i in range(23): t=Thread(target=func) t.start()
二 Event
线程的一个关键就是每个线程都是独立且状态不可预测的,如果程序中的其他线程需要判断某个线程的状态来决定自己下一步的动作,就要用到threading库中的Event对象。
from threading import Event event.isSet():返回event的状态值; event.wait():如果 event.isSet()==False将阻塞线程; event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.clear():恢复event的状态值为False。
例子:有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常,才能让那些工作线程链接服务器,如果链接不成功,会尝试重新链接,那么我们就可以采用threading.Event机制来协调各工作线程的连接操作
from threading import Event,Thread import threading import time,random def conn_mysql(): count =1 while not event.isSet(): # 为Flase if count >3: raise TimeoutError('连接超时') count +=1 event.wait(0.5) #event 为True,继续执行下面的 print('连接成功mysql') def check_mysql(): print('正在检查mysql') time.sleep(3) event.set() #将false改为True if __name__ == '__main__': event = Event() conn1 = Thread(target=conn_mysql) conn2 = Thread(target=conn_mysql) check = Thread(target=check_mysql) conn1.start() conn2.start() check.start()
三 定时器
定时器,指定n秒后执行某操作
from threading import Timer def hello(): print('hello world') t=Timer(4,hello) #4s后hello函数将执行 t.start()
#验证码 import random from threading import Timer class Code: def __init__(self): self.make_cache() def make_cache(self): self.cache=self.get_code() print(self.cache) self.t = Timer(5,self.make_cache) self.t.start() def get_code(self,num=4): res = '' for i in range(num): str1 = str(random.randint(0,9)) str2 =chr(random.randint(65,90)) # print(random.choice(str1+str2)) res += random.choice([str1,str2]) return res def check(self): while 1: code = input('>>').strip() if code.upper() == self.cache: print('输入正确') self.t.cancel() #结束定时器 break obj = Code() obj.check()
1.3.9 线程queue
三种不同的用法
1、class queue.Queue(maxsize=0) #队列:先进先出**
import queue q = queue.Queue(3) q.put('1') q.put('2') q.put('3') q.put('4',block=True,timeout=3) #block默认为True是阻塞,后抛异常 #q.put('4',block=False) #不阻塞,直接抛异常queue.Full #q.put_nowait('4') 不等待,阻塞,和上面一行作用一样。 print(q.get()) print(q.get()) print(q.get())
2、class queue.LifoQueue(maxsize=0)#堆栈:后进先出 last in first out**
import queue q=queue.LifoQueue(3) q.put('1') q.put('2') q.put('3') q.put_nowait('4') print(q.get()) print(q.get()) print(q.get())
ps:可实现三级菜单
3、class queue.PriorityQueue(maxsize=0) #优先级队列:存储数据时可以设计优先级的队列
import queue q=queue.PriorityQueue() #put进入一个元组,元组的第一个元素是优先级(通常是数字,也可以是非数字之间的比较),数字越小优先级越高 q.put((20,'a')) q.put((10,'b')) q.put((30,'c')) print(q.get()) print(q.get()) print(q.get()) ''' 结果(数字越小优先级越高,优先级高的优先出队): (10, 'b') (20, 'a') (30, 'c') '''
1.3.10 进程池与线程池
进程池,就是用来存放进程的池子,本质还是基于多进程,只不过是对开启进程的数目加上了限制 官网:https://docs.python.org/dev/library/concurrent.futures.html concurrent.futures模块提供了高度封装的异步调用接口 ThreadPoolExecutor:线程池,提供异步调用 ProcessPoolExecutor: 进程池,提供异步调用 Both implement the same interface, which is defined by the abstract Executor class.
基本方法
1、submit(fn, *args, **kwargs) 异步提交任务 2、map(func, *iterables, timeout=None, chunksize=1) 高阶函数 取代for循环submit的操作 3、shutdown(wait=True) 相当于进程池的pool.close()+pool.join()操作 wait=True,等待池内所有任务执行完毕回收完资源后才继续 wait=False,立即返回,并不会等待池内的任务执行完毕 但不管wait参数为何值,整个程序都会等到所有任务执行完毕 submit和map必须在shutdown之前 4、result(timeout=None) 取得结果 5、add_done_callback(fn) 回调函数 from multiprocessing import Pool if __name__ == "__main__": pool = Pool(34) #创建进程池 try: pool.map('函数',range(11)) except Exception as e: print(e) pool.close()#关闭进程池,不再接受新的进程 pool.join()#主进程阻塞等待子进程的退出
用法** +map方法
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import os,time,random def task(n): print('%s is runing' %os.getpid()) time.sleep(random.randint(1,3)) return n**2 if __name__ == '__main__': pool=ProcessPoolExecutor(max_workers=3) # 个数一般是CPU的个数 futures=[] for i in range(11): future=pool.submit(task,i) #pool.map(task,range(11))#这里也可以用map,取代了for+submit futures.append(future) pool.shutdown(True) #相当于进程池的pool.close()+pool.join()操作,等待池内所有任务执行完毕回收完资源后才继续,# 关闭池之后就不能继续提交任务,并且会阻塞,直到已经提交的任务完成 print('+++>') for future in futures: print(future.result())取得结果 future是一个对象
线程池实例
from concurrent.futures import ThreadPoolExecutor def func(i): print('start', os.getpid()) time.sleep(random.randint(1,3)) print('end', os.getpid()) return '%s * %s'%(i,os.getpid()) tp = ThreadPoolExecutor(20) # 个数一般是CPU的个数的5倍 # 获取返回值方法一: ret_l = [] for i in range(10): ret = tp.submit(func,i) ret_l.append(ret) # tp.shutdown() 关闭池之后就不能继续提交任务,并且会阻塞,直到已经提交的任务完成 for ret in ret_l: print(ret.result()) # 获取返回值方法二: ret = tp.map(func,range(20)) for i in ret: print(i)
回调函数
可以为进程池或线程池内的每个进程或线程绑定一个函数,该函数在进程或线程的任务执行完毕后自动触发,并接收任务的返回值当作参数,该函数称为回调函数
#爬虫例子 from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor import os,random,time,requests from multiprocessing import Pool def get(url): print('get %s'%url) response=requests.get(url) return {'url':url,'text':len(response.text)} def parse(res): print(res.result()) #res是对象 if __name__ == '__main__': urls=[ 'https://www.baidu.com', 'https://www.python.org', 'https://www.openstack.org', 'https://help.github.com/', 'http://www.sina.com.cn/' ] pool = ThreadPoolExecutor(3) for url in urls: pool.submit(get,url).add_done_callback(parse)
总结:
1. 方式: ```python # 创建一个池子 tp = ThreadPoolExcutor(池中线程/进程的个数) # 异步提交任务 ret = tp.submit(函数,参数1,参数2....) # 获取返回值 ret.result() # 在异步的执行完所有任务之后,主线程/主进程才开始执行的代码 tp.shutdown() # 阻塞,直到所有的任务都执行完毕 # map方法:迭代获取iterable中的内容,作为func的参数,让子线程来执行对应的任务 ret = tp.map(func,iterable) for i in ret: # 每一个都是任务的返回值 # 回调函数:要在ret对应的任务执行完毕之后,直接继续执行add_done_callback绑定的函数中的内容,并且ret的结果会作为参数返回给绑定的函数 ret.add_done_callback(函数名)
- 是单独开启线程进程还是池?
- 如果只是开启一个子线程做一件事情,就可以单独开线程
- 如果有大量的任务等待程序去做,要达到一定的并发数,开启线程池
- 根据你程序的io操作也可以判定是用池还是不用池:
- socket的server, 大量的阻塞io操作, 所以socketserver中没有使用池
- 爬虫,获取数据快,所以爬虫使用池
1.4 多协程
1.4.1 协程介绍
- 协程:是单线程下的并发,又称微线程。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
- 能够在一个线程下的多个任务之间来回切换,那么每一个任务都是一个协程
- 用户级别的,有我们自己写的python代码来控制切换的,是操作系统不可见的
1.4.2 好处
- 一个线程中的阻塞都被其他的各种任务占满了
- 迷惑操作系统,让操作系统觉的这个线程很忙,尽量减少这个线程进入阻塞的状态,提高了单线程对cpu的利用率
- 多个任务在同一个线程中执行,也达到了一个并发的效果,规避了每一个任务的IO操作(遇到io阻塞了就切换另外一个任务去执行),减少了线程的个数,减轻了操作系统的负担
1.4.3 缺点
- 协程的本质是单线程,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程
- 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
1.4.4 补充:
在cpython解释器下,协程和线程都不能利用多核,都是在一个CPU上轮流执行
- 由于多线程本身就不能利用多核,所以即便是开启了多个线程也只能轮流在一个CPU上执行
- 协程如果把所有任务的IO操作都规避掉,只剩下需要使用的CPU操作,就意味着协程可以做到提高CPU利用率的作用
1.4.5 多线程和协程的区别
1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行) 2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
总结协程特点:
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 附加:一个协程遇到IO操作自动切换到其它协程(如何实现检测IO,yield、greenlet都无法实现,就用到了gevent模块(select机制))
1.4.6 greenlet
import time from greenlet import greenlet def eat(): print('wusir is eating') time.sleep(0.5) # g2.switch() print('wusir finished eat') def sleep(): print('小马哥 is sleeping') time.sleep(0.5) print('小马哥 finished sleep') # g1.switch() g1 = greenlet(eat) g2 = greenlet(sleep) g1.switch()
1.4.7 协程的两种切换方式 gevent和asyncio
1、C语言完成的python模块:gevent模块
Gevent是一个第三方库,可以通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是greenlet,他是以c扩展模块形式接入python的轻量级协程。 greenlet全部运行在主程序操作系统进程内部。
用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的 g2=gevent.spawn(func2) g1.join() #等待g1结束 g2.join() #等待g2结束 #或者上述两步合作一步:gevent.joinall([g1,g2]) g1.value#拿到func1的返回值
遇到IO阻塞时会自动切换任务
上例gevent.sleep(2)模拟的是gevent可以识别的io阻塞,
而time.sleep(2)或其他的阻塞,gevent是不能直接识别的需要用下面一行代码,打补丁,就可以识别了
from gevent import monkey;monkey.patch_all()
import gevent from gevent import monkey;monkey.patch_all() #打 from threading import current_thread import time def eat(name): print('%s eat 1'%name,current_thread().getName()) gevent.sleep(2) # time.sleep(2) print('%s eat 2'%name,current_thread().getName()) def play(name): print('%s play 1'%name,current_thread().getName()) gevent.sleep(2) # time.sleep(2) print('%s play 2'%name,current_thread().getName()) g1=gevent.spawn(eat,'alex') #创建一个协程任务 g2=gevent.spawn(play,'alex') #创建协程任务 g1.join() #阻塞,知道g1任务完成 g2.join() #阻塞,知道g2任务完成 #gevent.joinall([g1,g2]) #等于上面两部 print('主')
2、python原生的底层的协程模块:asyncio模块
- asyncio模块,是基于yield机制切换的
- 应用
- 爬虫,webserver框架
- 提高网络编程的效率和并发效果
- 语法
- async:标识一个协程函数
- await:后面跟着一个asyncio模块提供的io操作的函数
- 协程函数这里要切换出去,还能保证一会儿再切回来
- await 必须写在async函数里,async函数是协程函数
- loop:事件循环,负责在多个任务之间进行切换
- 所有的协程的执行 调度 都离不开这个loop
# 示例一:起一个任务 import asyncio async def demo(): # 协程方法 print('start') await asyncio.sleep(1) # 阻塞 print('end') loop = asyncio.get_event_loop() # 创建一个事件循环 loop.run_until_complete(demo()) # 把demo任务丢到事件循环中去执行 # 示例二:启动多个任务,并且没有返回值 import asyncio async def demo(): # 协程方法 print('start') await asyncio.sleep(1) # 阻塞 print('end') loop = asyncio.get_event_loop() # 创建一个事件循环 wait_obj = asyncio.wait([demo(),demo(),demo()]) loop.run_until_complete(wait_obj) # 示例三:启动多个任务并且有返回值,按顺序取返回值 import asyncio async def demo(): # 协程方法 print('start') await asyncio.sleep(1) # 阻塞 print('end') return 123 loop = asyncio.get_event_loop() t1 = loop.create_task(demo()) t2 = loop.create_task(demo()) tasks = [t1,t2] wait_obj = asyncio.wait([t1,t2]) loop.run_until_complete(wait_obj) for t in tasks: print(t.result()) # 示例四:启动多个任务并且有返回值,谁先回来先取谁的结果 import asyncio async def demo(i): # 协程方法 print('start') await asyncio.sleep(10-i) # 阻塞 print('end') return i,123 async def main(): task_l = [] for i in range(10): task = asyncio.ensure_future(demo(i)) task_l.append(task) for ret in asyncio.as_completed(task_l): res = await ret print(res) loop = asyncio.get_event_loop() loop.run_until_complete(main())
import asyncio async def get_url(): reader,writer = await asyncio.open_connection('www.baidu.com',80) writer.write(b'GET / HTTP/1.1\r\nHOST:www.baidu.com\r\nConnection:close\r\n\r\n') all_lines = [] async for line in reader: data = line.decode() all_lines.append(data) html = '\n'.join(all_lines) return html async def main(): tasks = [] for url in range(20): tasks.append(asyncio.ensure_future(get_url())) for res in asyncio.as_completed(tasks): result = await res print(result) if __name__ == '__main__': loop = asyncio.get_event_loop() loop.run_until_complete(main()) # 处理一个任务
二 练习**
通过gevent实现单线程下的socket并发(from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞)
服务端 (单线程下实现并发,用gevent创建协程对象)
from gevent import monkey;monkey.patch_all() from socket import * import gevent #如果不想用money.patch_all()打补丁,可以用gevent自带的socket # from gevent import socket # s=socket.socket() def server(server_ip,port): s=socket(AF_INET,SOCK_STREAM) s.setsockopt(SOL_SOCKET,SO_REUSEADDR,1) s.bind((server_ip,port)) s.listen(5) while True: conn,addr=s.accept() gevent.spawn(talk,conn,addr) def talk(conn,addr): try: while True: res=conn.recv(1024) print('client %s:%s msg: %s' %(addr[0],addr[1],res)) conn.send(res.upper()) except Exception as e: print(e) finally: conn.close() if __name__ == '__main__': server('127.0.0.1',8080)
多线程并发多个客户端 模拟500个用户
from threading import Thread from socket import * import threading def client(server_ip,port): c=socket(AF_INET,SOCK_STREAM) #套接字对象一定要加到函数内,即局部名称空间内,放在函数外则被所有线程共享,则大家公用一个套接字对象,那么客户端端口永远一样了 c.connect((server_ip,port)) count=0 while True: c.send(('%s say hello %s' %(threading.current_thread().getName(),count)).encode('utf-8')) msg=c.recv(1024) print(msg.decode('utf-8')) count+=1 if __name__ == '__main__': for i in range(500): t=Thread(target=client,args=('127.0.0.1',8080)) t.start()
1.5 IO模型
1.5.1 IO模型介绍:
说一下IO发生时涉及的对象和步骤。对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,该操作会经历两个阶段:
1)等待数据准备 (Waiting for the data to be ready) 2)将数据从内核拷贝到进程中(Copying the data from the kernel to the process)
1.5.2 阻塞IO(blocking IO)
而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存, 然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被block了。
1.5.3 非阻塞IO(non-blocking IO)
也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好, 此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程, 循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程, 进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
所以,在非阻塞式IO中,用户进程其实是需要不断的主动询问kernel数据准备好了没有。
#服务端 from socket import * server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1',8099)) server.listen(5) server.setblocking(False) rlist=[] wlist=[] while True: try: conn, addr = server.accept() rlist.append(conn) print(rlist) except BlockingIOError: del_rlist=[] for sock in rlist: try: data=sock.recv(1024) if not data: del_rlist.append(sock) wlist.append((sock,data.upper())) except BlockingIOError: continue except Exception: sock.close() del_rlist.append(sock) del_wlist=[] for item in wlist: try: sock = item[0] data = item[1] sock.send(data) del_wlist.append(item) except BlockingIOError: pass for item in del_wlist: wlist.remove(item) for sock in del_rlist: rlist.remove(sock) server.close() #客户端 from socket import * c=socket(AF_INET,SOCK_STREAM) c.connect(('127.0.0.1',8080)) while True: msg=input('>>: ') if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8'))
但是非阻塞IO模型绝不被推荐
缺点:
1. 循环调用recv()将大幅度推高CPU占用率;这也是我们在代码中留一句time.sleep(2)的原因,否则在低配主机下极容易出现卡机情况 2. 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。 这会导致整体数据吞吐量的降低。
1.5.4 多路复用IO(IO multiplexing)
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket, 当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。 这个图和blocking IO的图其实并没有太大的不同,事实上还更差一些。因为这里需要使用两个系统调用\(select和recvfrom\), 而blocking IO只调用了一个系统调用\(recvfrom\)。但是,用select的优势在于它可以同时处理多个connection。
强调:
1. 如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
2. 在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
结论: select的优势在于可以处理多个连接,不适用于单个连接
select网络IO模型示例
#服务端 from socket import * import select server = socket(AF_INET, SOCK_STREAM) server.bind(('127.0.0.1',8093)) server.listen(5) server.setblocking(False) print('starting...') rlist=[server,] wlist=[] wdata={} while True: rl,wl,xl=select.select(rlist,wlist,[],0.5) #select查将准备好的数据返回 print(wl) for sock in rl: #准备好的套接字 if sock == server: conn,addr=sock.accept() rlist.append(conn) else: try: data=sock.recv(1024) if not data: sock.close() rlist.remove(sock) continue wlist.append(sock) wdata[sock]=data.upper() except Exception: sock.close() rlist.remove(sock) for sock in wl: #可以发数据的套接字 sock.send(wdata[sock]) wlist.remove(sock) wdata.pop(sock) #客户端 from socket import * c=socket(AF_INET,SOCK_STREAM) c.connect(('127.0.0.1',8081)) while True: msg=input('>>: ') if not msg:continue c.send(msg.encode('utf-8')) data=c.recv(1024) print(data.decode('utf-8'))
select监听fd变化的过程分析:
用户进程创建socket对象,拷贝监听的fd到内核空间,每一个fd会对应一张系统文件表,内核空间的fd响应到数据后, 就会发送信号给用户进程数据已到; 用户进程再发送系统调用,比如(accept)将内核空间的数据copy到用户空间,同时作为接受数据端内核空间的数据清除, 这样重新监听时fd再有新的数据又可以响应到了(发送端因为基于TCP协议所以需要收到应答后才会清除)。
该模型的优点:
相比其他模型,使用select() 的事件驱动模型只用单线程(进程)执行,占用资源少,不消耗太多 CPU,同时能够为多客户端提供服务。 如果试图建立一个简单的事件驱动的服务器程序,这个模型有一定的参考价值。
该模型的缺点:
首先select()接口并不是实现“事件驱动”的最好选择。因为当需要探测的句柄值较大时,select()接口本身需要消耗大量时间去轮询各个句柄。 很多操作系统提供了更为高效的接口,如linux提供了epoll,BSD提供了kqueue,Solaris提供了/dev/poll,…。 如果需要实现更高效的服务器程序,类似epoll这样的接口更被推荐。遗憾的是不同的操作系统特供的epoll接口有很大差异, 所以使用类似于epoll的接口实现具有较好跨平台能力的服务器会比较困难。 其次,该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,则对整个模型是灾难性的。
1.5.5 异步IO(Asynchronous I/O)
爬虫领域多,数据准备好后,自己做后面的操作(调用回调函数)
1.5.6 IO模型比较分析
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,四个IO模型可以分为两大类,
同步IO:blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类,
异步IO:而 asynchronous I/O后一类 。
到目前为止,已经将四个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阻塞。按照这个定义,四个IO模型可以分为两大类, 之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO这一类,而 asynchronous I/O后一类 。 有人可能会说,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操作的状态,也不需要主动的去拷贝数据。
1.5.7 一 了解select,poll,epoll
1.5.8 socketserver 模块
socketserver使用模式 1、定义功能类handle方法是开多线程的业务逻辑class Myserver(socketserver.BaseRequestHandler): def handle(self): pass 2、TCP协议,传端口和定义的类server = socketserver.ThreadingTCPServer(('127.0.0.1',9999),Myserver) 3、建立链接server.serve_forever() #accept'''
1.6 本章小结
- 操作系统
- 计算机中所有的资源都是由操作系统分配的
- 操作系统调度任务:时间分片、多道机制
- 提高CPU的利用率是我们努力的指标
- 什么是io操作?
- i:input
- 向内存输入
- 示例:input / read / recv / recvfrom / accept / connect / close
- o:output
- 从内存输出
- 示例:print / write / send / sendto / accept / connect / close
- i:input
- 什么是GIL?
- 全局解释器锁
- 由cpython解释器提供的
- 导致了一个进程中多个线程同一时刻只能有一个线程访问CPU
- 什么是IPC?
- 进程之间的通信机制
- 内置的模块(基于文件):Queue(队列)、Pipe (管道)
- 第三方工具(基于网络):redis / rabbitmq / kafka / memcache
- 发挥的都是消息中间件的功能
- 并发需求、高可用、断电保存数据、解耦
- 进程、线程、协程的区别:
- 进程:开销大,数据隔离,能利用多核,数据不安全,操作系统控制
- 线程:开销中,数据共享,cpython解释器下不能利用多核,数据不安全,操作系统控制
- 协程:开销小,数据共享,不能利用多核,数据安全,用户控制
6.在进程、线程中都需要用到锁的概念
- 互斥锁:在一个线程中不能连续acquire多次,效率高,产生死锁的几率大
- 递归锁:在一个线程中可以连续acquire多次,效率低,一把锁永远不死锁
- 死锁现象:在某一些线程中出现陷入阻塞并且永远无法结束阻塞的情况就是死锁现象
- 出现死锁:
- 多把锁+交替使用
- 互斥锁在一个线程中连续acquire
- 避免死锁:
- 在一个线程中只有一把锁,并且每一次acquire之后都要release
- 出现死锁:
- 进程和线程都有锁,可以互用吗?
- 所有在线程中能工作的基本都不能在进程中工作
- 在进程中能够使用的基本在线程中也可以使用
- 所以进程锁在线程中可以使用,但线程锁不能在进程中使用
7.关于线程的数据安全:
- 开启线程几乎不需要时间的,start是一个异步非阻塞方法
- 对于整数的+=、-=、*=、/=来说,异步的多线程数据不安全,同步的话就安全了
- 对于列表的操作,无论是异步还是同步的,都是数据安全的
练习题
1、简述计算机操作系统中的“中断”的作用?
cpu会切断,io阻塞,程序运行时间过长 中断:计算机执行期间,系统内发生任何非寻常的或者非预期的紧急需处理的时间,使得cpu暂时中断正在执行的程序而去执行相应的时间处理程序
2、简述计算机内存中的“内核态”和“用户态”;
操作系统的核心是内核,独立于普通的应用程序,内核可以访问受保护的内存空间,也可以访问底层硬件设备的所有权限,为了保护用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间分为内核空间和用户空间 内核态:运行操作系统的程序,os的数据存放 用户态:运行用户程序,用户进程的数据存放
3、进程间通信方式有哪些?
进程间的通讯(ipc) 消息队列(队列=管道+锁) 管道(使用消息传递) 信号量 套接字 共享内存
4、简述你对管道、队列的理解;
管道通常指无名管道 1、他是半双工的(数据只能在一个方向上流动),有固定的读端和写端 2、他只能用于具有亲缘关系的进程中通讯(也就是父与子进程,或兄弟进程之间) 3、数据不可反复读取 消息队列 1、消息队列是面向记录的,其中的消息具有特定的格式以及特定的优先级 2.消息队列独立于发送与接收进程,进程终止时,消息队列及其内容不会被删除 3、消息队列可实现消息随机查询 队列=管道+锁
5、请列举你知道的进程间通信方式;
队列。信号量。event事件,定时器Timer,线程queue,进程池线程池,异步调用+回调机制
6、什么是同步I/O,什么是异步I/O?
同步IO指的是同步传输,当发送一个数据请求时,会一直等待,直到有返回结果为止 异步IO指的是异步传输,当发送一个数据请求时,会立即去处理别的事情,当有数据处理完毕时,会自动返回结果
7、请问multiprocessing模块中的Value、Array类的作用是什么?举例说明它们的使用场景
8、请问multiprocessing模块中的Manager类的作用是什么?与Value和Array类相比,Manager的优缺点是什么?
9、写一个程序,包含十个线程,子线程必须等待主线程sleep 10秒钟之后才执行,并打印当前时间;
from threading import Thread,current_thread import time def talk(): print('%s is running'%current_thread().getName(),time.time()) if __name__ == '__main__': t_l = [] time.sleep(10) for i in range(10): t = Thread(target=talk) t.start() print('zhu')
10、写一个程序,包含十个线程,同时只能有五个子线程并行执行;
from concurrent.futures import ThreadPoolExecutor,ProcessPoolExecutor from threading import current_thread import time def talk(n): print('%s is running'%current_thread().getName(),time.time()) time.sleep(1) return n**2 if __name__ == '__main__': pool=ThreadPoolExecutor(5) lst=pool.map(talk,range(10)) pool.shutdown(True) # for i in lst: # print(i) print('zhu')
11、写一个程序,要求用户输入用户名和密码,要求密码长度不少于6个字符,且必须以字母开头,如果密码合法,则将该密码使用md5算法加密后的十六进制概要值存入名为password.txt的文件,超过三次不合法则退出程序;
import re,hashlib class Login(): def __init__(self): pass def save_file(self,user_obj): with open('password.txt','w') as f: f.write(user_obj) def get_md5(self,password): md5 = hashlib.md5() md5.update(password.encode('utf-8')) return md5.hexdigest() def verfiy_password(self,password): if len(password)>=6 and re.search('^[a-zA-Z]',password): return True else: return False def login(self): count = 0 while 1: if count<3: username = input('[name]:').strip() password= input('[password]:').strip() if self.verfiy_password(password): print('duile') md5_password=self.get_md5(password) print(md5_password) user_obj = {'user':username,'password':md5_password} self.save_file(str(user_obj)) break else: print('input error...') count += 1 else: print('input error,please waitting more time') break obj=Login() obj.login()
12、写一个程序,使用socketserver模块,实现一个支持同时处理多个客户端请求的服务器,要求每次启动一个新线程处理客户端请求;
#服务端 import socketserver port = ('127.0.0.1',9999) class Myserver(socketserver.BaseRequestHandler): def handle(self): while 1: data = self.request.recv(1024) if not data: break self.request.send(data.upper()) if __name__ == '__main__': server=socketserver.ThreadingTCPServer(port,Myserver) server.serve_forever() #客户端 import socketserver from socket import * from threading import Thread,current_thread port = ('127.0.0.1',9999) def c(): client = socket() client.connect(port) client.send('hello'.encode()) data = client.recv(1024) print(data.decode(),current_thread().getName()) if __name__ == '__main__': for i in range(100): t = Thread(target=c) t.start() print('wancheng')
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)