我的python之路【第九章】线程.进程.协程
本节大纲
day9 - 线程进程介绍 1. 工作最小单元是线程 2. 应用程序 -> 至少有一个进程 -> 至少有一个线程 3. 应用场景: IO密集型:线程 计算密集型:进程 4. GIL,全局解释器锁。 - 保证同一个进程中只有一个线程同时被调度 - 线程 1. 基本使用 def task(arg): time.sleep(arg) print(arg) for i in range(5): t = threading.Thread(target=task,args=[i,]) # t.setDaemon(True) # 主线程终止,不等待子线程 # t.setDaemon(False) t.start() # t.join() # 一直等 # t.join(1) # 等待最大时间 2. 锁 # 1. 只能有一个人使用锁 # lock = threading.Lock() # 只能开一把 # lock = threading.RLock()# 可以开多把 # 2. 多个人同时使用锁 # lock = threading.BoundedSemaphore(3) # 3. 所有的解脱锁的限制 # lock = threading.Event() # 4. 肆意妄为 # lock = threading.Condition() 3. 线程池 模式一:直接处理 def task(url): """ 任务执行两个操作:下载;保存本地 """ # response中封装了Http请求响应的所有数据 # - response.url 请求的URL # - response.status_code 响应状态码 # - response.text 响应内容(字符串格式) # - response.content 响应内容(字节格式) # 下载 response = requests.get(url) # 下载内容保存至本地 f = open('a.log','wb') f.write(response.content) f.close() pool = ThreadPoolExecutor(2) url_list = [ 'http://www.oldboyedu.com', 'http://www.autohome.com.cn', 'http://www.baidu.com', ] for url in url_list: print('开始请求',url) # 去连接池中获取链接 pool.submit(task,url) 模式二:分步处理 def save(future): """ 只做保存 # future中包含response """ response = future.result() # 下载内容保存至本地 f = open('a.log','wb') f.write(response.content) f.close() def task(url): """ 只做下载 requests """ # response中封装了Http请求响应的所有数据 # - response.url 请求的URL # - response.status_code 响应状态码 # - response.text 响应内容(字符串格式) # - response.content 响应内容(字节格式) # 下载 response = requests.get(url) return response pool = ThreadPoolExecutor(2) url_list = [ 'http://www.oldboyedu.com', 'http://www.autohome.com.cn', 'http://www.baidu.com', ] for url in url_list: print('开始请求',url) # 去连接池中获取链接 # future中包含response future = pool.submit(task,url) # 下载成功后,自动调用save方法 future.add_done_callback(save) - 进程 1. 基本使用 from multiprocessing import Process import time def task(arg): time.sleep(arg) print(arg) if __name__ == '__main__': for i in range(10): p = Process(target=task,args=(i,)) p.daemon = True # p.daemon = False p.start() p.join(1) print('主进程最后...') 2. 进程之间的数据共享 特殊的东西 - Array(‘类型’,长度) - Manager().list() / Manager().dict() 3. 进程池 ================== 结论 ================== IO密集:线程 计算密集:进程 - 协程 pip3 install greenlet 协程永远是一个线程在执行,对线程的一个分片处理。 二次加工: 自定义: select实现 现成 : pip3 install gevent - IO多路复用 监听多个socket对象是否有变化(可读,可写,发送错误) - 示例一: - socketserverIO - IO多路复用 - 线程 - 自定义异步非阻塞的框架 参考博客: 线程、进程、协程: http://www.cnblogs.com/wupeiqi/articles/5040827.html IO多路复用: http://www.cnblogs.com/wupeiqi/articles/5040823.html 本周作业: 服务端:socketserver 用IO多路复用select,使用“伪”并发 客户端: 基本操作: 聊天 上传 尝试: 客户端是否可以用select来实现???
1.线程的基本使用
import threading import time def task(arg): time.sleep(arg) print(arg) #target 写函数名,args 传入参数,必须是可迭代的 # t=threading.Thread(target=task,args=[13,]) # t.start() #start 是说准备好了,等待cpu来执行 for i in range(30): t=threading.Thread(target=task,args=[i,]) t.setDaemon(True) #主线程终止,不等待子线程----只能在start之前定义 t.start() #start 是说准备好了,等待cpu来执行 # t.join() #一直等待子线程执行完成后,执行主线程 # t.join(1) #等待最大时间,1s 如果子线程没有执行完就继续执行,每次等待1s print('end')
2.继承threading.Thread 重构父类方法
#继承父类threading.Thread class MyThread(threading.Thread): def __init__(self,func,*args,**kwargs): #重构父类的构造方法 super(MyThread,self).__init__(*args,**kwargs) #继承父类的构造方法 self.func=func #加入一个参数 def run(self): #重构run 方法 print('子类') self.func() def test(): time.sleep(1) print(1111) obj=MyThread(func=test) #这里的start方法其实还是从父类继承过来的。他其实是去调用了self.run() 方法,由于子类重构了run方法 obj.start() #执行了子类中的run() 方法
线程锁:
import threading import time v=10 #1.只能有一个人使用锁 # lock=threading.Lock() #创建一个锁,只能进去一个线程 # # lock=threading.RLock() #支持可以上两把锁 # def task(arg): # time.sleep(2) # #申请使用锁,其它人等待 # print('启动线程:',arg) # lock.acquire() # # lock.acquire() #配合Rlock使用 # time.sleep(1) # global v # v-=1 # print(v) # #释放锁 # lock.release() # # lock.release() #配合Rlock使用 # for i in range(10): # t=threading.Thread(target=task,args=(i,)) # t.start() '''2.多人同时使用锁''' #-----规定同时可以有三人使用锁----- # lock=threading.BoundedSemaphore(3) # def task(arg): # time.sleep(2) # #申请使用锁,其它人等待 # print('启动线程:',arg) # lock.acquire() # time.sleep(1) # global v # v-=1 # print(v) # #释放锁 # lock.release() # # lock.release() #配合Rlock使用 # for i in range(10): # t=threading.Thread(target=task,args=(i,)) # t.start() '''3.所有的线程解脱锁的限制''' # #事件锁 # lock=threading.Event() # def task(arg): # time.sleep(1) # #申请使用锁,其它人等待 # print('启动线程:',arg) # #锁住所有的线程,全部暂停.等待释放----- # lock.wait() # print(v) # # for i in range(10): # t=threading.Thread(target=task,args=(i,)) # t.start() # while True: # value=input('>>') # if value=='1': # #local.set() 放开所有被锁住的线程 # lock.set() #其实就将wait中的_flag 标签修改成True。并执行_cond.notify_all() # #与set()相对的 # # lock.clear() # self._flag = False 但不执行其它操作 '''4.肆意妄为''' lock=threading.Condition() #自定义锁中允许通过的数量,可以实时的传参 #配合地址池使用 def task(arg): time.sleep(2) #申请使用锁,其它人等待 print('启动线程:',arg) lock.acquire() lock.wait() print('线程,',arg) lock.release() # lock.release() #配合Rlock使用 for i in range(10): t=threading.Thread(target=task,args=(i,)) t.start() while True: value=input('>>>') #输入要运行线程的数量 lock.acquire() lock.notify(int(value)) #自定义通过锁的线程数量 lock.release()
线程池:
'''创建线程池-基本使用''' from concurrent.futures import ThreadPoolExecutor import time def task(arg): time.sleep(0.5) print(arg) pool = ThreadPoolExecutor(5) #线程池限制最多可以创建5个线程同时执行 for i in range(100): #去连接池中获取连接 -- 去执行 pool.submit(task,i) # #程序执行的过程发现,一次最多出现5个线程执行的结果
# 线程池,等待
from concurrent.futures import ThreadPoolExecutor, wait
#存放线程任务
fs = []
# 循环端口进行socket探测
for port in range(int(startport), int(endport)):
# 将启动的线程任务存放到fs
fs.append(self.pool.submit(self.socket_connect, ip, port))
# 等待fs中所有的线程task完成,如果是等待时间就,可以通过finished和doing 获取当前完成多少个线程任务,正在运行的有多少线程任务。len(doing)
finished, doing = wait(fs,)
self.log.info("-----------批量扫描端口结束-------------")
使用线程池请求http
#C:\Users\liuhao\AppData\Local\Programs\Python\Python35\Scripts #pip3 install requests '''使用线程池请求http ''' from concurrent.futures import ThreadPoolExecutor import requests def task(url): response = requests.get(url) print('得到结果:',url,len(response.content)) pool = ThreadPoolExecutor(2) #线程池中创建两个线程--- 他会等待 线程执行完毕再执行下一个线程 url_list=[ 'http://www.baidu.com', 'http://www.taobao.com', 'http://www.jingdong.com' ] for url in url_list: print('开始请求',url) #去连接池中获取连接 --去执行 pool.submit(task,url)
回调函数
'''利用回调函数,实现代码的多种功能的扩展性''' from concurrent.futures import ThreadPoolExecutor import requests def txt(future): #根据add_done_callbak 回调函数 执行下列操作 download_response=future.result() #获取download函数返回的结果 print('处理中',download_response.url,download_response.status_code,len(download_response.text)) def download(url): response=requests.get(url) return response #response 包含里下载的所有内容 pool = ThreadPoolExecutor(2) url_list = [ 'http://www.jingdong.com', 'http://www.baidu.com', 'http://www.taobao.com', ] for url in url_list: #去连接池中获取连接 print('开始请求',url) future = pool.submit(download,url) #执行这个方法返回一个实例 future.add_done_callback(txt) #通过这个返回的实例,调用传入的方法 也可以传入一个其它的方法-比如打印网站信息等等-做一些爬虫的匹配
在线程池中设置等待程序执行完毕(wait)
wait方法接会返回一个tuple(元组),tuple中包含两个set(集合),一个是completed(已完成的)另外一个是uncompleted(未完成的)。
使用wait方法的一个优势就是获得更大的自由度,它接收三个参数
FIRST_COMPLETED
FIRST_EXCEPTION
ALL_COMPLETE,默认设置为ALL_COMPLETED
默认: wait(fs, timeout=None, return_when=ALL_COMPLETED)
# 首先要在导入线程池的同时,导入wait from concurrent.futures import ThreadPoolExecutor,wait # 定义线程池 futures = [] pool = ThreadPoolExecutor(10) for num in range(10): futures.append(pool.submit(function, num)) # 如果采用默认的ALL_COMPLETED,程序会阻塞直到线程池里面的所有任务都完成。 wait(futures) # 如果采用FIRST_COMPLETED参数,程序并不会等到线程池里面所有的任务都完成。 #print(wait(futures,timeout=None,return_when='FIRST_COMPLETED'))
参考博文:http://python.jobbole.com/87272/
进程的基本使用--------就使用而言-只需要将调用的Thread改成Process就可以了-----其它的功能可线程都一样
from multiprocessing import Process import time def task(arg): time.sleep(arg) print(arg) if __name__ == '__main__':#windows中必须这样写,才能执行 for i in range(10): p=Process(target=task,args=(i,)) p.daemon=True #主进程不等待子进程执行完毕, # p.daemon = False #默认是False p.start() p.join(1) print('主进程中的主线程执行到最后......')
演示:进程之间的数据不共享------但是线程之间的数据是共享的
#验证进程之间数据不共享 from multiprocessing import Process from threading import Thread def task(num,li): li.append(num) print(li) if __name__ == '__main__': v=[] for i in range(10): # p = Thread(target=task, args=(i, v,)) #演示线程的数据共享 p=Process(target=task,args=(i,v,)) #演示进程的数据不共享 p.start()
进程之间如果想要实现数据共享---需要特殊的数据类型
# 方式一: #进程之间通过特殊的数据类型进行数据共享----C语言中的Array 数组 from multiprocessing import Process,Array def task(num,li): li[num]=1 print('进程:',num,list(li)) if __name__ == '__main__': # v=Array('数据类型','长度') v=Array('i',20) #C语言中数组, for i in range(20): p=Process(target=task,args=(i,v,)) p.start()
利用Manager实现进程之间的数据共享
#使用Manager 实现进程之间的数据共享:提倡使用 from multiprocessing import Process,Manager def task(num,li): li.append(num) print('进程:',num,li) if __name__ == '__main__': # dic=Manager().dict() v=Manager().list() for i in range(10): p=Process(target=task,args=(i,v,)) p.start() #会有报错-原因:进程之间建立socket连接进行通讯, #主进程关闭,socket连接意外断开--- # p.join() #两种模拟解决方式---提倡用进程池 input('>>>')
进程池的调用:
'''进程池--方式一:直接调用''' from concurrent.futures import ProcessPoolExecutor import time def task(arg): time.sleep(1) print(arg) if __name__ == '__main__': pool= ProcessPoolExecutor(2) for i in range(10): pool.submit(task,i)
#方式二: 分布进行 from concurrent.futures import ProcessPoolExecutor import time def call(arg): data=arg.result() time.sleep(1) print(data) def task(arg): #将arg+100 返回结果 print(arg) return arg+100 if __name__ == '__main__': pool= ProcessPoolExecutor(2) for i in range(10): obj=pool.submit(task,i) #obj获取到了task返回的结果,但是不能直接调用,需要特殊的方法才能获取 obj.add_done_callback(call) #回调函数执行call
协程:
Python通过yield
提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成:
#协程:使用一个线程去执行多个任务,将一个线程的时间进行切片-- 实现单线程的多并发 '''建议使用debug模式去查看下面代码的运行流程''' from greenlet import greenlet #导入协程的模块 def f1(): print('A+') g2.switch() #跳转到g2 对象 print('B+') g2.switch() def f2(): print('A-') g1.switch() #跳转到g1对象 print('B-') g1.switch() g1= greenlet(f1) #根据协程 创建g1对象,传入函数f1 g2= greenlet(f2) #根据协程 创建g2对象,传入函数f2 g1.switch() #开始运行
# 根据协程二次开发:协程+IO from gevent import monkey;monkey.patch_all() #下面是协程配合IO操作进行的伪并发 import gevent import requests def f(url): response=requests.get(url) print(response.url,response.status_code) #进行执行 gevent.joinall([ gevent.spawn(f,'http://www.baidu.com/'), gevent.spawn(f,'http://www.taobao.com/'), gevent.spawn(f,'http://www.jingdong.com/'), ])
IO多路复用:server端
# IO多路复用:8002,8001 '''利用IO多路复用:实现单线程多并发的server端''' import socket,select sk1 = socket.socket() sk1.bind(('127.0.0.1',8001,)) sk1.listen(5) sk2 = socket.socket() sk2.bind(('127.0.0.1',8002,)) sk2.listen(5) inputs = [sk1,sk2,] while True: # IO多路复用,同时监听多个socket对象 # - select,内部进行循环操作(1024) 主动查看 # - poll, 内部进行循环操作 主动查看 # - epoll, 被动告知 r,w,e = select.select(inputs,[],[],0.05) # r = [sk2,] #r中只有请求来的时候才会有值,没有请求过来-就没有值 # r = [sk1,] # r = [sk1,sk2] # r = [] # r = [conn,] # r = [sk1,Wconn] for obj in r: if obj in [sk1,sk2]: # 新连接捡来了... print('新连接来了:',obj) conn,addr = obj.accept() inputs.append(conn) #将conn连接加入到进行监听的列表中 else: # 有连接用户发送消息来了.. print('有用户发送数据了:',obj) data = obj.recv(1024) obj.sendall(data)
clinet:
'''测试使用的客户端''' import socket client = socket.socket() client.connect(('127.0.0.1',8002,)) while True: v = input('>>>') client.sendall(bytes(v,encoding='utf-8')) ret = client.recv(1024) print('服务器返回:',ret)
IO多路复用高级版
1.解决客户端断开连接服务端异常退出问题
2.实现将用户请求分离
import socket import select # IO多路复用:8002,8001 # ############################### 基于select实现服务端的“伪”并发 ############################### sk1 = socket.socket() sk1.bind(('127.0.0.1',8001,)) sk1.listen(5) sk2 = socket.socket() sk2.bind(('127.0.0.1',8002,)) sk2.listen(5) inputs = [sk1,sk2,] w_inputs = [] while True: #实现读写 分离, 将读取和写入的操作都分开 r,w,e = select.select(inputs,w_inputs,inputs,0.05) #(inputs=读取,w_inputs=写入,inputs=异常) for obj in r: if obj in [sk1,sk2]: # 新连接捡来了... print('新连接来了:',obj) conn,addr = obj.accept() inputs.append(conn) else: # 有连接用户发送消息来了.. print('有用户发送数据了:',obj) try: data = obj.recv(1024) except Exception as ex: #当客户端端口连接,默认会发送数据-----服务端会报错 data = "" if data: #如果data 有数据 : 将 这个实例加入的写操作中 w_inputs.append(obj) # obj.sendall(data) print('有数据:',w_inputs) else: #如果发来的没有数据 obj.close() #关闭这个实例 print('没有数据',inputs) inputs.remove(obj) #将inputs中的实例移除 print('没有数据w:',w_inputs) for obj in w: obj.sendall(b'ok') w_inputs.remove(obj) #将w_inputs中的实例移除