惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
Engineering at Meta
Engineering at Meta
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
D
DataBreaches.Net
N
Netflix TechBlog - Medium
T
The Blog of Author Tim Ferriss
L
LangChain Blog
Jina AI
Jina AI
MongoDB | Blog
MongoDB | Blog
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tailwind CSS Blog
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
T
Threatpost
V
V2EX
Security Archives - TechRepublic
Security Archives - TechRepublic
GbyAI
GbyAI
Scott Helme
Scott Helme
Cyberwarzone
Cyberwarzone
H
Help Net Security
大猫的无限游戏
大猫的无限游戏
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
T
Tenable Blog
S
Schneier on Security
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
F
Full Disclosure
腾讯CDC
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Security @ Cisco Blogs
A
Arctic Wolf
S
Securelist
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Tor Project blog
The Register - Security
The Register - Security
L
LINUX DO - 最新话题
Blog — PlanetScale
Blog — PlanetScale
U
Unit 42
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
L
LINUX DO - 热门话题
TaoSecurity Blog
TaoSecurity Blog
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
C
CERT Recently Published Vulnerability Notes

Jiajun的技术笔记

你好,2026! TiDB 源码阅读(六):TiDB Coprocessor 源码解析 性能优化的核心思想 TiDB 源码阅读(五):索引 TiDB 源码阅读(四):AST、逻辑计划、物理计划 CockroachDB Serverless Architecture podman 无故退出 Cursor Control-L (CTRL-L) Keyboard Shortcuts in Terminal Replace docker with podman Using xmonad with xfce4 A RC script for freebsd frpc 自己动手写一个k8s controller AI 会取代你的(编程)岗位吗? 自建DERP服务器提升Tailscale连接速度(使用Nginx转发) 自动升级Docker容器 再读《程序员修炼之道-从小工到专家》 让浏览器下载文件 再读《软件随想录》/《黑客与画家》/《软技能》 HTTP 压力测试中的 Coordinated Omission 2的补码 编程语言中的 context 是什么? flutter macOS 构建出错 Flatpak 使用小记 Golang CAS 操作是怎么实现的 PostgreSQL 当MQ来使用 Clash 结合 工作VPN 的网络设计 使用 PostgreSQL 搭建 JuiceFS PostgreSQL 配置优化和日志分析 有GitHub Copilot?那就可以搭建你的ChatGPT4服务 窗口函数的使用(以PG为例) 读《为什么学生不喜欢上学》 OpenAI Prompt Engineering 摘录和总结 读《打造真正的新产品》 VueJS 总结 Linux 自动挂载 alist 提供的webdav FreeBSD 使用 vm-bhyve 安装Debian虚拟机 FreeBSD 和 Linux 网卡聚合实现提速 GPT 帮我搞定了时区转换问题 长任务系统如何处理? macOS/Linux 编译 InputLeap 使用开源软KVM - synergy-core 解决 macOS 终端hostname一直变化问题 KVM 共享 Intel 集成显卡 PromQL 备忘 读《格鲁夫给经理人的第一课》 读《打开心智》 为什么要把复杂的联表操作拆成多个单表查询? 红包系统的设计 MySQL Index Condition Pushdown Optimization Go mod 简明教程 OpenWRT 使用 Android/iOS USB 网络 搭建旁路由 Golang gRPC 错误处理 编写可维护的单元测试代码 OAuth 2 详解(六):Authorization Code Flow with PKCE OAuth 2 详解(五):Device Authorization Flow OAuth 2 详解(三):Resource Owner Password Credentials Grant OAuth 2 详解(四):Client Credentials Flow OAuth 2 详解(二):Implict Grant Flow OAuth 2 详解(一):简介及 Authorization Code 模式 ElasticSearch 学习笔记 三种git流程以及发版模型 错误处理实践 权限模型(RBAC/ABAC) OIDC(OpenID Connect) 简介 任务队列简介 PostgreSQL 操作笔记 使用Drone CI构建CI/CD系统 Golang migrate 做数据库变更管理 使用PostgreSQL做搜索引擎 Nginx 源码阅读(三): 连接池、内存池 Nginx 源码阅读(二): 请求处理 Nginx 源码阅读(一): 启动流程 Go 泛型简明教程 KVM 显卡穿透给 Windows 使用 HTTP Router 处理 Telegram Bot 按钮回调 使用反射(reflect)对结构体赋值 GIN 是如何绑定参数的 你好 2022(2021 年终总结) 用Go导入大型CSV到PostgreSQL 使用 OpenWRT 搭建软路由 使用软KVM切换器 barrier 共享键鼠 SQL 防注入及原理 使用 gomock 测试 Go 代码 gevent不是黑魔法(二): gevent 实现 gevent不是黑魔法(一): greenlet 实现 用 entgo 替代 gorm 应用内使用crontab不是那么方便 单测时要不要 mock 数据库? Sentry 自建指南 用selenium完成自动化任务 用闲置的安卓手机做垃圾电话短信过滤 推荐三个时间管理工具 一次事故反思 当JS遇到uint64:JS整数溢出问题 SQLite3 存储以及ACID原理 Redis源码阅读:pub/sub实现 Redis源码阅读:zset实现 Redis源码阅读:bitmap 位图的运算 Redis源码阅读:set是怎么做交并集运算的?
借助coroutine用同步的语法写异步
2016-11-27 · via Jiajun的技术笔记

首先我们构造一个耗时足够久的服务器:

import tornado.gen
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    @tornado.gen.coroutine
    def get(self):
        yield tornado.gen.sleep(1)
        self.write("Hello, world\n")

if __name__ == "__main__":
    application = tornado.web.Application([
        (r"/", MainHandler),
    ])
    application.listen(8888)
    tornado.ioloop.IOLoop.current().start()

每次请求都耗时一秒钟:

root@arch tests: nohup python test.py > /dev/null &
[1] 15597
nohup: ignoring input and redirecting stderr to stdout
root@arch tests:
root@arch tests: ls
sama  test.py
root@arch tests:
root@arch tests: time curl localhost:8888
Hello, world

real    0m1.018s
user    0m0.000s
sys 0m0.007s
root@arch tests: time curl localhost:8888
Hello, world

real    0m1.016s
user    0m0.003s
sys 0m0.003s

阻塞型请求

我们先来看代码:

import socket
import time

PORT = 8888
CHUNK_SIZE = 4096


def request():
    sock = socket.socket()
    sock.connect(("", PORT))
    sock.send(b"GET / HTTP/1.1\r\n\r\n")
    data = sock.recv(CHUNK_SIZE)
    print(data.decode())


start = time.time()
request()
request()
end = time.time()
print("use time: %.2f second(s)" % (end - start))

这样子的话,请求一次就需要花费一秒,请求是一个接着一个来的,在这中间的时间 进程被投入睡眠。

I/O多路复用

这个时候我们的老前辈们就有新办法了,好,我们翻开《UNIX环境高级编程》,里面有 专门讲select的,看完这个之后,我们来看看 Python 3 提供的 selectors 模块

我们把上面的代码改改:

import selectors
import socket
import time

PORT = 8888
CHUNK_SIZE = 4096
COUNT = 0

selector = selectors.DefaultSelector()


def request(selector):
    global COUNT
    sock = socket.socket()
    sock.connect(("", PORT))
    selector.register(sock.fileno(), selectors.EVENT_WRITE, data=lambda: writable(selector, sock))
    COUNT += 1


def writable(selector, sock):
    selector.unregister(sock.fileno())
    sock.send(b"GET / HTTP/1.1\r\n\r\n")
    selector.register(sock.fileno(), selectors.EVENT_READ, data=lambda: readable(selector, sock))


def readable(selector, sock):
    global COUNT
    selector.unregister(sock.fileno())
    COUNT -= 1
    data = sock.recv(CHUNK_SIZE)
    print(data.decode())


start = time.time()
request(selector)
request(selector)

while COUNT:
    for key, _ in selector.select():
        callback = key.data
        callback()

end = time.time()
print("use time: %.1f second(s)" % (end - start))
root@arch tests: python client.py
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Etag: "7b4758d4baa20873585b9597c7cb9ace2d690ab8"
Server: TornadoServer/4.4.2
Content-Length: 13
Date: Sun, 27 Nov 2016 14:02:38 GMT

Hello, world

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Etag: "7b4758d4baa20873585b9597c7cb9ace2d690ab8"
Server: TornadoServer/4.4.2
Content-Length: 13
Date: Sun, 27 Nov 2016 14:02:38 GMT

Hello, world

use time: 1.0 second(s)

再运行一下发现两次请求也只花一秒钟时间。这就是I/O多路复用模型的作用~ 但是呢,大把大把的callback把函数拆的四分五散,很不利于阅读。所以接下来我们 就要介绍主角出场: coroutine

coroutine

In [6]: def use_yield():
...:     print("enter the func")
...:     value = yield "hello"
...:     print("got: ", value)
...:     return value
...:

In [7]: gen = use_yield()

In [8]: gen.send(None)
enter the func
Out[8]: 'hello'

In [9]: gen.send("world")
got:  world
---------------------------------------------------------------------------
StopIteration                             Traceback (most recent call last)
<ipython-input-9-ffdc45971c0a> in <module>()
----> 1 gen.send("world")

StopIteration: world

In [10]: type(gen)
Out[10]: generator

说好的coroutine呢?怎么最后输出的是generator?别着急,且听我慢慢说来。

首先我们先下个定义,包含了yield关键字的函数就叫generator。来我们先 默念三遍,包含了yield关键字的函数就叫generator; 包含了yield关键字的函数就叫generator; 包含了yield关键字的函数就叫generator。

什么叫generator呢?就是这个函数可以执行到中间某句话的时候,把控制权转让给别人。 并且在未来,别人可以让这个函数从那句话处继续执行。我们通过next让generator执行 到下一个yield处,如果之后没有了yield就会执行到函数结尾,然后抛一个 StopIteration 异常。而且我们还可以通过 .send 给generator发送数据,恢复它的执行。

个人的理解就是,在python的世界里,coroutine是建立在generator的语法基础上的产物。 并没有具体的形式,coroutine就是用户来控制程序切换。具体在python里就是用户通过 yield把控制权丢出去,通过 .send 或者 next 来切回那个函数里继续执行。

注:接下来所有说用 next 的地方,实际代码上我都是用的 .send

coroutine based I/O

我想在等待I/O的时候,把cpu控制权丢出去,让别人继续执行,等到I/O准备完成的时候, 再来执行我。这句话有点熟悉,就跟我们站在第一人称描述I/O多路复用的时候一样: 我想在等待I/O的时候把我挂起,让别人执行,我给你一个回调函数,等到I/O准备完成的 时候,你去执行这个回调函数。

那如果我们想通过yield来抹平回调函数把原本一个函数切分成两个函数的缝隙呢? 函数执行的一个缺点就是执行完之后,函数中的变量状态就丢失了。

> 注:我们简单说一下Python的VM,Python是有自己的指令的,就跟x86的cpu有 自己的指令一样。我们来简单看一下:

In [17]: def foo():
    ...:     bar()
    ...:

In [18]: def bar():
    ...:     pass
    ...:

In [19]: import dis

In [20]: dis.dis(foo)
2           0 LOAD_GLOBAL              0 (bar)
            3 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
            6 POP_TOP
            7 LOAD_CONST               0 (None)
            10 RETURN_VALUE

In [21]: dis.dis(bar)
2           0 LOAD_CONST               0 (None)
            3 RETURN_VALUE

首先执行foo函数的时候,会由其它函数把环境准备好,把回退指针准备好,然后 调用。

- `LOAD_GLOBAL` 首先从global()里加载bar函数
- `CALL_FUNCTION` 会调用该函数
- `POP_TOP` 会把该函数的栈清掉
- `LOAD_CONST` 把None加载到栈顶,因为这是foo函数的默认返回值
- `RETURN_VALUE` 把None返回

其实我们可以直接把一系列的函数存到 selector.register 的data里,但是我们 把它抽出来,就跟ES6里的 Promise 一样,我们管它叫 Future 。就是一个 普通的类,用来保存回调函数和执行结果的。

class Future:
    def __init__(self):
        self._reuslt = None
        self._callbacks = []

    def set_result(self, result):
        self._result = result
        for callback in self._callbacks:
            callback()

    def add_done_callback(self, callback):
        self._callbacks.append(callback)

所以我们把 register 改成:

selector.register(sock.fileno(), selectors.EVENT_READ, data=fut)

然后在下面的 select 处改成:

for key, _ in selector.select():
    fut = key.data
    fut.set_result(None)

因为在这里,key.data 已经不是回调函数,而是我们的Future了。

但是我们希望的结果是能够切回我们的函数继续执行,这时候就靠 next 了,那我们 要想个办法,让future执行完之后调用 next(coro) 。首先我们要找个地方保存住 对coro的引用,所以和Future一样,我们用一个类或者函数来保存都行。为了以后更方便 理解asyncio和tornado,我们用一个类,名字叫 Task

class Task:
    def __init__(self, coro):
        self.coro = coro

    def step(self):
        try:
            fut = self.coro.send(None)
        except StopIteration:
            return
        fut.add_done_callback(self.step)

这样我们调用的时候就是 task = Task(request()) 然后 task.step() 了, 首先 task = Task(request()) 会执行 Task.__init__ 会把request()这个 generator保存下来,为啥参数里叫做coro呢?因为我们把它用作coroutine,好以后 我们统称coroutine吧。

接下来通过 task.step() 启动coroutine,然后增加一个回调函数,一直执行 到 selctor.register ,然后yield。接着执行第二个 Task(request(selector)).step() 同样yield。接着执行 while COUNT 循环,然后执行 selctor.select 并且阻塞 于此,当socket可读时,就会执行 fut.set_result(None) 然后就会执行里面的 callback函数,其中有一个callback就是执行上面的 step ,借此执行了 self.coro.send(None) 从而恢复了coroutine的执行。

如果使用函数的形式,可以通过闭包达到这一点。

def task(coro):
    try:
        fut = coro.send(None)
    except StopIteration:
        return
    fut.add_done_callback(lambda: task(coro))

结合上面所说,代码应该是这样的:

import selectors
import socket
import time

PORT = 8888
CHUNK_SIZE = 4096
COUNT = 0

selector = selectors.DefaultSelector()


class Future:
    def __init__(self):
        self._result = None
        self._callbacks = []

    def set_result(self, result):
        self._result = result
        for callback in self._callbacks:
            callback()

    def add_done_callback(self, callback):
        self._callbacks.append(callback)


class Task():
    def __init__(self, coro):
        self.coro = coro

    def step(self):
        try:
            fut = self.coro.send(None)
        except StopIteration:
            return
        fut.add_done_callback(self.step)


def request(selector):
    global COUNT
    fut = Future()

    sock = socket.socket()
    sock.connect(("", PORT))
    selector.register(sock.fileno(), selectors.EVENT_WRITE, data=fut)
    COUNT += 1

    yield fut

    selector.unregister(sock.fileno())
    sock.send(b"GET / HTTP/1.1\r\n\r\n")

    fut = Future()  # 原来的fut已经用完了,我们要来个新的
    selector.register(sock.fileno(), selectors.EVENT_READ, data=fut)

    yield fut

    selector.unregister(sock.fileno())
    COUNT -= 1
    data = sock.recv(CHUNK_SIZE)
    print(data.decode())


start = time.time()
Task(request(selector)).step()
Task(request(selector)).step()

while COUNT:
    for key, _ in selector.select():
        fut = key.data
        fut.set_result(None)

end = time.time()
print("use time: %.1f second(s)" % (end - start))

另外, sock.connect 是阻塞的,这个时候我们需要把socket设置 成非阻塞的。 socket.setblocking(False) 可以把它设置成非阻塞的。

import selectors
import socket
import time

PORT = 8888
CHUNK_SIZE = 4096
COUNT = 0

selector = selectors.DefaultSelector()


class Future:
    def __init__(self):
        self._result = None
        self._callbacks = []

    def set_result(self, result):
        self._result = result
        for callback in self._callbacks:
            callback()

    def add_done_callback(self, callback):
        self._callbacks.append(callback)


class Task():
    def __init__(self, coro):
        self.coro = coro

    def step(self):
        try:
            fut = self.coro.send(None)
        except StopIteration:
            return
        fut.add_done_callback(self.step)


def request(selector):
    global COUNT
    COUNT += 1

    fut = Future()

    sock = socket.socket()
    sock.setblocking(False)

    try:
        sock.connect(("", PORT))
    except BlockingIOError:
        pass

    selector.register(sock.fileno(), selectors.EVENT_WRITE, data=fut)
    yield fut
    selector.unregister(sock.fileno())

    sock.send(b"GET / HTTP/1.1\r\n\r\n")

    fut = Future()  # 原来的fut已经用完了,我们要来个新的

    selector.register(sock.fileno(), selectors.EVENT_READ, data=fut)
    yield fut
    selector.unregister(sock.fileno())

    data = sock.recv(CHUNK_SIZE)
    print(data.decode())
    COUNT -= 1


start = time.time()
Task(request(selector)).step()
Task(request(selector)).step()

while COUNT:
    for key, _ in selector.select():
        fut = key.data
        fut.set_result(None)

end = time.time()
print("use time: %.1f second(s)" % (end - start))

这份代码对比起一开始的阻塞型代码,结构上就很类似了,不会因为回调而把一个 函数拆的四分五裂。好了,今天就写到这里吧,下一篇我准备讲讲 yield yield from await async ——– yield 的前世今生。