Python并发编程

+ 协程

协程,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:

  1. Monkey-patching,我们都叫猴子补丁,因为如果使用了这个补丁,Gevent直接修改标准库里面大部分的阻塞式系统调用,包括socket、ssl、threading和 select等模块,而变为协作式运行。但是我们无法保证你在复杂的生产环境中有哪些地方使用这些标准库会由于打了补丁而出现奇怪的问题
  2. 第三方库支持。得确保项目中用到其他用到的网络库也必须使用纯Python或者明确说明支持Gevent
  3. Greenlet不支持Jython和IronPython,这样就无法把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操作发生时,它会经历两个阶段:

  1. 等待数据准备 (Waiting for the data to be ready)(就是将数据放到内核缓存中)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

基于这两步,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.coroutineyield 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密集型,且没有其它不能选择多进程方式的因素,都统一直接上多进程模式。

 

  1. aiohttp。一个实现了PEP3156的HTTP的服务器,且包含客户端相关功能。最早出现,应该最知名。
  2. sanic。后起之秀,基于Flask语法的异步Web框架。
  3. uvloop。用Cython编写的、用来替代asyncio事件循环。作者说「它在速度上至少比Node.js、gevent以及其它任何Python异步框架快2倍」。
  4. ujson。比标准库json及其社区版的simplejson都要快的JSON编解码库。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值