Python3 - 开始python编程(十三)

Python3 - 开始python编程(十三)

上一篇文章中,我们介绍了迭代器和生成器。在本文中,我们将深入研究异步代码或可以同时执行多项操作的代码。只是警告一下,这一课将很难。到目前为止,您需要对我们所涵盖的所有内容都有很好的了解。好消息是,在此之后,其余的将比较容易。

尽管大多数文章在给出异步编程工作原理的示例时都使用”sleep()”,但我向朋友许诺,我会避免使用这种语法来解释异步代码。您只需要知道,当您调用 sleep(3)时,程序的执行将等待 3 秒,然后再次恢复。

在深入探讨之前,我们需要涵盖将在本文中使用的一些术语。

  • Coroutine -最低级别的异步编程,这是一个异步执行的函数。这些可以直接运行,也可以在task中使用。
  • Task -用于安排协程以由系统异步执行。
  • Future -用于暂挂协程,直到完成另一个协程。 (将其视为需要完成数据才能获取数据的后台代码)
  • Event Loop -循环访问一个或多个任务直到完成的循环。在 Web 服务器中,这可能是等待客户端连接的无限循环。这是 Python 异步的核心。它协调了在后台完成的所有工作。
  • CPU —中央处理单元;计算机中执行计算(即工作)的零件硬件。
  • CPU 物理内核 —今天的 CPU 包含多个内核。这些核心可能包括逻辑处理器(例如 Intel i3,i5,i7),每个物理核心负责处理数据或将工作移交给逻辑处理器。如果拆除 CPU,则可以接触物理内核。
  • 逻辑处理器-如果物理核心包含逻辑处理器,则它们将执行工作。逻辑处理器仅由于软件而存在,但这使您的计算机可以同时执行多项操作。 (在下载文件时编写代码时播放音乐等)。逻辑处理器是您在四核计算机上看到八个内核的原因。
  • Thread —执行工作的物理或逻辑处理器中的队列。处理器可以包含多个队列。他们只是根据优先级安排的。由于计算机具有多个内核,因此您的代码可以在一个内核的主线程上运行,而用户界面可以在另一个内核的主线程上运行。任何不在主线程上运行的代码都视为在后台运行。

Awaitables

asyncio 是我们可以导入以执行异步代码的一个模块。它是为您的 I / O(输入/输出)连接速度较慢而设计的,它提供了两个现在很重要的实用程序-“async”和”await”。使用asyncio的一个很好的例子是一个 Web 服务器。

“awaitable”是可以在”await”表达式中使用的对象。对于包含 await 的任何函数,在 def 前面使用 async。

Coroutines and Futures

虽然我在上面给了您一个定义,但我希望您在详细介绍之前对全局有一个基本的了解。

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
import asyncio
import json


async def connection(reader, writer):
    await reader.read(1024)

    head = b"HTTP/1.1 200 OK\r\n"
    head += b"Content-Type: application/json\r\n"
    head += b"\r\n"

    data = {
        "_id": "5d044617641bd54c27591990",
        "index": 0,
        "guid": "bf8043b5-4931-46ec-a330-a678fa5df19b",
        "isActive": True,
    }

    body = bytes(json.dumps(data), encoding="utf-8")

    writer.write(head + body)
    writer.close()


async def main(host, port):
    server = await asyncio.start_server(connection, host, port)

    async with server:
        await server.serve_forever()


try:
    asyncio.run(main("0.0.0.0", 8000))
except KeyboardInterrupt:
    # Control + c was pressed
    print("Stopping web server")

我知道这看起来像很多代码,但是我们将一次只完成一个部分。这是我在此处找到的脚本的重做。

在顶部,我们有两个导入:asyncio 和 json。当将数据返回给客户端时,asyncio将是繁重的工作,而json仅用于将字典编码为 JSON 结构 t 我们可以返回给每个客户。

下一个方法“异步定义连接(读取器,写入器)”用于在客户端请求数据时对其进行响应。因为我们使用了 async,所以这是一个协程对象,它将在单独的线程上运行,因此服务器可以在返回响应时处理其他请求。 reader用于将请求传递给我们的函数,`writer’构成对返回给客户端的响应。

首先,我们使用”await reader.read(1024)”读取请求。当我们要求某些东西时,请求中通常会包含一些数据。这些数据告诉我们客户想要做什么以及希望如何做。只要有效,这取决于我们的服务器。在这里,我们正在读取前 1024 个字节,这对于任何 GET 请求都应该足够了。我们使用await来搁置此方法,直到可以读取所有数据为止。这意味着reader.read(1024)是一个未来。

接下来,我们开始建立我们的回应。由于我们需要告诉客户如何理解我们发送回的数据,因此我们需要包含一个标头。

在此标头中,我们告诉客户一些事情:

  • HTTP / 1.1-我们响应的 HTTP 版本
  • 200 OK-请求的状态码,200 表示成功
  • Content-Type:application / json-我们返回的内容类型将为 JSON。

您可能会注意到我们在请求中包含了”\ r \ n”,并且在结尾处两次。

\ r是一个转义序列,它创建一个回车(CR)。当您点击页面边缘时,这代表了老式打字机。需要回车才能将回车(保持纸张的条)返回到页面的左边缘。 \ r 用于 Mac OS 版本早于 X 的换行符。

\ n 是一个转义序列,用于创建换行符(LF)。回到打字机;换行是当您向下移页面以开始新行时。如今,\ n 仍在 Linux 和 macOS 系统中使用,以代表新行。

\ r \ n是同时执行回车和换行(CRLF)的想法。它是对人们使用打字机时所发生情况的更准确描述,但它也被用于在 Windows 中创建新行。这也是在 Windows 和 Linux(或 Windows 和 macOS)之间切换时有时无法运行代码的重要原因。

好,回到它。接下来,我们有一个“数据”,它只是我使用此脚本创建的 JSON 的随机位,为简洁起见进行了一些修改,并将其存储为摘要中的字典。

接下来,我们使用 json.dumps(data)将字典转换为 JSON 字符串。 dumps代表“转储字符串”。由于我们的网络服务器只喜欢处理字节,因此我们将其强制转换为”bytes”对象,并为其提供 UTF-8 编码,以便我们的客户端可以正确解析它。

然后我们使用writer.write(header + body)。首先,它将”header”与”body”连接在一起,然后在关闭写入器之前,将这些数据写回到 Web 服务器以返回到客户端,这将有效地刷新写入器的缓冲区并从内存中删除该对象。

async def main(host,port)是一个协程,用于处理进入 Web 服务器的请求。它接受一个“主机”和一个“端口”作为参数,这些参数将在主体中用于创建基本的 Web 服务器。

服务器=等待 asyncio.start_server(连接,主机,端口)很长,但很容易理解。我们使用 asyncio.start_server 创建一个 server 对象。这将是一个未来,它将使用上面的connection()协程,host,将要托管的服务器的 IP 地址以及我们将接受的服务器上的port创建一个 Web 服务器。连接来自。

与服务器异步是我们尚未涉及的新语法。 “with”本质上完成了我们想要完成的工作之后的创建和所需的任何清理。与服务器异步使用 async 启动服务器,如果遇到任何问题,它将彻底关闭服务器。在这里,我们使用它来运行将来的await server.serve_forever()。是的,上面为我们创建的服务器还带有一个“异步”功能。

最后,我们需要设置服务器。

asyncio.run()创建一个事件循环,使我们可以运行协程。 main(“0.0.0.0”,8000)是我们传入的协程,因为它最终将调用我们的连接 coroutine。

如果您不熟悉 IP 地址,请快速查看以下内容:

  • 127.0.0.1-没有像家一样的地方,这是您的本地回送地址。
  • 0.0.0.0-告诉服务器在所有接口(网卡)上的所有 IP 地址上托管。如果您同时具有以太网端口和无线卡,则该服务器将可用于两个接口。对此要小心;您可能只想为一个网络接口指定 IP 地址,始终首选以太网。

每台计算机都有许多可用的端口,确切的说是 65536,尽管我们从 0 开始,所以最大端口号是 65535。请务必查看ll 个已知端口(其中有 1024 个)。由于它们是为特定功能保留的,因此在测试过程中应尽量远离它们。端口 80 用于 HTTP,而端口 443 用于 HTTPS。虽然我不能说我记得所有的人,但我记得我使用最多的人。

您还需要注意其他端口,例如 PostgreSQL — 5432,MSSQL — 1433、1434,MySQL — 3306 和 RDP — 3389。

这些是必不可少的事情,您将越来越多地使用它们。

回到它!您可能会注意到,我将其包装在 try / except 块中。我这样做是因为当我在键盘上按ctrl + c时,程序崩溃了。我不喜欢它,所以我处理了”KeyboardInterrupt”异常,该异常在用户执行中断时发生。我本来可以通过并让它安静地结束,但是为了退出对 Web 管理员的友好而决定在退出之前打印“停止 Web 服务器”。

照片由Glenn Carstens-PetersUnsplash上拍摄

任务(Tasks)

对于任务,首先要记住的是任务不是线程安全的。这意味着,如果数据被另一个协程使用的一个协程更改,则可能会出现一些意外结果,或者更糟的是,您的程序可能会崩溃。

在线程安全方面,应这样考虑:我们俩都驾驶同一辆车。我们必须计划谁在什么时候开车。如果我开车在 8-5 之间工作,而您尝试在上午 10 点开车,那么您将无法开车,因为那辆车不在那儿。

翻译:假设我们有一个缓冲区,可以包含我们要存储在其中的任何数据。然后,我们有两个任务使用同一缓冲区执行工作。一个任务使用整数数据类型,另一个任务使用字符串。

第一个任务在缓冲区内部存储 42,第二个任务用”Hello”替换缓冲区的内容,然后第一个任务尝试使用缓冲区向缓冲区中添加 1。我们?

有了这个警告,让我们深入研究任务。

任务用于安排协程。任务也可以用于同时运行多个协程。

任务有几种可用的方法,这些方法使您可以取消任务,从任务返回结果,检查任务的状态以及向任务添加或删除 callbacks。

回调实质上是任务完成时将调用的方法或函数。如果我有一个在屏幕上打印”Bob”的任务,则可能有一个回调,该回调调用了一个函数,该函数在完成时会打印”Program finish”。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import asyncio


def count(run):
    for i in range(100):
        print(f"{run} - {i}")


async def create_tasks():
    inner_loop = asyncio.get_event_loop()
    tasks = [inner_loop.run_in_executor(None, count, i) for i in range(300)]
    await asyncio.gather(*tasks)


outer_loop = asyncio.get_event_loop()
outer_loop.run_until_complete(create_tasks())
outer_loop.close()

和往常一样,我们导入 asyncio。然后,我们有一个count函数,该函数将当前运行编号作为参数。然后,我们为每次运行打印 100 次迭代。我们之前已经完成了所有这一切。

create_tasks 是一个异步函数,它使用 asyncio.get_event_loop()创建内部事件循环。它提供了一个事件循环,我们可以用来执行任务。

然后,我创建了一个列表理解功能,该列表生成功能可运行 300 个任务。在此列表理解中,我们使用inner_loop.run_in_executor。该函数的第一个参数需要一个concurrent.futures.executor实例。如果像在这里那样传递”None”,我们将使用默认的执行程序。可以满足我们的需求。第二个参数”count”是指我们希望同时(即同时)调用的函数。最后,”i”是我们传入的参数,在调用时将传递给”count”函数。

最后,我们在 asyncio.gather(* tasks)中使用`for _来将我们所有的任务添加到将来,从而将所有任务聚合为一个结果。

我们的程序将开始创建一个外部循环,该循环将用于计划内部循环,将其告知run_until_complete并传入函数create_tasks()作为唯一需要执行的任务。

任务完成后,外部循环将关闭。虽然看起来我们忘记了关闭内部循环,但是当create_tasks结束时,它自动关闭了。如果尝试在函数末尾关闭它,则会收到运行时错误,提示我们无法关闭正在运行的事件循环。

我希望您运行此代码并查找在输出中看起来很奇怪的任何东西。如果使用较小的范围,则会发现较少的异常,也许没有。但是请放心,因为我们不是以线程安全的方式创建所有这些任务的。如果我们想这样做,那么如果每个任务的结果都存储在同一个地方,那么我们将一次处理一个结果。


摘要

今天我们了解了协程,任务,Future和事件循环。我认为这已经足够了,但是就异步编程而言,我们还有更多内容要讲。是的,兔子洞更深了!我们不仅有asyncio,而且还有线程和多处理功能可供使用。我们还没有介绍后两个,但是这些都在讨论中。

如果您不了解某些内容,请不要担心。使用异步代码需要时间和经验。探索,自己尝试,不要害怕做出改变;它可以总是固定的。

建议阅读

异步-异步 I/O-Python 3.7.3 文档

Python 中的异步 IO:完整的演练-真正的 Python


下一步是什么

更多异步代码!只有这次,我们才不会涉及”asyncio”。取而代之的是,我们将研究其他异步编程的方法,其中”asyncio”可能并不理想。请继续练习!

Rating: