python开发基础篇:七:协程简介

1:协程简介:https://www.cnblogs.com/Eva-J/articles/8324673.html

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

效率的追求不断提高,基于单线程来实现并发效果
对于线程来说始终是一个线程,在一个线程之间切换(切换速度快,不涉及锁和数据切换的问题,数据都在一个寄存器和栈里)

协程的切换与正常程序的执行:
使用yield迭代器实现两个程序交叉切换:两个函数交替实现的效果
def func1():
    print(1)
    yield
    print(3)
    yield
def func2():
    g = func1()
    next(g)
    print(2)
    next(g)
    print(4)
func2()
使用生成器模拟生产者消费者模型:
def consumer():
    while 1:
        n = yield
        print(f"消费了数据:{n}")
def producer():
    g = consumer()
    next(g)
    for i in range(10):
        print(f"生产了数据:{i}")
        g.send(i)
producer()
一个线程之间两个任务切换执行,yield自带保存函数执行的状态,实现上下文的切换
pip安装greenlet模块:在一个单线程中提供切换状态的模块
pip安装Gevent模块
greenlet模块实现交互切换
from greenlet import greenlet
def eat1():
    print("吃鸡腿1")
    g2.switch()
    print("吃鸡腿2")
    g2.switch()
def eat2():
    print("吃饺子1")
    g1.switch()
    print("吃饺子2")
g1 = greenlet(eat1)
g2 = greenlet(eat2)

g1.switch()  # 在哪个地方switch就在哪个地方转
打印:
    吃鸡腿1
    吃饺子1
    吃鸡腿2
    吃饺子2
gevent内部也是这么做切换的,gevent切换内部封装的就是greenlet模块,greelet是底层模块
暂时这个实例没有规避到等待和I/O等待的时间
greenlet模块只能做切换,并不能规避掉程序当中的I/O时间
yiled可以保存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级
send可以把一个函数的结果传给另外一个函数,以此实现单线程内程序之间的切换

单纯地切换反而会降低运行效率
#串行执行 import time def consumer(res): '''任务1:接收数据,处理数据''' pass def producer(): '''任务2:生产数据''' res=[] for i in range(10000000): res.append(i) return res start=time.time() #串行执行 res=producer() consumer(res) #写成consumer(producer())会降低执行效率 stop=time.time() print(stop-start) #1.5536692142486572 #基于yield并发执行 import time def consumer(): '''任务1:接收数据,处理数据''' while True: x=yield def producer(): '''任务2:生产数据''' g=consumer() next(g) for i in range(10000000): g.send(i) start=time.time() #基于yield保存状态,实现两个任务直接来回切换,即并发的效果 #PS:如果每个任务中都加上打印,那么明显地看到两个任务的打印是你一次我一次,即并发执行的. producer() stop=time.time() print(stop-start) #2.0272178649902344

在代码之间切换执行反而会降低效率,基于yield需要保存状态(
greelet更慢),实现两个任务的切换,更耗费时间,保存状态再去切换-----切换反而让效率更低了

如果在同一个程序中有I/O的情况下才切换的话会让效率提供很多,
  yield和greenet都不能在切换的时候  规避I/O时间

Gevent:能够规避I/O时间的模块,协程模块

 2:gevent协程实例

import gevent
def func1():
    print(1)
    gevent.sleep(1)  # gevent.sleep和time.sleep效果类似,都是睡1s
    print(2)
def func2():
    print(3)
    gevent.sleep(1)
    print(4)
g1 = gevent.spawn(func1)    # 发布任务并且执行
g2 = gevent.spawn(func2)
g1.join()
g2.join()
gevent.sleep(5)

g1任务和g2任务和主协程任务相当于3个任务完全异步,所以需要g1.join和g2.join阻塞主协程
gevent:遇见他认识的io会自动切换的模块
  gevent.sleep:自带的sleep方法,他自己认识的I/O阻塞,time.sleep他就不认识了他就不会切换的
join和joinall的使用
import gevent
def func1():
    print(1)
    gevent.sleep(1)  # gevent.sleep和time.sleep效果类似,都是睡1s
    print(2)
def func2():
    print(3)
    gevent.sleep(1)
    print(4)
g1 = gevent.spawn(func1)    # 发布任务并且执行
g2 = gevent.spawn(func2)
# g1.join()
# g2.join()
gevent.joinall([g1, g2])

g1.join()+join()    等同    event.joinall([g1, g2])
gevent自带的一些I/O他就认识,不自带的I/O操作就不认识,
I/O操作:sleep,socket,网络url请求(requests和urllib等)一般都是这些I/O操作
引入这些I/O模块之前先导入monkey模块
    from gevent import monkey
    monkey.patch_all()
    这两行代码会把下面的导入的所有模块的I/O都打成一个包,就认识sleep,和socket这些I/O操作了,
  这两行代码必须写前面,比如想识别time模块的I/O time.sleep必须写在import time模块的前面,顶行写就行 from gevent import monkey;monkey.patch_all() import gevent import time def func1(): print(1) time.sleep(1) print(2) def func2(): print(3) time.sleep(1) print(4) g1 = gevent.spawn(func1) # 发布任务并且执行 g2 = gevent.spawn(func2) gevent.joinall([g1, g2]) 现在只能识别gevent内置模块和导入模块的一些I/O 现在遇到的主要的I/O是:time模块,socket模块,urllib和request网络请求,open打开文件(需要操作系统打开给一个文件句柄) input和打开文件是内置的方法,from gevent import monkey;monkey.patch_all()这行代码无法让他遇到input和文件操作I/O跳转,只能作用于导入的包里面的I/O

gevent封装了greenlet的模块在内部帮他实现切换工作,gevent在切换程序的基础上又实现了规避I/O
from gevent import monkey;monkey.patch_all()
import time
import gevent
from threading import currentThread
def func1():
    print(currentThread().name, currentThread())  # Dummy-1
    print(1)
    time.sleep(1)
    print(2)
def func2():
    print(currentThread().name, currentThread())  # Dummy-2
    print(3)
    time.sleep(1)
    print(4)
g1 = gevent.spawn(func1)  # 发布任务并且执行
g2 = gevent.spawn(func2)
gevent.joinall([g1, g2])

currentThread().name查看线程名字打印:
    Dummy-1和Dummy-2
    Dummy:虚设的,假的,虚拟假设的线程,实际上 Dummy-1和Dummy-2还在同一个线程之内的
协程执行和同步程序执行的效率对比
from gevent import monkey;monkey.patch_all()
import time
import gevent
def task(args):
    time.sleep(1)
    print(args)
def sync_func():  # 同步
    for i in range(10):
        task(i)
def async_func():  # 异步
    g_l = []
    for i in range(10):
        g_l.append(gevent.spawn(task, i))  # 发起一个协程任务,给协程任务传参数
    gevent.joinall(g_l)
start_time = time.time()
sync_func()
print(time.time() - start_time)     # 10.118812084197998
start_time = time.time()
async_func()
print(time.time() - start_time)     # 1.0129859447479248

有I/O的情况下使用协程效果比较明显,整体程序都是在一个线程内执行的
I/O在程序当中对程序效率影响很大

3:使用协程爬取网页  

from gevent import monkey;monkey.patch_all()  # 让request里面的I/O阻塞能够被捕捉到
import gevent
import requests
import time

"""
爬取网页
10个网页
协程函数去发起10个网页的爬取任务:
    10个网页get url需要一定时间响应,协程函数一个网页去爬取过程当中遇到网络延时或者等待I/O就马上去执行下一个任务了
    上面的网页等着,而下面的网页可以开始都执行上了,这样可以复用一些I/O时间,协程的意义
"""
url_lis = [
    "https://www.baidu.com",
    "https://www.sogou.com",
    "https://www.hao123.com",
    "https://www.jd.com",
    "https://www.taobao.com",
    "https://www.sohu.com",
    "https://www.qq.com",
    "https://www.python.org",
    "https://www.cnblogs.com",
    "https://www.apache.org/",
]
def get_url(url):
    res = requests.get(url)  # 这里可以复用等待时间,发起get请求就陷入I/O等待了
    # return url, res.status_code, len(res.text)
    print(url, res.status_code, len(res.text))
start_time
= time.time() for url in url_lis: ret = requests.get(url) print(url, ret.status_code, len(ret.text)) print(time.time() - start_time) # 2.8345634937286377 start_time = time.time() g_lis = [] for url in url_lis: g_lis.append(gevent.spawn(get_url, url)) gevent.joinall(g_lis) print(time.time() - start_time) # 1.0743591785430908 协程发送get请求不等结果(识别到这个地方有网络延时),马上切换到另外一个任务,利用等待的时间 协程在执行多个任务,多I/O的情况下才能有优势

4:使用gevent协程实现socket聊天

server.py
from gevent import monkey;monkey.patch_all()
import gevent
import socket
def talk(conn):
    while 1:
        ret = conn.recv(1024).decode("utf8")
        print(ret)
        conn.send(ret.upper().encode("utf8"))
    conn.close()
sk = socket.socket()
sk.bind(("127.0.0.1", 9999))
sk.listen()
while 1:
    conn, addr = sk.accept()
    gevent.spawn(talk, conn)    # 聊天任务放到线程,进来一个链接开启一个协程去聊天
sk.close()

基于一个线程的协程,能同时接收多个客户端链接
协程的开启毫无压力:一个协程可以接收500个并发甚至更多
协程数目:(cpu_count + 1)*cpu_count*5*500    这样一台4核机器接收5w的并发

gevent:
    1:处理导入进来的模块中间存在I/O的情况才能帮忙节省时间(网络延迟相关的,socket相关的这些都能识别到)
        一般爬虫和网络编程用到协程比较多
    2:
client.py
import socket
import time
import threading
def my_client():
    sk = socket.socket()
    sk.connect(("127.0.0.1", 9999))
    while 1:
        sk.send(b"hi")
        ret = sk.recv(1024).decode("utf8")
        print(ret)
        time.sleep(1)
    sk.close()
for i in range(500):
    t = threading.Thread(target=my_client).start()

5:域名

1:域名比较好记
2:服务端后端服务几百台机器,不知道具体访问哪个机器的ip,只知道具体访问的域名
nginx:反向代理
客户端请求先访问到一个服务器上(一组服务器),nginx也可能有几台机器跑的都是nginx,请求访问到其中的一台
  niginx机器就跑一个服务,他来做转发,比如客户端某个机器访问www.baidu.com/zhishi这个域名,
  其实是访问nginx机器上,niginx机器根据请求后面的参数什么的找到真正对应的机器,对应服务的机器也可能很多台
  nginx随机分配一台(内部自己的算法分配,负载均衡),消息的转发,一般代理的机器少,后台服务的机器多,
  代理管理着很多项目做转发,nginx反向代理一台机器接收处理的并发送需要支持5w个(nginx运用:进程+线程+协程),
  对一条线程在I/O处理上做了复用,类似协程,nginx对I/O处理很好所以能接收高并发,几个代理就能把服务器的资源分配都做好
nginx:利用高并发编程十分优秀的程序

 

 

posted @ 2022-02-12 15:30  至高无上10086  阅读(670)  评论(0编辑  收藏  举报