+ 协程
协程,Coroutine,是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方(非CPU),在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,CPU感觉不到协程的存在,协程是用户自己控制的。
协程实现原理:利用一个线程,分解一个线程成为多个“微线程”,属于程序级别的划分。
协程的优点:
无需线程上下文切换的开销
无需数据操作锁定及同步的开销
方便切换控制流,简化编程模型
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理
缺点:
无法利用多核资源:协程的本质是个单线程,它不能同时将单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上,协程如果要使用多核CPU的话,那么就需要先启多个进程,在每个进程下启一个线程,然后在线程下在启协程。
日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
python通过yield提供对协程的基本支持,但是不完全,第三方库gevent为协程提供了比较完善的支持。
从句法上看,协程与生成器类似,都是定义体中包含 yield 关键字的函数。可是,在协程中, yield 通常出现在表达式的右边(例如, datum = yield),可以产出值,也可以不产出 —— 如果 yield 关键字后面没有表达式,那么生成器产出 None。
参考网址:https://blog.csdn.net/andybegin/article/details/77884645
协程可能会从调用方接收数据,不过调用方把数据提供给协程使用的是 .send(datum) 方法,而不是next(…) 函数。
def simple_coroutine():
print('-> start')
x = yield
print('-> recived', x)
sc = simple_coroutine()
next(sc)
sc.send('zhexiao')
1. 协程使用生成器函数定义:定义体中有 yield 关键字。
2. yield 在表达式中使用;如果协程只需从客户那里接收数据,那么产出的值是 None —— 这个值是隐式指定的,因为 yield 关键字右边没有表达式。
3. 首先要调用 next(…) 函数,因为生成器还没启动,没在 yield 语句处暂停,所以一开始无法发送数据。
4. 调用send方法,把值传给 yield 的变量,然后协程恢复,继续执行下面的代码,直到运行到下一个 yield 表达式,或者终止。
==注意:send方法只有当协程处于 GEN_SUSPENDED 状态下时才会运作,所以我们使用 next() 方法激活协程到 yield 表达式处停止,或者我们也可以使用 sc.send(None),效果与 next(sc) 一样==。
协程可以身处四个状态中的一个。当前状态可以使用inspect.getgeneratorstate(…) 函数确定,该函数会返回下述字符串中的一个:
1. GEN_CREATED:等待开始执行
2. GEN_RUNNING:解释器正在执行
3. GEN_SUSPENED:在yield表达式处暂停
4. GEN_CLOSED:执行结束
import time
def consumer(name):
print('--->staring eating baozi....')
while True:
new_baozi = yield
print("%s is eating baozi %s" %(name,new_baozi))
def producer():
r = con.__next__()
r = con2.__next__()
n = 0
while n<5:
n += 1
print("chenkaifang is making baozi %s" %n)
con.send(n)
con2.send(n)
if __name__ == '__main__':
con = consumer("c1")
con2 = consumer("c2")
p = producer()
yield from高级用法....
greenlet有已经封装好的协程:
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()
参考网址:https://www.cnblogs.com/zhaof/p/7536569.html
Gevent是一种基于协程的Python网络库,它用到Greenlet提供的,封装了libevent事件循环的高层同步API。它让开发者在不改变编程习惯的同时,用同步的方式写异步I/O的代码。它的特点是协程的自动切换(遇到IO操作自动轮训切换)。
使用Gevent的性能确实要比用传统的线程高,甚至高很多。但这里不得不说它的一个坑,实际使用中要慎用Gevent:
gevent是第三方库,通过greenlet实现协程,其基本思想是:当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成。
gevent使用实例1:
from gevent import monkey;monkey.patch_socket()
import gevent
def f(n):
for i in range(n):
print(gevent.getcurrent(),i)
gevent.sleep(0)#让出CPU使用权
g1 = gevent.spawn(f,5)
g2 = gevent.spawn(f,5)
g3 = gevent.spawn(f,5)
g1.join()
g2.join()
g3.join()
gevent I/O操作实例1:
from gevent import monkey;monkey.patch_all()
import gevent
import urllib.request
def f(url):
print("GET:%s" %url)
resp = urllib.request.urlopen(url)
data = resp.read()
print("%d bytes received form %s" %(len(data),url))
gevent.joinall([
gevent.spawn(f,"https://www.python.org/"),
gevent.spawn(f,"https://www.yahoo.com/"),
gevent.spawn(f,"https://github.com/"),
])
+ 多线程
由于GIL的存在,即使硬件有N个核,也只能利用一个核。
ThreadLocal类型处理变量共享与绑定,与Java类似。
threading.Thread类的使用:
1,在自己的线程类的__init__里调用threading.Thread.__init__(self, name = threadname) Threadname为线程的名字
2, run(),通常需要重写,编写代码实现做需要的功能。
3,getName(),获得线程对象名称
4,setName(),设置线程对象名称
5,start(),启动线程
6,jion([timeout]),等待另一线程结束后再运行。
7,setDaemon(bool),设置子线程是否随主线程一起结束,必须在start()之前调用。默认为False。
8,isDaemon(),判断线程是否随主线程一起结束。
9,isAlive(),检查线程是否在运行中。
参考网址:
https://www.cnblogs.com/chengd/articles/7770898.html
https://www.cnblogs.com/semiok/articles/2640929.html
#Lock不允许同一线程多次acquire
import time
import threading
balance = 0
lock = threading.Lock()
def change(n):
global balance
balance = balance + n
balance = balance - n
def run_thread(n):
for i in range(10000):
lock.acquire()
try:
change(n)
finally:
lock.release()
t1 = threading.Thread(target = run_thread,args=(5,))
t2 = threading.Thread(target = run_thread,args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance)
Rlock允许同一线程多次acquire
threading.Condition,高级锁,内部维护一个锁对象,提供wait、notify、notifAll方法(必须在获得锁后才能调用)
threading.Semaphore BoundedSemaphore 信号量同步
thread.Event:事件处理机制
Queue:同步队列
threading.active_count threading.current_thread threading.enumerate threading.get_ident threading.main_thread
线程同步的4种方式:锁、信号量、条件变量、同步队列
+ 多进程
Windows下python用multiprocessing实现多进程,multiprocessing
模块不但支持多进程,其中managers
子模块还支持把多进程分布到多台机器上,一个服务进程可以作为调度者,将任务分布到其他多个进程中,依靠网络通信。由于managers
模块封装很好,不必了解网络通信的细节,就可以很容易地编写分布式多进程程序。
参考网址:https://blog.csdn.net/cityzenoldwang/article/details/78584175
import multiprocessing
import os
def run_proc(name):
print('Child process {0} {1} Running '.format(name, os.getpid()))
if __name__ == '__main__':
print('Parent process {0} is Running'.format(os.getpid()))
for i in range(5):
p = multiprocessing.Process(target=run_proc, args=(str(i),))
print('process start')
p.start()
p.join()
print('Process close')
Pool 可以提供指定数量的进程供用户使用,默认是 CPU 核数。当有新的请求提交到 Poll 的时候,如果池子没有满,会创建一个进程来执行,否则就会让该请求等待。
apply_async 方法用来同步执行进程,允许多个进程同时进入池子,apply只能允许一个进程进入池子,在一个进程结束之后,另外一个进程才可以进入池子。
进程间通信Queue、Pipe、Socket。基于消息传递的并发编程是大势所趋,通过消息队列交换数据。这样极大地减少了对使用锁定和其他同步手段的需求, 还可以扩展到分布式系统中。
multiprocessing给每个进程赋予单独的Python解释器,这样就规避了全局解释锁所带来的问题。
+ 异步IO
通常,我们写服务器处理模型的程序时,有以下几种模型:
(1)每收到一个请求,创建一个新的进程,来处理该请求;
(2)每收到一个请求,创建一个新的线程,来处理该请求;
(3)每收到一个请求,放入一个事件列表,让主进程通过非阻塞I/O方式来处理请求(就是以事件驱动的方式来处理)
上面的几种方式,各有千秋,
第(1)种方法,由于创建新的进程的开销比较大,所以,会导致服务器性能比较差,但实现比较简单。
第(2)种方式,由于要涉及到线程的同步,有可能会面临死锁等问题。
第(3)种方式,在写应用程序代码时,逻辑比前面两种都复杂。
综合考虑各方面因素,一般普遍认为第(3)种方式是大多数网络服务器采用的方式。
对于一次IO访问(以read举例),数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个read操作发生时,它会经历两个阶段:
基于这两步,Linux有5种网络模型:阻塞、非阻塞、I/O多路复用、信号驱动、异步IO。
IO multiplexing就是我们说的select,poll,epoll。I/O多路复用的特点是通过一种机制 一个进程能同时等待多个文件描述符,而这些文件描述符(socket连接)其中的任意一个进入读就绪状态,select()函数就可以返回。epoll同样只告知那些就绪的文件描述符,epoll采用基于事件的就绪通知方式,epool会告诉用户进程具体哪个socket连接有数据了,所以用户进程不需要在将所有socket 连接全都循环一次才发现具体哪个有数据。epoll解决C10K问题,采用mmap减少复制开销,epoll技术的编程模型就是异步非阻塞回调,也可以叫做Reactor、事件驱动、事件轮循(EventLoop)、libevent、Tornado、Node.js这些就是epoll时代的产物。
协程本质上也是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。进程/线程是操作系统充当了EventLoop调度,而协程是自己用epoll进行调度。
asyncio在python3.4被引入标准库,Python 3.5添加了async和await这两个关键字,分别用来替换asyncio.coroutine
和yield from,
协程成为新的语法,而不再是一种生成器类型了。
首先需要明确一点,asyncio使用单线程、单个进程的方式切换;现存的一些库其实并不能原生的支持asyncio(因为会发生阻塞或者功能不可用),比如requests,如果要写爬虫,配合asyncio的应该用aiohttp,其他的如数据库驱动等各种Python对应的库也都得使用对应的aioXXX版本了。
asyncio模块包含多种同步机制:信号量、锁、条件变量、事件、队列
import asyncio
async def hello1():
print("1,Hello World!")
r = await asyncio.sleep(1)
print("1,Hello again!")
for i in range(5):
print(i)
async def hello2():
print("2,Hello World!")
print("2,Hello again!")
for i in range(5):
print(i)
coros = [hello1(),hello2()]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.gather(*coros))
+ 同步、异步、阻塞、非阻塞
同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回。
异步:当一个异步过程调用发出后,调用者不会立刻得到结果。通过状态、通知来通知调用者,或通过回调函数处理这个调用。
单进程的异步编程模型称为协程。
阻塞调用是指调用结果返回之前,当前线程会被挂起。函数只有在得到结果之后才会返回。对于同步调用来说,很多时候当前线程还是激活的,只是从逻辑上当前函数没有返回而已。
非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
同步阻塞:效率最低,什么都不能干;异步阻塞;同步非阻塞也效率低;异步非阻塞效率高。
+ 进程、线程、协程应用场景
IO密集型:用多线程+gevent(更好), 多线程 (抛弃Gevent,用asyncio)
计算密集型:用多进程
如果要启动大量的子进程,可以用进程池的方式批量创建子进程。ThreadPoolExecutor和ProcessPoolExecutor
如果分不清任务是CPU密集型还是IO密集型,就用下面两个方法试,哪个快用哪个:
from multiprocessing import Pool
from multiprocessing.dummy import Pool
如果一个任务拿不准是CPU密集还是I/O密集型,且没有其它不能选择多进程方式的因素,都统一直接上多进程模式。