返回顶部

11 python 协程和异步io

并发、并行、同步、异步、阻塞、非阻塞

并发:是指一个时间段内,有几个程序在同一个cpu上运行,但是任意时刻只有一个cpu上运行

并行:是指任意时刻点上,有多个程序同时运行在多个cpu上

同步:是指代码调用IO操作时,必须等待IO操作完成才返回的调用方式

异步:是指代码调用IO操作时,不必等待IO操作完成就返回的调用方式

阻塞:是指调用函数时候当前线程被挂起

非阻塞:是指调用函数时候当前线程不会被挂起,而是立即返回

IO 多路复用 (select、poll 和 epoll)

C10K问题

如何在1Gcpu,2G内存,1g网络环境下,让单台服务器同时为1万个客户端提供FTP服务

 Unix下五中I/O模型

  阻塞式I/O

  非阻塞式I/O

  I/O多路复用

  信号驱动I/O

  异步I/O

                         阻塞式I/O

在等待数据返回的时候存在大量的时间等待过程

 

                        非阻塞式I/O

虽然在遇到I/O操作是不在阻塞,但是需要CPU不断的去询问是否已成功准备好,消耗大量CPU操作,在数据准备好后,把数据从内核赋值到用户空间还是要阻塞

 

 

             I/O多路复用

 虽然在遇到I/O操作时,他也是处于阻塞的状态,但是它和阻塞I/O的区别是,它可以监听多个socket,如果有一个socket的状态发生变化会立即返回处理,即把要监听的多个socket放到一个列表中监听

主要有3种模型:select poll epoll 

epoll并不代表一定比select好

  在并发高的情况下,连接活跃度不是很高, epoll比select
  并发性不高,同时连接很活跃, select比epoll好

                       异步IO

 遇到IO操作时立即返回,等待数据从内核赋值到用户内存空间后,在通知程序来执行,无IO等待时间

 

 

select+回调+事件循环获取html

非阻塞io虽然在遇到io操作时不会等待,但会消耗大量的cpu进行询问,在数据从内核赋值到用户空间还是有一定的等待时间。

和同步的等待时间没什么区别,但是它可以在阻塞的时候除了不停的询问外,还可以在别的事情

复制代码
#1. epoll并不代表一定比select好
# 在并发高的情况下,连接活跃度不是很高, epoll比select
# 并发性不高,同时连接很活跃, select比epoll好

#通过非阻塞io实现http请求

import socket
from urllib.parse import urlparse


#使用非阻塞io完成http请求

def get_url(url):
    #通过socket请求html
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    #建立socket连接
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.setblocking(False)
    try:
        client.connect((host, 80)) #阻塞不会消耗cpu
    except BlockingIOError as e:
        pass

    #不停的询问连接是否建立好, 需要while循环不停的去检查状态
    #做计算任务或者再次发起其他的连接请求

    while True:
        try:
            client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
            break
        except OSError as e:
            pass

    data = b""
    while True:
        try:
            d = client.recv(1024)
        except BlockingIOError as e:
            continue
        if d:
            data += d
        else:
            break

    data = data.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()

if __name__ == "__main__":
    get_url("http://www.baidu.com")
通过非阻塞io实现http请求
复制代码

 同步http请求和select+回调+时间循环请求对比

同步http请求20个url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import socket
from urllib.parse import urlparse
 
def get_url(url):
    #通过socket请求html
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"
 
    #建立socket连接
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 
    client.connect((host, 80)) #阻塞不会消耗cpu
 
    client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
 
    data = b""
    while True:
        d = client.recv(1024)
        if d:
            data += d
        else:
            break
 
    data = data.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()
 
if __name__ == "__main__":
    import time
    start_time = time.time()
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        get_url(url)
    print(time.time()-start_time)

 执行时间如下

select+回调+时间循环请求20个url

回调+事件循环+select(poll\epoll) 是目前各大框架底层常用的IO多路复用技术

1 事件循环,不停的请求socket的状态并调用对应的回调函数,通过loop事件循环  不断循环检测ready是否有返回值   ready = selector.select()  ready有返回值的话执行对应的回调函数 

2. select本身是不支持register模式, selectors在select进行了封装可以把socket注册到select中

  selector.register("socket文件类型", 事件类型(读/写), socket建立成功后的回调函数地址)

3. socket状态变化以后的回调是由程序员完成的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import socket
from urllib.parse import urlparse
from selectors import DefaultSelector, EVENT_READ, EVENT_WRITE
 
 
selector = DefaultSelector()
#使用select完成http请求
urls = []
stop = False
 
 
class Fetcher:
    def connected(self, key):
        selector.unregister(key.fd)
        self.client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(self.path, self.host).encode("utf8"))
        selector.register(self.client.fileno(), EVENT_READ, self.readable)
 
    def readable(self, key):
        d = self.client.recv(1024)
        if d:
            self.data += d
        else:
            selector.unregister(key.fd)
            data = self.data.decode("utf8")
            html_data = data.split("\r\n\r\n")[1]
            print(html_data)
            self.client.close()
            urls.remove(self.spider_url)
            if not urls:
                global stop
                stop = True
 
    def get_url(self, url):
        self.spider_url = url
        url = urlparse(url)
        self.host = url.netloc
        self.path = url.path
        self.data = b""
        if self.path == "":
            self.path = "/"
 
        # 建立socket连接
        self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.client.setblocking(False)
 
        try:
            self.client.connect((self.host, 80))  # 阻塞不会消耗cpu
        except BlockingIOError as e:
            pass
 
        #注册
        selector.register(self.client.fileno(), EVENT_WRITE, self.connected)
 
 
def loop():
    #事件循环,不停的请求socket的状态并调用对应的回调函数
    while not stop:
        ready = selector.select()
        for key, mask in ready:
            call_back = key.data
            call_back(key)
 
if __name__ == "__main__":
    fetcher = Fetcher()
    import time
    start_time = time.time()
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        urls.append(url)
        fetcher = Fetcher()
        fetcher.get_url(url)
    loop()
    print(time.time()-start_time)

 执行时间如下

 

 

协程是什么 

回调虽然有很到的性能,但是对于我们而言也面临了不少的问题

1 可读性差

2 共享状态管理困难

3 异常处理困难 

 我们即想使用回调的高性能,又想使用起来简单,所以就有了协程

 协成类似于一个可以暂停的函数(可以向暂停的地方传入值),可以在一个线程中进行来回切换

 

生成器进阶-send、close和throw方法

 send 传值给生成器 yield产出值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def gen_func():
    #1. 可以产出值, 2. 可以接收值(调用方传递进来的值)
    html = yield "http://projectsedu.com"
    print(html)
    return "bobby"
 
if __name__ == "__main__":
    gen = gen_func()
    #在调用send发送非none值之前,我们必须启动一次生成器, 方式有两种1. gen.send(None), 2. next(gen)
    url = gen.send(None)
    #download url
    html = "bobby"
    print(gen.send(html)) #send方法可以传递值进入生成器内部,同时还可以重启生成器执行到下一个yield位置
    print(gen.send(html))  # 抛出异常 StopIteration 

输出结果如下

close关闭生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def gen_func():
    #1. 可以产出值, 2. 可以接收值(调用方传递进来的值)
    try:
        yield "http://projectsedu.com"
    except RuntimeError:
        pass
 
    yield 2
    yield 3
    return "bobby"
 
if __name__ == "__main__":
    gen = gen_func()
    print(next(gen))
    gen.close()  # 关闭生成器
    print("bobby")
 
    #GeneratorExit是继承自BaseException, Exception

输出结果如下

throw 向生成器内部抛出一个异常

1
2
3
4
5
6
7
8
9
10
11
def gen_func():
     
    yield "http://projectsedu.com"
    yield 2
    yield 3
    return "bobby"
 
if __name__ == "__main__":
    gen = gen_func()
    print(next(gen))
    gen.throw(Exception, "download error")

 

输出结果如下

 

 yield 和 yield from 区别

yield 返回的是一个具体的值

1
2
3
4
5
6
7
8
9
10
11
12
13
from itertools import chain
 
my_list = [1,2,3]
 
def my_chain(*args, **kwargs):
    print(args)
    for my_iterable in args:
 
        for value in my_iterable:
            yield value
 
for value in my_chain(my_list, range(5,10)):
    print(value)

 

输出结果如下

 

yield from 返回的是一个可迭代的对象,在内部帮助我们遍历取值

1
2
3
4
5
6
7
8
9
10
my_list = [1,2,3]
 
def my_chain(*args, **kwargs):
    print(args)
    for my_iterable in args:
 
        yield from my_iterable
 
for value in my_chain(my_list, range(5,10)):
    print(value)

  

 输出结果如下

 

生成器进阶-yield from

yield from 在内部帮助我们做了大量的异常处理

 一段伪代码如下

1
2
3
4
5
6
def g1(gen):
    yield from gen
 
def main():
    g = g1()
    g.send(None)

1  main 调用方 g1 (委托生成器)  gen 子生成器
2  yield from 会在调用方 main 与子生成器 gen 之间建立一个双向通道

3 在这个双向通道内 调用方main可以直接传值给子生成器gen 

4  yield from 会把子生成器的最终的值返回给委托生成器 

 案例:统计一个字典中value为列表对其求和返回

main调用方调用委托生成器middle 通过yield from 和子生成器 sales_sum 建立双向通道

通过双向通道向其传值,通过send(none) 编程停止,子生成器通过yield from 把最终的统计结果返回给委托生成器

 

复制代码
#python3.3新加了yield from语法
final_result = {}


def sales_sum(pro_name):
    total = 0
    nums = []
    while True:
        x = yield
        print(pro_name+"销量: ", x)
        if not x:
            break
        total += x
        nums.append(x)
    return total, nums

def middle(key):
    while True:
        final_result[key] = yield from sales_sum(key)
        print(key+"销量统计完成!!.")

def main():
    data_sets = {
        "bobby牌面膜": [1200, 1500, 3000],
        "bobby牌手机": [28,55,98,108 ],
        "bobby牌大衣": [280,560,778,70],
    }
    for key, data_set in data_sets.items():
        print("start key:", key)
        m = middle(key)  # 委托生成器
        m.send(None) # 预激middle协程
        for value in data_set:
            m.send(value)   # 给协程传递每一组的值
        m.send(None)
    print("final_result:", final_result)

if __name__ == '__main__':
    main()
复制代码

 

输出结果如下

async和await

 python为了将语义变得更加明确,因为有时候我们通过yield 关键字很难区分一个函数是生成器还是协程,就引入了async和await关键词用于定义原生的协程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import types
 
@types.coroutine
def downloader(url):
    yield "zhangbiao"
 
async def download_url(url):
    html = await downloader(url)
    return html
 
if __name__ == "__main__":
    coro = download_url("http://www.imooc.com")
    res = coro.send(None)
    print(res)

  

 输出结果如下

 

 

 

 

posted @   Crazymagic  阅读(363)  评论(0编辑  收藏  举报
编辑推荐:
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
阅读排行:
· 终于写完轮子一部分:tcp代理 了,记录一下
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示

目录导航