并行关键看是否能同时处理多个任务.依靠多核,同一时间每个CPU上执行一个任务
并发关键看是否能在一段时间内处理多个任务并不要求同时,
他俩最本质的特点就是是否能同时处理多个任务.无多核无并行
并行指的是多个CPU,并发主要是针对一个CPU而已,多个任务在一个CPU上交替切换任务。
并发的目的:充分利用处理器的每一个核,达到最高的处理性能。
协程线程进程的区别
协程也被称为微线程,下面对比一下协程和线程:
- 线程之间需要上下文切换成本相对协程来说是比较高的,尤其在开启线程较多时,但协程的切换成本非常低。
- 同样的线程的切换更多的是靠操作系统来控制,而协程的执行由我们自己控制
- 不需要线程的的锁机制
进程 数据隔离 数据不安全 切换开销最大 操作系统控制 能用多核
线程 数据共享 数据不安全 切换开销大 操作系统控制 不能用多核
协程 数据共享 数据安全 切换开销小 用户控制 不能用多核
协程
引用官方说法:
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
并发的本质:就是多个任务在一条线程中来回切换并保存状态,来实现一条线程上的io降到最低
线程和进程都是靠操作系统来控制切换+保存状态的,而协程的是靠用户来切换和保存状态。
既然需要我们自己来控制切换和保存状态,我们的yield的关键字可以解决这个问题,但是有一个关键的问题是:什么时候切换?只有遇到io切换才能提高程序效率,
所以协程最关键是用来解决单线程实现并发的io问题的。
协程也被称为''微线程'',协程并不是真实存在的,是程序员自己命名的
cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制),一种情况是该任务发生了阻塞,另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它
协程的本质
协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升单线程的执行效率。为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:
#1. 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行。 #2. 作为1的补充:可以检测io操作,在遇到io操作的情况下才发生切换
协程的作用
1.减少任务切换的开销。
2.实现单线程下并发。单线程并发要解决的核心是,解决io,遇到io切换才能提高效率,而对于线程和进程来说io问题已经被操作系统解决了,然后协程需要我们自己来解决io。
3.最主要的问题是解决io,提高了单线程的执行效率
协程的优点
1.切换开销小,速度快
2.不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多
3.可以实现单线程下的并发
协程的缺点
-
协程的本质是单线程下,无法利用多核.
-
协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
线程相当于协程来说的一个优点?
我们知道线程是靠操作系统的来规避io的,协程是靠用户规避io的,但是用户不能规避所有的io,一些文件操作的io(比如print)线程感知比协程更敏感.目前为止协程能感知到的io有请求网络
greenlet
官网https://greenlet.readthedocs.io/en/latest/
greenlet是一个轻量级的并发编程模块。它实现了并发但是没有实现协程,因为协程要求遇到io才切换。
官网例子:
from greenlet import greenlet def test1(): print(12) gr2.switch() #切换到gr2greenlet当调用test2函数 print(34) def test2(): print(56) gr1.switch() print(78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch() #当绑定的函数有参数时,从这里传进去
我们首先创建两个greenlet并分别绑定text1和text2,然后执行switch切换到gr1然后执行text1函数打印出12,然后再切换到gr2打印56,然后再切换到gr1因为greenlet能保存状态所以执行print(78)这个代码打印出34,text结束由于此时没有切换到gr2 78不会被打印出来
结果
12 56 34
greenlet 的模块和yield并没有什么区别,在协程中只是单纯的来切换任务,并没有解决效率问题,只有遇到io的时候切换才会有效率
如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦(需要先得到初始化一次的生成器,然后再调用send。。。非常麻烦),而使用greenlet模块可以非常简单地实现这20个任务直接的切换
from greenlet import greenlet import time def eat(name): print('%s eat 1' %name) time.sleep(1) g2.switch('egon') print('%s eat 2' %name) g2.switch() def play(name): print('%s2 play 1' %name) g1.switch() print('%s2 play 2' %name) g1=greenlet(eat) g2=greenlet(play) g1.switch('egon')#可以在第一次switch时传入参数,以后都不需要
结果:
egon eat 1 egon2 play 1 egon eat 2 egon2 play 2
总结:greenlet只是提供了一种比生成器更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。
gevent
Gevent 是一个第三方库,可以通过gevent实现协程,进而实现并发编程,在genent中用到的主要模块就是greenlet,该模块用c语言写的,它比greenlet模块多了一个可以检测出io再切换
主要思想是:
当一个greenlet遇到io操作时,比如说访问网站,就自动切换到其他的greenlet,等待io操作完成再在适当的时候切换回来继续执行,由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
作用:
- 切换+保存状态
- 自动能检测到io
- 遇到io切换任务
gevent可以规避哪些io
def patch_all(socket=True, dns=True, time=True, select=True, thread=True, os=True, ssl=True, httplib=False, # 这里就表明了不支持网络io也就是说请求网页的io subprocess=True, sys=False, aggressive=True, Event=True, builtins=True, signal=True, queue=True, **kwargs):
from gevent import monkey monkey.patch_all() #这个方法就是为了监听代码下边的所有的io操作,必须写在代码的开头 import gevent import time def eat(name): print('%s eat 1' %name) gevent.sleep(2) #其实就是genvent.sleep可以识别io操作,有io后直接切换到下一个任务 print('%s eat 2' %name) return 'eat' def play(name): print('%s play 1' %name) gevent.sleep(1) print('%s play 2' %name) return 'play' start=time.time() g1=gevent.spawn(eat,"小红") #创建协程对象,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的 g2=gevent.spawn(play,'小明') #这也是异步提交 g1.join()#等待g1完成 g2.join()#等待g完成 #或者把上面两步何为一步 gevent.joinall([g1,g2]) print("主线程开始运行",time.time()-start) print(g1.value,g2.value) #拿到上述函数的返回值
结果:
小红 eat 1 小明 play 1 小明 play 2 小红 eat 2 主线程开始运行 2.003354787826538 eat play
gevent 的应用
例子1爬虫
from gevent import monkey;monkey.patch_all() import gevent import requests import time def get_page(url): print('GET: %s' %url) response=requests.get(url) if response.status_code == 200: print('%d bytes received from %s' %(len(response.text),url)) start_time=time.time() gevent.joinall([ gevent.spawn(get_page,'https://www.python.org/'), gevent.spawn(get_page,'https://www.yahoo.com/'), gevent.spawn(get_page,'https://github.com/'), ]) stop_time=time.time() print('run time is %s' %(stop_time-start_time))
结果:
59631 bytes received from https://github.com/ 499780 bytes received from https://www.yahoo.com/ 48823 bytes received from https://www.python.org/ run time is 1.8453788757324219
例子2 socket的服务端和客户端的并发
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) 服务端 服务端
#_*_coding:utf-8_*_ __author__ = 'Linhaifeng' from socket import * client=socket(AF_INET,SOCK_STREAM) client.connect(('127.0.0.1',8080)) while True: msg=input('>>: ').strip() if not msg:continue client.send(msg.encode('utf-8')) msg=client.recv(1024) print(msg.decode('utf-8')) 客户端
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() 多线程并发多个客户端
asyncio
asyncio
是Python 3.4版本引入的标准库,直接内置了对异步IO的支持。
asyncio
的编程模型就是一个消息循环。我们从asyncio
模块中直接获取一个EventLoop
的引用,然后把需要执行的协程扔到EventLoop
中执行,就实现了异步IO。
import asyncio async def func(): """模拟请求网页""" print('start请求网页') await asyncio.sleep(1) # await 用在会发生阻塞的代码前,await必须写在一个async函数中 print("end请求网页") async def func2(): """ 模拟解析网页 :return: """ print("开始解析网页") await asyncio.sleep(2) print("结束解析网页") loop = asyncio.get_event_loop() # loop.run_until_complete(func()) # 一个任务时 loop.run_until_complete(asyncio.wait([func(), func2()])) # 多个任务时
结果:
start请求网页
开始解析网页
end请求网页
结束解析网页