python 进程,IO多路复用,协程
上篇内容回顾:
1. GIL锁
GIL的叫做全局解释器锁,用于限制一个进程中在一个时间点只有一个线程能够执行(获取到GIL的线程),语言初期设计的初衷是用于维护数据安全,但是在现有的硬件环境下,GIL锁在执行计算密集型运算时效率会导致效率下降(较其他支持多线程语言来说)。
2. 进程和线程的区别
进程是cpu资源分配的最小单元。
线程是cpu计算的最小单元。
一个进程中可以有多个线程,默认有一个主线程。
对于python来说它的进程和线程和其他语言有差异,是有GIL锁。
GIL锁限制了一个进程中同一时刻只有一个线程能在CPU中执行。
注意:IO密集型操作可以使用多线程;计算密集型可以使用多进程
3.Lock 和 RLock
同步锁和递归锁
递归锁能够解决同步锁中出现死锁的问题
4.线程池
from concurrent.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(10)
pool.submit(func, arg1, arg2)
限制线程开启的数量
线程过多会导致大量的线程上下文切换,会消耗资源,导致cpu性能下降。
5. threading.local
local = threading.local()
为每一个线程开辟一个私有空间,存放线程独有的变量。
6. 线程常用方法
setName (线程名称) 给线程定义一个自定义名称
setDaemon(bool) 给子线程设置守护线程,参数默认值是False,当为True时则是主线程执行完之后,所有正在运行的子线程也全部强制关闭
start 启动线程
join(int) 给予参数(int类型),表示主线程等待子线程多少秒,不给参数,默认是子线程执行完后,才执行后续主线程代码。
t = threading.current_thread() 在线程要执行的方法中运行,获取当前的线程对象,t为当前的线程对象
t.getName() 获取线程的名字
进程
1.什么是进程
进程是计算机中的最小的资源分配单位,我们编写的代码是存放在硬盘上的,在代码运行起来的时候就会产生进程,进程就是我们代码的实现的一个实体。
进程和线程的几点区别:
1.进程进行资源分配,线程才是执行代码的实体,进程是线程的容器。
2.不同进程中的数据是相互隔离的,而同进程下的多个线程是共享进程下的数据的。
进程与线程的选择取决以下几点:
1、需要频繁创建销毁的优先使用线程;因为对进程来说创建和销毁一个进程代价是很大的。
2、线程的切换速度快,所以在需要大量计算,切换频繁时用线程,还有耗时的操作使用线程可提高应用程序的响应
3、因为对CPU系统的效率使用上线程更占优,所以可能要发展到多机分布的用进程,多核分布用线程;
4、并行操作时使用线程,如C/S架构的服务器端并发线程响应用户的请求;
5、需要更稳定安全时,适合选择进程;需要速度时,选择线程更好。
2.如何创建一个进程
跟创建线程的方法几乎一样,也有两种方式:
实例化一个multiprocess.Process 的对象:
import multiprocessing def task(args): print(args) for i in range(10): # 创建10个进程 t = multiprocessing.Process(target=task, args=(i,)) t.start()
创建一个类继承multiprocess.Process,重写run方法:
import multiprocessing class P(multiprocessing.Process): def __init__(self, target, args=(), kwargs={}): super().__init__() self.target = target self.args = args self.kwargs = kwargs def run(self): self.target(*self.args, **self.kwargs) def task(args): print(args) for i in range(10): m = P(target=task, args=(i,)) m.start()
3.进程常用方法
m.daemon(bool) 设置主进程为子进程守护进程
m.name 设置进程的名字
start() 启动进程
m.join() 主进程等待子进程执行完成后继续执行
m1 = mutiprocessing.current_process() 获取方法当前运行进程的对象
m1.ident/pid 获取当前进程的id
import multiprocessing import time def func(arg): time.sleep(2) m = multiprocessing.current_process() #获取当前方法所在线程的对象 print(m.name) #获取线程对象的名字 print(m.ident, m.pid) #获取线程的id print(arg) for i in range(10): p = multiprocessing.Process(target=func, args=(i,)) p.daemon = False # 只能在进程没有执行之前设置守护进程,当为True时设置,将主进程设置为子进程的守护进程,当主进程销毁时,所有设置守护进程的子线程全都一起强制销毁 p.name = '进程%s' % i p.start() p.join() print(123)
4.创建进程池
创建进程池和线程池的方法基本也是一样
import time from concurrent.futures import ProcessPoolExecutor def task(args): time.sleep(1) print(args) pool = ProcessPoolExecutor(5) for i in range(50): pool.submit(task, i)
5.进程的数据共享
同台服务器上进程之间默认是进程之间数据是相互隔离的,进程之间的数据共享在业务场景中一般使用第三方中间件来完成(redis,mongodb等),但python也提供了几种方式在代码里直接实现进程间数据共享。
使用进程模块提供的Queue类来实现,multiprocessing中实现了跟模块queue的Queue功能一样的Queue,使用它能实现进程之间的数据共享。
import time from concurrent.futures import ProcessPoolExecutor queue = multiprocessing.Queue() def task(args): queue.put(args) print(args) time.sleep(2) pool = ProcessPoolExecutor(5) for i in range(10): pool.submit(task, i) while True: try: v = queue.get(timeout=2) print(v) except Exception: break
使用multiprocessing的Manager类来实现数据共享,使用该类对象创建一个特殊的字典就可以用于进程之间线程的共享了。
import multiprocessing from concurrent.futures import ProcessPoolExecutor m_d = multiprocessing.Manager() dic = m_d.dict() # 普通的字典无法实现进程数据共享的功能,用Manager可以创建一个支持进程之间数据共享的字典 def task(arg): dic[arg] = arg print(dic) pool = ProcessPoolExecutor(5) for i in range(10): pool.submit(task, i) m = multiprocessing.Process(target=task, args=(i,)) m.start() m.join() print(dic)
IO多路复用
1. 什么叫IO多路复用?
一个请求到来了,nginx使用epoll接收请求的过程是怎样的?, 多看看这个图就了解了。提醒下,ngnix会有很多链接进来, epoll会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。
单个线程中管理多个IO操作对象就叫IO多路复用
IO多路复用:检测多个socket是否发生变化(是否已经完成连接/是否接收到数据)(可写,可读)
2. 异步非阻塞模型(socket+IO多路复用+回调函数)
操作系统检测socket是否发生变化,有三种模式(操作系统)
select: 限制监听的socket的个数,上限是1024个;循环检测
poll: 不限制监听的socket的个数;循环检测(水平触发)
epoll: 不限制监听的socket的个数;回调方式(边缘触发)
import select import socket key_list = ['sx', 'sb', 'db'] class Rep(object): ''' 将socket封装,使新的对象拥有一个回调函数 ''' def __init__(self, sk, func): ''' :param sk: socket对象 :param func: 回调函数 ''' self.sk = sk self.func = func def fileno(self): ''' 由于select模块是调用fileno来获取socket的唯一标识,在对象中定义一个fileno供select调用 :return: ''' return self.sk.fileno() class Nb(object): def __init__(self, url, key_list): self.socket = [] self.conn = [] self.url = url self.key_list = key_list def add(self, url, func): ''' 创建socket :param url: :param func: :return: ''' client = socket.socket() client.setblocking(False) #设置线程非阻塞, 实现非阻塞, try: client.connect((url, 80)) # 设置非阻塞在connect和recv连接是会抛出异常,此处暂时捕获不用处理 except BlockingIOError as e: pass obj = Rep(client, func) #对socket进行封装 self.socket.append(obj) #要检测的 self.conn.append(obj) def run(self): ''' 使用select来进行socket监控并进行处理,select中由于遇到io操作就会进行切换,实现并发 :return: ''' while True: # select.select的原理是循环检测self.socket, self.conn, []这三个列表中的socket状态,如果列表中的数据发生变化则把变化的数据返回 # r_list, w_list, e_list r_list为已经返回数据的socket的列表,socket状态为可读,w_list为socket连接成功的列表,socket状态为可写,e_list为socket出现异常的列表 r_list, w_list, e_list = select.select(self.socket, self.conn, [], 0.05) # 最多最多检测0,05秒 for el in w_list: # 对已连接的socket进行发送数据操作 el.sk.sendall( ('GET /s?wd=%s HTTP/1.0\r\nhost:%s\r\n\r\n' % (self.key_list.pop(0), self.url)).encode('utf-8')) self.conn.remove(el) # 连接操作已完成,从conn列表中删除 for item in r_list: # 对已经返回数据的socket进行接收数据 chunk_list = [] while True: try: # 由于socket设置了非阻塞,这里也要捕获异常 chunk = item.sk.recv(8096) if not chunk: break chunk_list.append(chunk) except BlockingIOError as e: pass body = b''.join(chunk_list) # 获取到结果 item.func(body) # 执行回调函数,完成异步 self.socket.remove(item) # 接收数据操作已完成,从socket列表中删除 if not self.socket: break def func(body): #回调函数 print(body) t = Nb('www.baidu.com', key_list) for i in key_list: t.add('www.baidu.com', func) t.run()
协程