python进阶之 协程编程

1.协程概念 


首先来回顾一下线程和进程!   进程:是操作系统分配资源的的最小单位   线程:是cpu调度的最小单位 按道理来说我们已经算是把cpu的利用率提高很多了。但是我们知道无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。

随着我们对于效率的追求不断提高,基于单线程来实现并发又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。
偶操作系统来
首先我们要知道并发的本质:切换+保存状态
cpu正在运行一个任务,会在两种情况下去切换走执行其他的任务(线程进程切换有操作系统来完成)
  1.一种情况是该任务发生阻塞
  2.另外一种情况是该任务计算的时间过长或有一个优先级更高的程序替代了它
  (下图也是进程的切换原理,但是线程才是执行单位,所以下图也是线程的三种状态)


对于单线程下,我们不可避免程序中出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。

协程的本质就是单线程,由用户自己控制遇到io阻塞了就切换另外一个任务执行,以此来提升效率。为了实现它,我们需要寻找一种可以同时满足一下条件的解决方案:
  1.可以控制多任务之间切换,切换之间将任务保存下来啊,以便重新运行,可以基于暂停的位置继续执行
  2.作为1的补充,可以检测io操作,在遇到io操作的情况下才发生

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

2.认识协程

协程:单线程下的并发,又叫微线程
一句话说明什么是协程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的
需要注意的是:
  1.python中的进程和线程是属于内核级别的,由操作系统控制(如单线程遇到io或者执行时间过长就会被迫交出cpu执行权限)
  2.单线程内开启协程,一旦遇到io,就会冲应用程级别(并非操作系统)控制切换,依次来提升效率(非io操作的切换与效率无关)

协程的优点:
  1.协程的切换开销更小,属于程序级别的切换,操作系统完全感觉不到,因而更加轻量级
  2.单线程内就可以实现并发效果,更大限度的利用cpu
  3.就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。
协程的缺点:
  1.协程本质是单线程,无法利用多核,可以利用多个程序开启多个进程,每个进程内开启线程,每个线程内开启协程
  2.协程指的是单个线程,因而更容易出现阻塞,将会阻塞整个线程
协程的特点:
  1.必须在单个进程里面实现并发
  2.修改数据不需要加锁
  3.用户程序里面保存多个控制流的上下文栈
  4.一个协程遇到io操作自动切换到其他协程(如何检测io,yiled,greenlet都无法实现,就用到了gevent模块)
出现的意义 : 
  1.多个任务中的IO时间可以共享,当执行一个任务遇到IO操作的时候,
2.可以将程序切换到另一个任务中继续执行
3.在有限的线程中,实现任务的并发,节省了调用操作系统创建\销毁线程的时间
4.并且协程的切换效率比线程的切换效率要高
5.协程执行多个任务能够让线程少陷入阻塞,让线程看起来很忙
6.线程陷入阻塞的次数越少,那么能够抢占CPU资源就越多,你的程序效率看起来就越高
协程数据安全么??
  协程数据是绝地安全的,因为都是在线程内部的进行数据交换的,也不会走到机器码,没有什么原子性的规矩,就是早用户级别的切换数据和共享数据

 3.协程模块

注意:单纯的切换(在没有io的情况下或者没有重复开辟内存空间的操作),反而会降低程序的执行速度
安装:pip inastll greenlet
import time
from greenlet import greenlet   # 协程模块
def eat():    # 协程任务 协程函数
    print('start eating')
    g2.switch()
    time.sleep(1)
    print('end eating')
    g2.switch()

def sleep():  # 协程任务 协程函数
    print('start sleeping')
    g1.switch()
    time.sleep(1)
    print('end sleeping')

g1 = greenlet(eat)
g2 = greenlet(sleep)
#g1.switch() 
#因为协程格式遇到io操作后自动切换,所以在不加g1.switch()的话会不执行
greenlet实现状态切换
greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

单线程里的这20个任务的代码通常会既有计算操作又有阻塞操作,我们完全可以在执行任务1时遇到阻塞,就利用阻塞的时间去执行任务2。。。。如此,才能提高效率,这就用到了Gevent模块

安装:pip3 install gevent

Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

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的返回值
gevent用法
from gevent import monkey
monkey.patch_all()
#gevent能够识别的io阻塞的情况很少,加上上面两句是在告诉gevent要识别全部的io操作,可以点到patch_all()里面看gevent支持的io阻塞类型
import time
import gevent
import threading

def eat():    # 协程任务 协程函数
    print('start eating')
    time.sleep(1)
    print('end eating')

def sleep():  # 协程任务 协程函数
    print('start sleeping')
    time.sleep(1)
    print('end sleeping')

g1 = gevent.spawn(eat)   # 创建协程
g2 = gevent.spawn(sleep)
gevent.joinall([g1,g2])  # 阻塞 直到协程任务结束


#我们可以用threading.current_thread().getName()来查看每个g1和g2,查看的结果为DummyThread-n,即假线程
gevent遇到io切换

下载源码

# 请求网页
url_dic = {
    '协程':'http://www.cnblogs.com/Eva-J/articles/8324673.html',
    '线程':'http://www.cnblogs.com/Eva-J/articles/8306047.html',
    '目录':'https://www.cnblogs.com/Eva-J/p/7277026.html',
    '百度':'http://www.baidu.com',
    'sogou':'http://www.sogou.com',
    '4399':'http://www.4399.com',
    '豆瓣':'http://www.douban.com',
    'sina':'http://www.sina.com.cn',
    '淘宝':'http://www.taobao.com',
    'JD':'http://www.JD.com'
}

import time
from gevent import monkey;monkey.patch_all()
from urllib.request import urlopen
import gevent

def get_html(name,url):
    ret = urlopen(url)
    content = ret.read()
    with open(name,'wb') as f:
        f.write(content)

start = time.time()
for name in url_dic:
    get_html(name+'_sync.html',url_dic[name])
ret = time.time() - start
print('同步时间 :',ret)

start = time.time()
g_l = []
for name in url_dic:
    g = gevent.spawn(get_html,name+'_async.html',url_dic[name])
    g_l.append(g)
gevent.joinall(g_l)
ret = time.time() - start
print('异步时间 :',ret)
gevent应用举例

 通过gevent实现单线程下的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)
server
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'))

client
client
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()
多线程并发多个客户端

线程里面起协程

import os
from gevent import monkey;monkey.patch_all()
import time
import random
import gevent
from threading import Thread
class Sayhi(Thread):
    def __init__(self,name):
        super().__init__()
        self.name = name

    @staticmethod
    def func(item):
        time.sleep(random.uniform(1,2))
        print('协程:sayhi %s  %s'%(item,os.getpid()))

    def run(self):
        count = 20
        l = []
        for i in range(count):
            g = gevent.spawn(self.func,i)
            l.append(g)
        gevent.joinall(l)
if __name__ == '__main__':
    p = Sayhi('kobe')
    p.start()
    p.join()
    print(os.getpid())
线程里面开启协程

 

协程误区

任务的执行过程叫做协程,协程是用来对任务进行监控,遇到io就切换,其实并没有协程,线程,但是确实有进程,可以查看进程pid
协程里面的数据是安全的,不需要加锁
进程用来获取资源,协程监控任务io,线程用来具体执行

我的理解:
  为什么会出现协程?
    在单线程下,如果任务遇到io阻塞(当进程cpu时间片到期或者更高级别的任务来临,cpu都会切走),
    那么当前任务就会被挂起,cpu切走去执行其他任务,那么为了让cpu能够'尽可能长时间的来运行我们的任务,减少在io阻塞时,cpu切走的概率',我们可以在线程内部创建一个新的东西,
    在用户级别监控当多任务执行时,如果其中一个任务遇到io阻塞时,在协程在阻塞的这段时间内去执行其他不阻塞的任务。来达到长时间使用cpu的目的,从而提高任务效率。

那我们要如何理解协程那?
  在我看来,协程和cpu做的事情是一样的,cpu在执行任务时,遇到io阻塞,会保存当前线程/进程的程序计数器,内粗信息等,然后切换到另一任务去执行
  网上说“协程是比线程更小的执行单位,为啥说他是执行单位,因为他有和cpu一样的上下文切换,可以从一个协程切换到另一个协程,只要这个协程保存着上一次的执行状态,就可以继续执行任务”
  但是我觉得协程可能更像是一种监控任务监工,当这个监工遇到任务阻塞的时候,就会记录这个任务的当前信息,保存起来,切换到另一个不阻塞的任务,等该任务不阻塞时候,下一次继续调用这个任务。
  这个作用和python里面一个关键字特别像,yiled。
  yiled  yiled

 协程 协程  协程

 

 

返回系列

posted @ 2019-04-22 10:48  thep0st  阅读(38)  评论(0)    收藏  举报