基于协程的并发

一、什么是协程

协程是单线程下的并发,又称微线程,纤程。协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。我们需要知道的是:

#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
#2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(非io操作的切换与效率无关)

协程的特点是:

  • 必须在只有一个单线程里实现并发;
  • 修改共享数据不需加锁;
  • 用户程序里自己保存多个控制流的上下文栈
  • 一个协程遇到IO操作自动切换到其它协程

二、Greenlet

如果我们在单个线程内有20个任务,要想实现在多个任务之间切换,使用yield生成器的方式过于麻烦,而使用greenlet模块可以非常简单地实现这20个任务直接的切换。

#安装
pip3 install greenlet
from greenlet import greenlet

def test1():
    print(12)
    gr2.switch()   #切换到gr2
    print(34)
    gr2.switch()   #切换到gr2

def test2():
    print(56)
    gr1.switch()   #切换到gr1
    print(78)

gr1 = greenlet(test1)
gr2 = greenlet(test2)
gr2.switch()      #切换到gr2

单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度。greenlet当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。如此,才能提高效率,这就用到了Gevent模块。

三、Gevent

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程。

具体使用如下:

#用法
g1=gevent.spawn(func,1,,2,3,x=4,y=5)创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

g2=gevent.spawn(func2)

g1.join() #等待g1结束

g2.join() #等待g2结束

#或者上述两步合作一步:gevent.joinall([g1,g2])

g1.value#拿到func1的返回值

遇到IO阻塞时会自动切换任务

import gevent
import requests,time

start = time.time()

def f(url):
    print('GRT:%s'%url)
    resp = requests.get(url)
    data = resp.text
    print('%d bytes received from %s'%(len(data),url))

gevent.joinall([
    gevent.spawn(f,'http://www.python.org/'),
    gevent.spawn(f,'http://www.yahoo.com/'),
    gevent.spawn(f,'http://www.baidu.com/'),
    gevent.spawn(f,'http://www.sina.com.cn/'),

])
# f('http://www.python.org/')
# f('http://www.yahoo.com/')
# f('http://www.baidu.com/')
# f('http://www.sina.com.cn/')

print('cost time:',time.time()-start)
gevent

注释部分是串行执行,在网速稳定情况下,明显使用gevent模块比串行执行效率高很多。

我们可以通过gevent实现单线程下的socket并发:

from gevent import monkey;monkey.patch_all()  #from gevent import monkey;monkey.patch_all()一定要放到导入socket模块之前,否则gevent无法识别socket的阻塞
from socket import *
import gevent

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)
服务端
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()
多线程并发多个客户端
02-12 15:26