├── .gitignore ├── .markdownlint.json ├── README.md ├── docs ├── chapter1 │ ├── communicating_in_parallel_programming.md │ ├── discovering_Pythons_parallel_programming_tools.md │ ├── exploring_common_forms_of_parallelization.md │ ├── identifying_parallel_programming_problems.md │ ├── index.md │ ├── summary.md │ ├── taking_care_of_GIL.md │ └── why_use_parallel_programming.md ├── chapter2 │ ├── index.md │ ├── 使用数据分解.md │ ├── 分治技术.md │ ├── 处理和映射.md │ ├── 总结.md │ └── 用管道分解任务.md ├── chapter3 │ ├── index.md │ ├── 从多个输入中得到斐波那契最大的值.md │ ├── 总结.md │ └── 爬取网页.md ├── chapter4 │ ├── crawling_the_web_using_the_concurrent_futures_module.md │ ├── defining_threads.md │ ├── index.md │ ├── using_threading_to_obtain_the_Fibonacci_series_term_with_multiple_inputs.md │ └── 总结.md ├── chapter5 │ ├── crawling_the_web_using_processPoolExecutor.md │ ├── implementing_multiprocessing_communication.md │ ├── index.md │ ├── understanding_the_concept_of_a_process.md │ ├── using_multiprocessing_to_compute_fibonacci_series_terms_with_multiple_inputs.md │ └── 总结.md ├── chapter6 │ ├── discovering_pp.md │ ├── index.md │ ├── understanding_interprocess_communication.md │ ├── using_pp_to_calculate_the_fibonacci_series_term_on_smp_architecture.md │ ├── using_pp_to_make_a_distributed_web_crawler.md │ └── 总结.md ├── chapter7 │ ├── defining_queues_by_task_types.md │ ├── dispatching_a_simple_task.md │ ├── index.md │ ├── setting_up_the_environment.md │ ├── understanding_celery.md │ ├── understanding_celery_architecture.md │ ├── using_celery_to_make_a_distributed_web_crawler.md │ ├── using_celery_to_obtain_a_fibonacci_series_term.md │ └── 总结.md ├── chapter8 │ ├── index.md │ ├── 使用asyncio.md │ ├── 总结.md │ ├── 理解事件循环.md │ └── 理解阻塞非阻塞和异步操作.md ├── files │ └── Parallel Programming with Python.pdf ├── imgs │ ├── 1-01.png │ ├── 1-02.png │ ├── 1-03.png │ ├── 2-01.png │ ├── 2-02.png │ ├── 2-03.png │ ├── 3-01.png │ ├── 3-02.png │ ├── 6-02.png │ ├── 6-03.png │ ├── 6-04.png │ ├── 7-01.png │ ├── 7-11.png │ ├── 7-12.png │ ├── 7-13.png │ ├── 7-14.png │ ├── 7-15.png │ ├── 7-16.png │ ├── 7-17.png │ └── 7-18.png └── index.md └── mkdocs.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | temp.py 132 | .DS_Store -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD007": false, 3 | "MD046": false, 4 | "MD013": false, 5 | "MD024": false, 6 | "MD051": false, 7 | "MD036": false, 8 | "MD029": false, 9 | "MD033": false, 10 | "MD049": false 11 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 使用python并发编程 2 | 3 | 这是我在学习python的路上,在github上淘到的一本电子书,虽然出版时间有点久了,但知识是无价的,并且较新的技术或理念也是在原有知识基础上发展起来的。故放在这,为了尊重原翻译者,固新建了一个分支`docs`,如有访问原翻译的需求,请访问[master](https://github.com/hellowac/parallel-programming-with-python-zh/tree/master)分支。 4 | 5 | 在参考原文的基础上,完善了原仓库中部分对python编程术语的描述,以及补充了部分图表截图等等,主要如下: 6 | 7 | 1. 完善了部分章节的翻译描述。 8 | 2. 补充了部分章节的截图。 9 | 3. 使用python的mkdocs工具文档化了电子书,并且利用github pages 发布出来方便复习和分享。 10 | 11 | ## 在线阅读 12 | 13 | 阅读: [python并发编程-zh](https://hellowac.github.io/parallel-programming-with-python-zh/) 14 | -------------------------------------------------------------------------------- /docs/chapter1/communicating_in_parallel_programming.md: -------------------------------------------------------------------------------- 1 | # 在并行编程通信 2 | 3 | 在并行编程中,被派去执行任务的**wokers**通常需要建立沟通,以便合作解决问题。 在大多数情况下,这种通信的建立方式是可以在**worker**之间交换数据。 在**并行编程**方面,有两种通信形式更广为人知:**共享状态**和**消息传递**。 在以下各节中,将对两者进行简要说明。 4 | 5 | ## 理解共享状态 6 | 7 | **worker**之间最著名的一种交流方式是**共享状态**。 **共享状态**似乎简单易用,但有很多缺陷,因为其中一个进程对**共享资源**进行的无效操作会影响所有其他进程,从而产生不良结果。 由于显而易见的原因,这也使得程序无法在多台机器之间分发。 8 | 9 | 为了说明这一点,我们将使用一个真实的案例。 假设你是某家银行的客户,而这家银行只有一名出纳员。 当你去银行时,你必须排队等待机会。 进入队列后,您会注意到一次只有一位顾客可以使用收银员,收银员不可能同时接待两位顾客而不会出错。 计算提供了以受控方式访问数据的方法,并且有多种技术,例如**互斥锁**(mutex)。 10 | 11 | **互斥锁**(Mutex)可以理解为一个特殊的过程变量,**表示访问数据的可用性级别**。 也就是说,在我们现实生活中的例子中,客户有一个号码,在特定的时刻,这个号码会被激活,收银员将专门为这个客户服务。 在流程结束时,这位顾客将为下一位顾客腾出收银员,依此类推。 12 | 13 | !!! info "" 14 | 15 | 在某些情况下,程序运行时数据在变量中具有常量值,并且共享数据仅用于读取目的。 因此,访问控制不是必需的,因为它永远不会出现完整性问题。 16 | 17 | ## 理解信息传递 18 | 19 | 当我们旨在避免源自共享状态的数据访问控制和同步问题时使用**消息传递**(Message passing)。 **消息传递**由运行进程中的消息交换机制组成。 每当我们开发具有分布式体系结构的程序时,它都非常常用,其中放置它们的网络中的**消息交换**是必要的。 例如,Erlang 等语言使用此模型在其并行架构中实现通信。 一旦在每次消息交换时都复制了数据,就不可能出现访问并发方面的问题。 尽管内存使用率似乎高于共享内存状态,但使用此模型也有优势。 它们如下: 20 | 21 | * 没有数据访问并发. 22 | * 消息可以在本地(各种进程)或分布式环境中交换. 23 | * 这使得可伸缩性问题发生的可能性降低,并实现了不同系统的互操作性. 24 | * 总的来说,按照程序员的方式维护起来很容易. -------------------------------------------------------------------------------- /docs/chapter1/discovering_Pythons_parallel_programming_tools.md: -------------------------------------------------------------------------------- 1 | # 发现Python并行编程的工具 2 | 3 | 由 Guido Van Rossum 创建的 Python 语言是一种多范式、多用途的语言。 由于其强大的简单性和易于维护,它已在世界范围内被广泛接受。 它也被称为包含电池的语言。 模块种类繁多,使用起来更顺畅。 在并行编程中,Python 具有简化实现的内置和外部模块。 本书是基于Python3.X的。 4 | 5 | ## Python的threading模块 6 | 7 | Python的**threading**模块为模块 **_thread** 提供了一个抽象层,它是一个较低级别的模块。 它提供的功能可以帮助程序员完成基于线程开发并行系统的艰巨任务。 **threading**模块的官方文档可以在{target="_blank"}中找到。 8 | 9 | ## Python的mutliprocess模块 10 | 11 | **multiprocessing** 模块旨在为使用基于进程的并行性提供一个简单的 API。 该模块类似于线程模块,它简化了进程之间的交替,没有太大的困难。基于进程的方法在 Python 用户社区中非常流行,因为它是回答有关使用 CPU 绑定线程和 Python 中存在的 GIL 的问题的替代方法。 **multiprocessing**模块的官方文档可以在以下位置找到: 12 | 13 | ## Python的parallel模块 14 | 15 | **parallel Python** 是外部模块,它提供了丰富的 API,这些API利用进程的方法创建并行和分布式系统。该模块是轻量级并且易安装的,并可与其他 Python 程序集成。 可以在 找到 **parallel Python** 模块。 在所有功能中,我们可能会强调以下内容: 16 | 17 | * 自动检测最佳配置 18 | * 在运行时可以更改许多工作进程的状态 19 | * 动态的负载均衡 20 | * 容错性 21 | * 自动发现计算资源 22 | 23 | ## Celery分布式任务队列 24 | 25 | **Celery** 是一个出色的 Python 模块,用于创建分布式系统并具有出色的文档。 它在并发形式上使用了至少三种不同类型的方法来执行任务——multiprocessing、Eventlet 和 Gevent。 然而,这项工作将集中精力于多处理方法的使用。 而且,只需要通过配置就能实现进程间的互相通信,它将作为一项课题研究,以便读者能够与他/她自己的实验进行比较。 26 | 27 | Celery模块可以在官方的项目页面{target="_blank"}得到。 28 | -------------------------------------------------------------------------------- /docs/chapter1/exploring_common_forms_of_parallelization.md: -------------------------------------------------------------------------------- 1 | # 探索并行化的几种模式 2 | 3 | 当我们试图定义**并行系统**的主要方案时,有疑惑很正常。常常会有人提到**并发系统**和**并行系统**,这两个术语看起来像是讲的同一件事。然而实际上有着轻微的差异。 4 | 5 | ## 并发编程 6 | 7 | 在**并发编程**中,我们有一个场景,一个程序派发了几个 workers,这些工人争着使用 CPU 来运行一个任务。 在争执发生的阶段由 CPU 调度器控制,其功能是定义在特定时刻哪个 worker 适合使用资源。 在大多数情况下,**CPU** 调度程序运行进程的排序任务非常快,以至于我们可能会产生伪并行的印象。 因此,**并发编程**是**并行编程**的抽象。 8 | 9 | !!! info "" 10 | 11 | 并行系统争夺同一CPU来运行任务 12 | 13 | 下图显示了一个**并发编程**方案: 14 | 15 | ![1](../imgs/1-01.png) 16 | 17 | ## 并行编程 18 | 19 | **并行编程**可以被定义为一种方案,在这种方案中,程序数据创建workers以在多核环境中同时运行特定任务,而无需在他们之间并发访问 CPU。 20 | 21 | !!! info "" 22 | 23 | 并行系统同时运行任务。 24 | 25 | 下面的图显示了**并行编程**的概念: 26 | 27 | ![1](../imgs/1-02.png) 28 | 29 | ## 分布式编程 30 | 31 | **分布式编程**旨在通过在物理上分离的计算机(节点)之间交换数据来共享处理的可能性。 32 | 33 | 由于多种原因,**分布式编程**变得越来越流行; 他们的探索如下: 34 | 35 | * **容错性**(Fault-tolerance):由于系统是分散的,我们可以将处理分配给网络中的不同机器,从而在不影响整个系统功能的情况下对特定机器进行单独维护。 36 | * **横向扩展**(Horizontal scalability):一般来说,我们可以增加分布式系统的处理能力。 我们可以连接新设备而无需中止正在执行的应用程序。 可以说,与垂直可扩展性相比,它更便宜、更简单。 37 | * **云计算**:随着硬件成本的降低,我们需要这类业务的增长,我们可以获得以共同合作方式运行的大型机器集群,并以对用户透明的方式运行程序。 38 | 39 | > 分布式系统在物理上分离的节点中运行任务。 40 | 41 | 下图显示了一个分布式系统的方案: 42 | 43 | ![1](../imgs/1-03.png) 44 | -------------------------------------------------------------------------------- /docs/chapter1/identifying_parallel_programming_problems.md: -------------------------------------------------------------------------------- 1 | # 识别并行编程的问题 2 | 3 | 勇敢的键盘战士在并行编程幽灵居住的土地上作战时可能会遇到一些经典问题。 当没有经验的程序员使用结合了共享状态的 worker 时,许多这些问题会更频繁地发生。 其中一些问题将在以下各节中进行描述。 4 | 5 | ## 死锁 6 | 7 | **死锁**(Deadlock)是指两个或多个 worker 无限期地等待资源释放的情况,由于某种原因,该资源被同一组的 worker 阻塞。 为了更好地理解,我们将使用另一个真实案例。 想象一下入口处有一扇旋转门的银行。 客户 A 转向一侧,这将允许他进入银行,而客户 B 试图通过这个旋转门的入口侧离开银行,这样两个客户都会被困在推门处,但无处可去。 这种情况在现实生活中会很滑稽,但在编程中会很悲惨。 8 | 9 | !!! info "" 10 | 11 | **死锁**(Deadlock)是一种现象,其中进程等待释放任务条件的发生,但这种情况永远不会发生 12 | 13 | ## 饥饿 14 | 15 | 这个问题是由于一个或者多个进程不公平的竞争所引起的副作用,这会花费更多的时间来执行任务。想象有一组进程,A进程正在执行繁重的任务,而且这个任务还有数据处理优先级。现在,想象一下,高优先级的进程A持续不断的占用CPU,而低优先级的进程B将永远没有机会。因此可以说进程B在CPU周期中是饥饿的。 16 | 17 | > **饥饿**(Starvation)是由于进程排名策略调整不当造成的。 18 | 19 | ## 竞态条件 20 | 21 | 当一个进程的结果取决于执行结果的顺序,而这个顺序由于缺乏同步机制而被打破时,我们就会面临竞态条件。 它们是由在大型系统中极难过滤的问题引起的。 例如,一对夫妇有一个联名账户; 操作前的初始余额为 100 美元。 下表显示了常规情况,其中有保护机制和预期事实的顺序,以及结果: 22 | 23 | !!! info "常规操作而不会出现静态条件" 24 | 25 | | 丈夫 | 妻子 | 账户余额(美元) | 26 | | -------- | -------- | ---------------- | 27 | | | | 100 | 28 | | 读取余额 | | 100 | 29 | | 存款20 | | 100 | 30 | | 结束操作 | | 120 | 31 | | | 读取余额 | 120 | 32 | | | 取款10 | 120 | 33 | | | 结束操作 | 110 | 34 | 35 | 在下表中,有问题的场景出现了。假设账户没有同步机制,并且操作的顺序也和预期不一样。 36 | 37 | !!! info "类比在联合账户和竞争条件下平衡问题" 38 | 39 | | 丈夫 | 妻子 | 账户余额(美元) | 40 | | --------------------- | --------------------- | ---------------- | 41 | | | | 100 | 42 | | 读取余额 | | 100 | 43 | | 取款100 | | 100 | 44 | | | 读取余额 | 100 | 45 | | | 取款10 | 100 | 46 | | 结束操作
更新余额 | | 0 | 47 | | | 结束操作
更新余额 | 90 | 48 | 49 | 由于在操作的顺序中意外的缺乏同步,最终结果存在明显的不一致。并行编程的特征之一是不确定性。无法预见两个 worker 将在什么时候运行,甚至谁先运行。 因此,同步机制必不可少。 50 | 51 | !!! info "" 52 | 53 | **不确定性**(Non-determinism),如果与缺乏同步机制相结合,可能会导致竞争条件问题 54 | -------------------------------------------------------------------------------- /docs/chapter1/index.md: -------------------------------------------------------------------------------- 1 | # 并行、并发以及分布式编程的对比分析 2 | 3 | 并行编程可以被定义为一种模型,这个模型旨在创造一种能与**被准备用于同时执行代码指令的环境相兼容**的程序。并行技术被用于软件开发还不是很长。几年前,处理器在其他组件中只有一个**算术逻辑单元** (**ALU**),它在一个时间空间内一次只能执行一条指令。 多年来,只考虑了一个以**赫兹**(Hz)为单位的时钟,以确定处理器在给定时间间隔内可以处理的指令数。 时钟数量越多,就 **KHz**(每秒千次操作)、**MHz**(每秒百万次操作)和当前的 **GHz**(每秒十亿次操作)而言,可能执行的指令越多。 4 | 5 | 总而言之,每个周期提供给处理器的指令越多,执行速度就越快。 在80 年代,*Intel 80386* 出现了革命性的处理器,它允许以先发制人的方式执行任务,也就是说,可以定期中断一个程序的执行,为另一个程序提供处理器时间; 这意味着基于*时间分片*(time-slicing)的**伪并行**(pseudo-parallelism)。 6 | 7 | 在 80 年代后期,*Intel 80486* 实现了流水线系统(pipelining system),实际上将执行阶段划分为不同的子阶段。 实际上,在处理器的一个周期中,我们可以在每个子阶段同时执行不同的指令。 8 | 9 | 上一节中提到的所有进步都导致了性能的多项改进,但这还不够,因为我们面临着一个微妙的问题,最终会成为所谓的**摩尔定律** ({target="_blank"}). 10 | 11 | 探寻满足时钟高能耗的过程最终与物理限制发生冲突; 处理器会消耗更多的能量,从而产生更多的热量。 此外,还有另一个同样重要的问题:便携式计算机市场在 20 世纪 90 年代加速发展。 因此,拥有能够使这些设备的电池在远离插头的地方持续足够长的时间的处理器是极其重要的。 来自不同制造商的多种技术和处理器系列诞生了。 在服务器和大型机方面,Intel 值得一提的是其 Core(R 产品系列,即使只有一个物理芯片,它也可以通过模拟多个处理器的存在来欺骗操作系统。 12 | 13 | 在 Core(R)系列中,处理器进行了重大的内部更改,并采用了称为核心(core)的组件,这些组件具有自己的 **ALU** 和缓存 **L2** 和 **L3**,以及执行指令的其他元素。 这些核心,也称为**逻辑处理器**(logical processors),允许我们同时并行执行同一程序的不同部分,甚至不同的程序。 *age core* 通过优于其前身的功率处理实现了更低的能源使用。 由于内核并行工作,模拟独立的处理器,我们可以拥有一个多核芯片和一个较低的时钟,从而根据任务获得比具有更高时钟的单核芯片更好的性能。 14 | 15 | 当然,如此多的改进已经改变了我们进行软件设计的方式。 今天,我们必须考虑并行性来设计合理利用资源而不浪费资源的系统,从而为用户提供更好的体验并节省个人计算机和处理中心的资源。 **并行编程**比以往任何时候都更多地出现在开发人员的日常生活中,而且显然,它永远不会倒退。 16 | 17 | 本章节包含以下几个主题: 18 | 19 | * 为什么使用并行编程? 20 | * 介绍并行化的常见形式 21 | * 在并行编程中通信 22 | * 识别并行编程的问题 23 | * 发现Python的并行编程工具 24 | * 小心Python的**全局解释器锁**(Global Interpreter Lock - GIL) 25 | -------------------------------------------------------------------------------- /docs/chapter1/summary.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 | 在这一章节,我们学了一些并行编程的概念,并学习了一些模型的优点以及缺点。在谈到并行性的时候对于一些问题或者潜在问题进行了一些简短的解释。我们也对Python的一些内置和外部的模块进行了简短的介绍,这让开发人员在建立并行系统时会更加轻松。 4 | 5 | 在下一章节,我们会学习一些设计并行算法的技术。 6 | -------------------------------------------------------------------------------- /docs/chapter1/taking_care_of_GIL.md: -------------------------------------------------------------------------------- 1 | # 小心Python GIL 2 | 3 | **GIL** 是一种用于实现标准 Python(称为 CPython)的一种机制,以避免由不同线程同时执行字节码。 Python 中 **GIL** 的存在是该语言用户之间激烈讨论的一个原因。 选择 **GIL** 是为了保护 **CPython** 解释器使用的内部内存,它没有为线程的并发访问实现同步机制。 无论如何,当我们决定使用线程时,**GIL** 会导致问题,而这些线程往往受 **CPU** 限制。 例如,**I/O** 线程不在 **GIL** 的范围之内。 也许该机制对 Python 的演变带来的好处多于对它的伤害。 显然,我们不能仅将速度视为判断某事好坏的单一论据。 4 | 5 | 在某些情况下,使用多进程配合**消息传递**能更好的平衡可维护性、可扩展性以及性能之间的关系。 可即便如此,在某些情况下确实需要线程,这将被 **GIL** 制服。 在这些情况下,可以做的就是用C语言编写一些代码作为扩展,并将它们嵌入到Python程序中。 因此,还有其他选择; 由开发人员分析真正的必要性。 那么,问题来了:一般来说,**GIL** 是反派吗?重要的是要记住,**PyPy** 团队正在研究 **STM** 实现,以便从 **Python** 中删除 **GIL**。 有关该项目的更多详细信息,请访问 {target="_blank"}。 6 | -------------------------------------------------------------------------------- /docs/chapter1/why_use_parallel_programming.md: -------------------------------------------------------------------------------- 1 | # 为什么使用并行编程 2 | 3 | 自从计算系统发展以来,它们已经开始提供一个能使我们以并行的方式运行特定程序的独立部分的机制,从而增强响应和总体性能。 此外,我们可以很容易的就验证配备有多个处理器以及多核的机器。那么,为什么不利用这种架构呢? 4 | 5 | 并行编程在所有系统开发环境中都是可实现的,从智能手机和平板电脑到研究中心的重型计算。 并行编程的坚实基础将使开发人员能够优化应用程序的性能。 这会增强用户体验以及计算资源的消耗,从而减少完成复杂任务的处理时间。 6 | 7 | 举一个并行性的例子,让我们想象一个场景,在这个场景中,有一些任务,其中一个任务是从数据库中检索一些信息,而这个数据库规模又很大。再假如,这个应用还需要顺序执行,在这个应用中,这些任务必须以一定的逻辑顺序,一个接一个的执行。当用户请求数据时,在返回的数据没有结束之前,其它系统将一直被阻塞。然而,利用并行编程,我们将会创造一个新的worker来在数据库中查询信息,而不会阻塞这个应用的其它功能,从而提高它的使用。 8 | 9 | 举一个并行性的例子,让我们想象一个场景,在这个场景中,除了其他任务之外,应用程序还从数据库中查询信息,并且这个数据库有相当大的规模。 还要考虑,应用程序中的任务是按逻辑顺序一个接一个地执行的。 当用户请求数据时,系统的其余部分将被阻塞,直到数据返回未结束。 然而,利用并行编程,我们将被允许创建一个新的 **worker**,它将在这个数据库中查找信息而不阻塞应用程序中的其他功能,从而增强它的使用。 10 | -------------------------------------------------------------------------------- /docs/chapter2/index.md: -------------------------------------------------------------------------------- 1 | # 设计并行算法 2 | 3 | 在开发并行系统时,在你编码之前,有几个方面你必须要留意。为了在任务中获得成功,从一开始将并行的问题列出来是至关重要的。在这一章节,我们将接触一些技术方面的解决方案。 4 | 5 | 这个章节将包含一些几个议题: 6 | 7 | - 分治技术 8 | - 数据分解 9 | - 用管道分解任务 10 | - 处理和映射 11 | -------------------------------------------------------------------------------- /docs/chapter2/使用数据分解.md: -------------------------------------------------------------------------------- 1 | # 使用数据分解 2 | 3 | 并行化问题的方法之一是通过**数据分解**。想象一下有这么一个场景,在这个场景中我们要以标量4乘以一个2x2矩阵(这个矩阵被称为矩阵A).在一个顺序执行系统中,我们将一个接一个的执行每个乘法的操作,最后生成所有指令的最终结果。根据矩阵A的大小,这个问题的顺序解决方案可能是旷日持久的。然而,当数据分解被应用的时候,我们可以想象矩阵A被分解为一个一个小的部分,这些分片数据被相关的workers以并行的方式接受并处理。下图以一个2x2矩阵乘以一个标量值的例子说明了数据分解应用的概念: 4 | 5 | ![1](../imgs/2-02.png) 6 | 7 | 上图中出现的矩阵相乘的问题有一定的对称性,每个必要的操作的结果是由一个单独的worker执行的,而且每个worker执行同样数量的操作来解决问题。然而,在现实世界中,worker的数量和已分解的数据数量的关系是不对称的,这将直接影响解决方案的性能。最后,每个worker所产生的结果必须整合起来以便使程序最终输出意义结果。为了进行这种整合,workers之间需要进行**信息交换**或是**共享状态**。 8 | 9 | !!! info "" 10 | 11 | 数据分解的粒度选择可能会影响解决方案的性能。 12 | -------------------------------------------------------------------------------- /docs/chapter2/分治技术.md: -------------------------------------------------------------------------------- 1 | # 分治技术 2 | 3 | 当你面对一个复杂的问题时,要做的第一件事就是分解问题,以确定可以独立处理的部分。 通常,解决方案中的可**并行化部分**是可以分割和分布式处理的部分,以便由不同的worker处理。 分而治之的技术涉及**递归**地分割**域**(domain),直到找到并解决完整问题的不可分割的单元。 **归并排序**、**快速排序**等排序算法都可以通过这种方式解决。 4 | 5 | 下图显示了**归并排序**在六个元素的向量中的应用,可以看到使用了**分而治之技术**: 6 | 7 | ![1](../imgs/2-01.png) 8 | -------------------------------------------------------------------------------- /docs/chapter2/处理和映射.md: -------------------------------------------------------------------------------- 1 | # 处理和映射 2 | 3 | workers的数量并不足以在一个单一的步骤里解决一个特定的问题。因此前面的章节给出的分解技术是很有必要的。然而分解技术不应该被随意的应用,有一些因素会影响解决方案的性能。在数据或者任务分解之后,我们应该问这么一个问题:“我们应该在workers中如何划分进程的负载来获得比较好的性能?”这不是一个很好回答的问题,因为这取决于正在研究的问题。 4 | 5 | 基本上,我们在定义过程映射时可以提到两个重要的步骤: 6 | 7 | * 识别独立的任务 8 | * 识别需要数据交换的任务 9 | 10 | ## 识别独立的任务 11 | 12 | 在系统中识别独立的任务将允许我们在不同的workers之间分配任务,因为这些任务不需要持续的通信。因为不需要一个数据单元,所以任务可以在不同的workers间执行而不会影响其它任务的执行。 13 | 14 | ## 识别需要数据交换的任务 15 | 16 | 将需要相互通讯的任务组合起来放到单个worker中可以提高性能。当有大的通信负载的时候的时候这个真的可以提高性能,因为它能减少任务间信息交换的开销。 17 | 18 | ## 平衡负载 19 | 20 | 在并行解决方案中一个典型的问题是如何为不同的工作单元分配计算资源。我们**越是将任务分配给不同的workers处理,我们将是需要越多的通信**。另一方面,我们越是将任务组合起来分配給一个worker,与通信相关的开销越小。然而,我们可能会**增加空转**,也就是说,**浪费了计算的能力**。在并行编程中,浪费并不好。此外,越是将数据聚合在一个worker中,越会减少通过简单的添加更多的设备而增加计算能力的可扩展的灵活性。在一个**基于通信的架构**(轻微的数据聚合)中,**为集群或者网格简单的增加机器从而提升处理性能甚至不需要中断正在运行的系统。** 21 | -------------------------------------------------------------------------------- /docs/chapter2/总结.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 | 在这一章节,我们讨论了一些创建并行解决方案的方法。你的注意力应重点放在在不同的workers之间划分处理负载,考虑聚合而不是数据。 4 | 5 | 在下一章节,我们将学习如何识别并行的问题。 6 | -------------------------------------------------------------------------------- /docs/chapter2/用管道分解任务.md: -------------------------------------------------------------------------------- 1 | # 用管道分解任务 2 | 3 | **管道技术**用于必须以协作方式执行以解决问题而组织任务的一种技术。 **管道**(pipeline)将大型任务分解为以并行方式运行的较小的独立任务。 管道模型可以比作汽车工厂的装配线,其中底盘是输入的原材料。 随着原材料经过不同的生产阶段,几个worker依次执行不同的动作,直到过程结束,这样我们才能准备好汽车。 该模型与开发的顺序范式非常相似; 任务一个接一个地对数据执行,通常,一个任务得到一个输入,这是前一个任务的结果。 那么这个模型与顺序技术有什么区别呢? 管道技术的每个阶段都有自己的worker,并且他们以并行的方式处理问题。 4 | 5 | 计算上下文中的一个示例可能是系统批量处理图像并将提取到数据库中的数据持久化。 我们将得到以下事实序列: 6 | 7 | * 接受输入的图像并且以对这些图片以并行的方式进行排列,这些图片将在第二阶段进行处理 8 | * 解析图像,并且有用的信息将会被送到第三阶段 9 | * 在第三阶段,过滤器被并行的应用在图像上 10 | * 来自第三阶段的数据结果被保存在数据库中 11 | 12 | !!! info "" 13 | 14 | **管道技术**的每个阶段都以独立的方式与自己的worker一起执行。 但是,它建立了数据通信机制,以便可以交换信息。 15 | 16 | 下图展示了管道的概念: 17 | 18 | ![1](../imgs/2-03.png) 19 | -------------------------------------------------------------------------------- /docs/chapter3/index.md: -------------------------------------------------------------------------------- 1 | # 识别一个可并行的问题 2 | 3 | 前一章我们从不同角度探讨了并行方面的一些问题。现在我们将分析一些具体的问题,这些将在具体实现时自始至终会对我们有指导的作用。 4 | 5 | 这个章节包含以下的议题: 6 | 7 | * 获得有多个输入的斐波那契最高的值 8 | * 网页爬虫 9 | -------------------------------------------------------------------------------- /docs/chapter3/从多个输入中得到斐波那契最大的值.md: -------------------------------------------------------------------------------- 1 | # 从多个输入中得到斐波那契最大的值 2 | 3 | 众所周知,斐波那契数列被定义如下: 4 | 5 | F(n)={0, if n=01, if n=1F(n1)+F(n2),ifn>1 6 | 7 | 实际上,按照从0到10计算斐波那契的值,结果将会是0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 和55. 8 | 9 | 用迭代法计算斐波那契最高的值的Python代码如下: 10 | 11 | ```python 12 | def fibonacci(input): 13 | a, b = 0, 1 14 | for item in range(input): 15 | a, b = b, a + b 16 | return a 17 | ``` 18 | 19 | 斐波那契函数为一个特定的输入的数据计算一个最大的斐波那契值。让我们想象一个场景,在这个场景中,需要计算斐波那契的值,而本网站将会从一个用户那里接收到几个输入。假设用户提供一个数组的值作为输入,因此使这些计算按顺序将会很有趣。但是假使是100万个用户同时发出请求那么将会怎么样?在这种情况下,一些用户不得不等待很长一段时间才能得到他们的答案。 20 | 21 | 让我们只考虑先前文中的Python实现的斐波那契函数代码。我们怎么在有一个数组数据输入的情况下使用并行性来实现它?在前一章已经展示过多种实现的方式,这里我们使用其中一种方式-**数据分解**。我们可以将数组分为几个单元,每个单元关联一个任务然后被一个worker执行。下图描述了一个建议的解决方案: 22 | 23 | ![1](../imgs/3-01.png) 24 | 25 | !!! info "" 26 | 27 | 作为对读者的建议,请完成实施缓存计算值机制的练习,以避免浪费 CPU 时间。 我们推荐像 [memcached](http://memcached.org/){target="_blank"} 这样的缓存数据库。 28 | -------------------------------------------------------------------------------- /docs/chapter3/总结.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 | 在这一章节,我们学习了并行中出现的常见问题以及可能的解决方案。下面将会展示任何使用不同的Python库来解决上面展示的问题。 所提出的问题下面将使用不同的并行 Python 库来实现解决方案。 4 | 5 | 在下一章节,当使用`threading`模块,我们专注于的线程的解决方案,在用到`mutliprocess`模块时,解决方案也会涉及到使用多进程,以此类推。 6 | -------------------------------------------------------------------------------- /docs/chapter3/爬取网页.md: -------------------------------------------------------------------------------- 1 | # 爬取网页 2 | 3 | 本书要研究的另一个问题是并行网络爬虫的实现。 网络爬虫由浏览网络以搜索页面信息的计算机程序组成。 要分析的场景是一个序列网络爬虫由可变数量的**统一资源定位器**(Uniform Resource Locators - URLs)提供的问题,它必须搜索提供的每个 URL 中的所有链接。 假设输入的 URL 数量可能比较多,我们可以通过以下方式规划一个寻找并行性的解决方案: 4 | 5 | 1. 将所有的**URLs**分成组组合到一个数据结构中。 6 | 2. 把这些**URLs**分配給多个任务,这样写任务会爬取每个URL中的包含信息。 7 | 3. 将这些任务分派给多个并行的**workers**来执行。 8 | 4. 前一阶段的结果必须传递到下一阶段,这将改进原始收集的数据,从而保存它们并将它们与原始 **URL** 相关联。 9 | 10 | 正如我们在提议的解决方案的编号步骤中所观察到的,存在以下两种方法的组合: 11 | 12 | * **数据分解**:这发生在我们划分和关联URLs到任务上。 13 | * **用管道进行任务分解**:这包含三个阶段的管道,这发生在我们**链接接收**、**存储**以及**组织爬取的结果**的任务。 14 | 15 | 下图显示了解决方案: 16 | 17 | ![1](../imgs/3-02.png) 18 | -------------------------------------------------------------------------------- /docs/chapter4/crawling_the_web_using_the_concurrent_futures_module.md: -------------------------------------------------------------------------------- 1 | # 使用concurrent.futures模块爬取web信息 2 | 3 | 下一节将通过实现并行 Web 爬虫来使用我们的代码。 在此方案中,我们将使用一个非常有趣的 **Python** 资源,即 `concurrent.futures` 模块中的 `ThreadPoolExecutor`。 在前面的示例中,我们分析了 `parallel_fibonacci.py`,使用了非常原始的线程形式。 此外,在特定时刻,我们不得不手动创建和初始化多个线程。 在较大的程序中,很难管理这种情况。 在这种情况下,有一些机制允许建立一个线程池。 线程池只不过是一个数据结构,它保留了多个先前创建的线程,供某个进程使用。 它旨在**重用线程**,从而避免不必要的线程创建——这是昂贵的。 4 | 5 | 基本上,如前一章所述,我们将有一个算法将分阶段执行一些任务,这些任务相互依赖。 在这里,我们将研究并行网络爬虫的代码。 6 | 7 | 导入一些模块并设置日志文件后,我们使用名为 `re` 的内置模块创建了一个**正则表达式**(有关此模块的完整文档可在 {target="_blank"})。 我们将使用它来过滤从抓取阶段返回的页面中的链接。 代码如下: 8 | 9 | ```python 10 | html_link_regex = re.compile('') 11 | ``` 12 | 13 | 接下来我们创建一个同步队列来模拟输入数据. 然后我们创建一个名为`result_dict`的字典实例. 在此,我们会将 URL 及其各自的链接关联为列表结构。 相关代码如下: 14 | 15 | ```python 16 | urls = queue.Queue() 17 | urls.put('http://www.google.com') 18 | urls.put('http://br.bing.com/') 19 | urls.put('https://duckduckgo.com/') 20 | urls.put('https://github.com/') 21 | urls.put('http://br.search.yahoo.com/') 22 | result_dict = {} 23 | ``` 24 | 25 | 再接下来我们定义一个名为`group_urls_task`的函数,该函数用于从同步队列中抽取出URL并存入`result_dict`的key值中. 另一个应该留意的细节是,我们调用`Queue`的`get`方法是,带了两个参数,第一个参数为`True`表示阻塞其他线程访问这个同步队列,第二个参数是`0.05`表示阻塞的超时时间,这样就防止出现由于同步队列中没有元素而等待太长时间的情况出现. 毕竟,在某些情况下,你不会想化太多的时间来等待新元素的到来. 相关代码如下: 26 | 27 | ```python 28 | def group_urls_task(urls): 29 | try: 30 | url = urls.get(True, 0.05) # true表示阻塞其他线程访问这个队列,0.05表示阻塞的超时时间 31 | result_dict[url] = None 32 | logger.info("[%s] putting url [%s] in dictionary..." % (threading.current_thread().name, url)) 33 | except queue.Empty: 34 | logging.error('Nothing to be done, queue is empty') 35 | ``` 36 | 37 | 现在,我们有了负责完成每个作为参数发送给 `crawl_task` 函数的 `URL` 的抓取阶段的任务。 基本上,抓取阶段是通过获取接收到的 `URL` 指向的页面内的所有链接来完成的。 爬取返回的元组包含第一个元素作为 `crawl_task` 函数接收的 `URL`。 第二步,提取链接列表。 `requests`模块(关于`request`模块的官方文档可以在{target="_blank"}找到)用于从URL获取网页。 代码如下: 38 | 39 | ```python 40 | def crawl_task(url): 41 | links = [] 42 | try: 43 | request_data = requests.get(url) 44 | logger.info("[%s] crawling url [%s] ..." % ( 45 | threading.current_thread().name, url)) 46 | links = html_link_regex.findall(request_data.text) 47 | except: 48 | logger.error(sys.exc_info()[0]) 49 | raise 50 | finally: 51 | return (url, links) 52 | ``` 53 | 54 | 进一步分析代码,我们将看到创建了一个 `ThreadPoolExecutor` 对象(有关 `ThreadPoolExecutor` 对象的更多信息,请访问 {target="_blank"} ) 在 `concurrent.futures` 模块中有特色。 在这个 `ThreadPoolExecutor` 对象的构造函数中,我们可以定义一个名为 `max_workers` 的参数, 该参数决定了线程池中的线程数。 在从同步队列中删除 `URL` 并将键插入到 `result_dict` 的阶段,选择是使用三个工作线程。 数量将根据问题而有所不同。 在定义 `ThreadPoolExecutor` 并使用 `with` 语句来保证结束线程之后,这些线程将在 `with` 语句范围的输出中执行。 在 `ThreadPoolExecutor` 对象的范围内,我们在同步队列中对其进行迭代,并通过 `submit` 方法分派它为包含 `URL` 的队列线程执行引用。 总而言之,`submit` 方法为线程的执行安排了一个可调用对象,并返回一个包含为执行创建的安排的 `Future` 对象。 `submit` 方法接收一个可调用对象及其参数; 在我们的例子中,可调用项是任务 `group_urls_task`,参数是对同步队列函数的引用。 调用这些参数后,池中定义的工作线程将以并行、异步的方式执行预订。 代码如下: 55 | 56 | ```python 57 | with concurrent.futures.ThreadPoolExecutor(max_workers=3) as group_link_threads: 58 | for i in range(urls.qsize()): 59 | group_link_threads.submit(group_urls_task, urls) 60 | ``` 61 | 62 | 在前面的代码之后,我们又创建了一个`ThreadPoolExecutor`; 但是这一次,我们要使用上一阶段`group_urls_task`生成的`key`来运行爬虫阶段。 这一次我们所使用的代码有些不同: 63 | 64 | ```python 65 | future_tasks = { 66 | crawler_link_threads.submit(crawl_task, url): url 67 | for url in result_dict.keys()} 68 | ``` 69 | 70 | 我们映射了一个名为`future_tasks`的临时字典。它将包含`submit`的结果,通过`result_dict`中的每个`URL`来完成。也就是说,对于每个`key`,我们在`future_tasks`中创建一个条目。在映射之后,我们需要收集`submit`的结果,因为它们是用一个循环执行的,它使用`concurrent.futures.as_completed(fs,timeout=None)`方法在`future_tasks`中寻找已完成的条目。这个调用返回一个`Future`对象实例的迭代器。因此,我们可以在已经派发的预订所处理的每个结果中进行迭代。在`ThreadPoolExecutor`的最后,对于爬行线程,我们使用`Future`对象的`result()`方法。在抓取阶段的情况下,它返回结果元组。通过这种方式,我们在`future_tasks`中生成最后的条目,如下面的截图所示。 71 | 72 | ```shell 73 | $ python temp2.py 74 | 2023-03-01 15:53:51,289 - [ThreadPoolExecutor-0_0] putting url [http://www.google.com] in dictionary... 75 | 2023-03-01 15:53:51,289 - [ThreadPoolExecutor-0_1] putting url [http://br.bing.com/] in dictionary... 76 | 2023-03-01 15:53:51,290 - [ThreadPoolExecutor-0_0] putting url [https://duckduckgo.com/] in dictionary... 77 | 2023-03-01 15:53:51,290 - [ThreadPoolExecutor-0_2] putting url [https://github.com/] in dictionary... 78 | 2023-03-01 15:53:51,290 - [ThreadPoolExecutor-0_1] putting url [http://br.search.yahoo.com/] in dictionary... 79 | 2023-03-01 15:53:51,334 - Starting new HTTP connection (1): 127.0.0.1:7890 80 | 2023-03-01 15:53:51,408 - Starting new HTTPS connection (1): duckduckgo.com:443 81 | 2023-03-01 15:53:51,411 - Starting new HTTP connection (1): 127.0.0.1:7890 82 | 2023-03-01 15:53:51,584 - http://127.0.0.1:7890 "GET http://www.google.com/ HTTP/1.1" 200 6588 83 | 2023-03-01 15:53:51,585 - [ThreadPoolExecutor-1_0] crawling url [http://www.google.com] ... 84 | 2023-03-01 15:53:51,621 - Starting new HTTPS connection (1): github.com:443 85 | 2023-03-01 15:53:51,625 - http://127.0.0.1:7890 "GET http://br.bing.com/ HTTP/1.1" 302 0 86 | 2023-03-01 15:53:51,628 - Resetting dropped connection: 127.0.0.1 87 | 2023-03-01 15:53:51,704 - https://duckduckgo.com:443 "GET / HTTP/1.1" 200 None 88 | 2023-03-01 15:53:51,706 - [ThreadPoolExecutor-1_2] crawling url [https://duckduckgo.com/] ... 89 | 2023-03-01 15:53:51,822 - Starting new HTTP connection (1): 127.0.0.1:7890 90 | 2023-03-01 15:53:51,894 - http://127.0.0.1:7890 "GET http://www.bing.com/?cc=br HTTP/1.1" 200 None 91 | 2023-03-01 15:53:51,962 - https://github.com:443 "GET / HTTP/1.1" 200 None 92 | 2023-03-01 15:53:51,978 - [ThreadPoolExecutor-1_1] crawling url [http://br.bing.com/] ... 93 | 2023-03-01 15:53:52,045 - [ThreadPoolExecutor-1_0] crawling url [https://github.com/] ... 94 | 2023-03-01 15:53:52,223 - http://127.0.0.1:7890 "GET http://br.search.yahoo.com/ HTTP/1.1" 301 25 95 | 2023-03-01 15:53:52,225 - Starting new HTTPS connection (1): br.search.yahoo.com:443 96 | 2023-03-01 15:53:52,697 - https://br.search.yahoo.com:443 "GET / HTTP/1.1" 200 17530 97 | 2023-03-01 15:53:52,859 - [ThreadPoolExecutor-1_2] crawling url [http://br.search.yahoo.com/] ... 98 | ``` 99 | 100 | 又一次,我们可以发现每个线程池中的线程执行是乱序的,但这不重要,重要的是,`resul\_dict`中输出的内容就是最终结果. 101 | 102 | ## 完整代码 103 | 104 | 译者注: 105 | 106 | ```python 107 | import sys 108 | import re 109 | import logging, threading 110 | import queue 111 | from concurrent.futures import ThreadPoolExecutor 112 | 113 | import requests 114 | 115 | logger = logging.getLogger() 116 | logger.setLevel(logging.DEBUG) 117 | formatter = logging.Formatter('%(asctime)s - %(message)s') 118 | 119 | ch = logging.StreamHandler() 120 | ch.setLevel(logging.DEBUG) 121 | ch.setFormatter(formatter) 122 | logger.addHandler(ch) 123 | 124 | html_link_regex = re.compile('') 125 | 126 | urls = queue.Queue() 127 | urls.put('http://www.google.com') 128 | urls.put('http://br.bing.com/') 129 | urls.put('https://duckduckgo.com/') 130 | urls.put('https://github.com/') 131 | urls.put('http://br.search.yahoo.com/') 132 | 133 | result_dict = {} 134 | 135 | def group_urls_task(urls): 136 | try: 137 | url = urls.get(True, 0.05) # true表示阻塞其他线程访问这个队列,0.05表示阻塞的超时时间 138 | result_dict[url] = None 139 | logger.info("[%s] putting url [%s] in dictionary..." % (threading.current_thread().name, url)) 140 | except queue.Empty: 141 | logging.error('Nothing to be done, queue is empty') 142 | 143 | def crawl_task(url): 144 | links = [] 145 | try: 146 | request_data = requests.get(url) 147 | logger.info("[%s] crawling url [%s] ..." % (threading.current_thread().name, url)) 148 | links = html_link_regex.findall(request_data.text) 149 | except: 150 | logger.error(sys.exc_info()[0]) 151 | raise 152 | finally: 153 | return (url, links) 154 | 155 | 156 | if __name__ == "__main__": 157 | 158 | with ThreadPoolExecutor(max_workers=3) as group_link_threads: 159 | for i in range(urls.qsize()): 160 | group_link_threads.submit(group_urls_task, urls) 161 | 162 | with ThreadPoolExecutor(max_workers=3) as crawler_link_threads: 163 | future_tasks = { 164 | crawler_link_threads.submit(crawl_task, url): url 165 | for url in result_dict.keys()} 166 | ``` 167 | -------------------------------------------------------------------------------- /docs/chapter4/defining_threads.md: -------------------------------------------------------------------------------- 1 | # 什么是线程 2 | 3 | **线程是进程中的不同执行线**。 让我们把一个程序想象成一个蜂巢,在这个蜂巢内部有一个收集花粉的过程。 这个采集过程是由几只工蜂同时工作来完成的,以解决花粉不足的问题。 工蜂扮演着线程的角色,在进程内部活动并共享资源来执行它们的任务。 4 | 5 | 线程属于同一个进程,共享同一个内存空间。 因此,开发人员的任务是控制和访问这些内存区域。 6 | 7 | ## 使用线程的优点和缺点 8 | 9 | 在决定使用线程时必须考虑一些**优点**和**缺点**,这取决于用于实现解决方案的语言和操作系统。 10 | 11 | 使用线程的**优势**如下所示: 12 | 13 | - 同一进程内的线程**通信**、**数据定位**、**共享信息**的速度快 14 | - 线程的创建比进程的创建成本更低,因为不需要复制主进程上下文中包含的所有信息 15 | - 通过处理器的高速缓存优化内存访问,充分利用**数据局部性**(data locality)的优势。 16 | 17 | 使用线程的**缺点**如下: 18 | 19 | - 数据共享允许快速通信。 但是,它也允许缺乏经验的开发人员引入难以解决的错误。 20 | - 数据共享限制了解决方案的灵活性。 例如,迁移到分布式架构可能会让人头疼。 通常,它们限制了算法的可扩展性。 21 | 22 | !!! info "" 23 | 24 | 在 Python 编程语言中,由于 GIL,使用**计算密集型**(CPU-bound)的线程可能会损害应用程序的性能。 25 | 26 | ## 理解不同类型的线程 27 | 28 | 有两种类型的线程,**内核线程**和**用户线程**。 **内核线程是由操作系统创建和管理的线程**, 其上**下文的交换**、**调度**和**结束**都由当前操作系统的内核来进行管理。 对于**用户线程**,这些状态由**包**(package)开发人员控制。 29 | 30 | 我们可以引用每种线程的一些优点: 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | 54 | 58 | 62 | 63 | 64 |
线程类型优点缺点
内核线程 44 | 一个内核线程其实就是一个进程. 因此即使一个内核线程被阻塞了,其他的内核线程仍然可以运行。
45 | 内核线程可以在不同的 CPU 上运行。 46 |
48 | 创建线程和线程间同步的消耗太大
49 | 实现依赖于平台 50 |
用户线程 55 | 用户线程的创建和线程间同步的开销较少
56 | 用户线程是平台无关的。 57 |
59 | 同一进程中的所有用户线程都对应一个内核线程. 因此,若该内核线程被阻塞,则所有相应的用户线程都会被阻塞。
60 | 不同用户线程无法运行在不同CPU上 61 |
65 | 66 | ## 线程的状态 67 | 68 | 线程的生命周期有五种可能的状态。它们如下: 69 | 70 | - **新建**(Creation): 该过程的主要动作就是创建一个新线程, 创建完新线程后,该线程被发送到待执行的线程队列中。 71 | - **运行**(Execution): 该状态下,线程获取到并消耗CPU资源。 72 | - **就绪**(Ready): 该状态下,线程在待执行的线程队列中排队,等待被执行 73 | - **阻塞**(Blocked): 该状态下,线程由于等待某个事件(例如I/O操作)的出现而被阻塞. 这时线程并不使用CPU。 74 | - **死亡**(Concluded): 该状态下,线程释放执行时使用的资源并结束整个线程的生命周期。 75 | 76 | ## 是使用threading模块还是_thread模块 77 | 78 | Python提供了两个模块来实现基于系统的线程: **`_thread`模块**(该模块提供了使用线程相关的较低层的API; 它的文档可以在 找到)和**`threading`模块**(该模块提供了使用线程相关的更高级别的API; 它的文档可以在 中找到). **`threading`模块**提供的接口要比**`_thread`模块**的结构更友好一些. 至于具体选择哪个模块取决于开发者, 如果开发人员发现在较低级别使用线程很容易,实现他们自己的线程池并拥抱锁和其他原始功能(features),他/她宁愿使用`_thread`。 否则,`threading`是最明智的选择。 79 | -------------------------------------------------------------------------------- /docs/chapter4/index.md: -------------------------------------------------------------------------------- 1 | # 使用threading和concurrent.futures模块 2 | 3 | 在上面的章节里,我们列举了一些能够被并行化解决的潜在问题. 在本章中,我们会分析如何使用Python中的threading模块来解决这些问题. 4 | 5 | 本章包含如下议题: 6 | 7 | - 定义什么是线程 8 | - 选择使用threading库还是_thread库 9 | - 使用threading模块来为多个输入同时计算Fibonacci序列 10 | - 使用concurrent.futures模块爬取web信息 11 | -------------------------------------------------------------------------------- /docs/chapter4/using_threading_to_obtain_the_Fibonacci_series_term_with_multiple_inputs.md: -------------------------------------------------------------------------------- 1 | # 使用多线程解决斐波那契序列多输入问题 2 | 3 | 现在是时候实现了。任务是在给定多个输入值时并行执行斐波那契数列的各项。 出于教学目的,我们将固定四个元素中的输入值和四个线程来处理每个元素,模拟 worker 和要执行的任务之间的完美对称。 该算法将按如下方式工作: 4 | 5 | 1. 首先使用一个列表存储四个待输入值,这些值将被放入对于线程来说互相锁定的数据结构。 6 | 2. 输入值被放入可被锁定的数据结构之后,负责处理斐波那契序列的线程将被告知可以被执行。这时,我们可以使用python线程的同步机制`Condition`模块(`Condition`模块对共享变量提供线程之间的同步操作),模块详情请参考:{target="_blank"}。 7 | 3. 当每个线程结束斐波那契序列的计算后,分别把结果存入一个字典。 8 | 9 | 接下来我们将列出代码,并且讲述其中有趣的地方: 10 | 11 | 代码开始处我们加入了对编码额外的支持,导入`logging`, `threading`和`queue`模块。此外,我们还定义了我们例子中用到主要数据结构。 12 | 一个字典,被命名为`fibo_dict`,将用来存储输入输出数据,输入数据为`key`,计算结果(输出数据)为值。 13 | 我们同样定义了一个队列对象,该对象中存储线程间的共享数据(包括读写)。 14 | 我们把该对象命名为`shared_queue`。最后我们定义一个列表模拟程序的四个输入值。代码如下: 15 | 16 | ```python 17 | #coding: utf-8 18 | 19 | import logging, threading 20 | 21 | from queue import Queue 22 | 23 | logger = logging.getLogger() 24 | logger.setLevel(logging.DEBUG) 25 | formatter = logging.Formatter('%(asctime)s - %(message)s') 26 | 27 | ch = logging.StreamHandler() 28 | ch.setLevel(logging.DEBUG) 29 | ch.setFormatter(formatter) 30 | logger.addHandler(ch) 31 | 32 | fibo_dict = {} 33 | shared_queue = Queue() 34 | input_list = [3, 10, 5, 7] 35 | ``` 36 | 37 | !!! info "下载示例代码" 38 | 39 | 您可以从{target="_blank"}上的帐户下载您购买的所有 Packt 书籍的示例代码文件。 如果您在其他地方购买了本书,您可以访问 {target="_blank"} 并注册以便将文件直接通过电子邮件发送给您。 40 | 41 | 在下面的代码行中,我们将从名为 `Condition` 的线程模块中定义一个对象。 该对象旨在根据特定条件同步对资源的访问。 42 | 43 | ```python 44 | queue_condition = threading.Condition() 45 | ``` 46 | 47 | 使用 Condition 对象的用于控制队列的创建和在其中进行条件处理。 48 | 49 | 下一段代码是定义将由多个线程执行的函数。我们将称它为 `fibonacci_task`。`fibonacci_task`函数接收`condition`对象作为参数,它将控制`fibonacci_task`对`share_queue`的访问。在这个函数中,我们使用了`with`语句(关于`with`语句的更多信息,请参考)来简化内容的管理。如果没有`with`语句,我们将不得不明确地获取锁并释放它。有了`with`语句,我们可以在开始时获取锁,并在内部块的退出时释放它。`fibonacci_task`函数的下一步是进行逻辑评估,告诉当前线程:"虽然`shared_queue`是空的,但要等待。" 这就是`condition`对象的`wait()`方法的主要用途。线程将等待,直到它得到通知说`shared_queue`可以自由处理。一旦我们的条件得到满足,当前线程将在`shared_queue`中获得一个元素,它马上计算斐波那契数列的值,并在`fibo_dict`字典中生成一个条目。最后,我们调用`task_done()`方法,目的是告知某个队列的任务已经被提取并执行。代码如下: 50 | 51 | ```python 52 | 53 | def fibonacci_task(condition): 54 | with condition: 55 | while shared_queue.empty(): 56 | logger.info("[%s] - waiting for elements in queue.." % threading.current_thread().name) 57 | condition.wait() 58 | else: 59 | value = shared_queue.get() 60 | a, b = 0, 1 61 | for item in range(value): 62 | a, b = b, a + b 63 | fibo_dict[value] = a 64 | shared_queue.task_done() 65 | logger.debug("[%s] fibonacci of key [%d] with result [%d]" % (threading.current_thread().name, value, fibo_dict[value])) 66 | ``` 67 | 68 | 我们定义的第二个函数是`queue_task`函数,它将由负责为`shared_queue`填充要处理的元素的线程执行。我们可以注意到获取`condition`作为访问`shared_queue`的一个参数。对于`input_list`中的每一个项目,线程都会将它们插入`shared_queue`中。 69 | 70 | 将所有元素插入 `shared_queue` 后,该函数通知负责计算斐波那契数列的线程队列已准备好使用。 这是通过使用 `condition.notifyAll()` 完成的,如下所示: 71 | 72 | ```python 73 | def queue_task(condition): 74 | logging.debug('Starting queue_task...') 75 | with condition: 76 | for item in input_list: 77 | shared_queue.put(item) 78 | logging.debug("Notifying fibonacci_task threadsthat the queue is ready to consume..") 79 | condition.notifyAll() # python3.10 中使用 notify_all() 80 | ``` 81 | 82 | 在下一段代码中,我们创建了一组包含四个线程的集合,它们将等待来自 `shared_queue` 的准备条件。 然后我们强调了线程类的构造函数,它允许我们定义函数。该线程将使用目标参数执行,该函数在`args`中接收的参数如下: 83 | 84 | ```python 85 | threads = [ 86 | threading.Thread(daemon=True, target=fibonacci_task,args=(queue_condition,)) 87 | for i in range(4) 88 | ] 89 | ``` 90 | 91 | 接着我们使用`thread`对象的`start`方法开始线程: 92 | 93 | ```python 94 | [thread.start() for thread in threads] 95 | ``` 96 | 97 | 然后我们创建一个线程处理`shared_queue`,然后执行该线程。代码如下: 98 | 99 | ```python 100 | prod = threading.Thread(name='queue_task_thread', daemon=True, target=queue_task, args=(queue_condition,)) 101 | prod.start() 102 | ``` 103 | 104 | 最后,我们对所有计算斐波那契数列的线程调用了`join()`方法。这个调用的目的是让让主线程等待子线程的调用,直到所有子线程执行完毕之后才结束主线程。请参考下面的代码: 105 | 106 | ```python 107 | [thread.join() for thread in threads] 108 | ``` 109 | 110 | 程序的执行结果如下: 111 | 112 | ```shell 113 | $ python temp.py 114 | 2023-03-01 12:19:26,873 - [Thread-1 (fibonacci_task)] - waiting for elements in queue.. 115 | 2023-03-01 12:19:26,873 - [Thread-2 (fibonacci_task)] - waiting for elements in queue.. 116 | 2023-03-01 12:19:26,874 - [Thread-3 (fibonacci_task)] - waiting for elements in queue.. 117 | 2023-03-01 12:19:26,874 - [Thread-4 (fibonacci_task)] - waiting for elements in queue.. 118 | 2023-03-01 12:19:26,874 - Starting queue_task... 119 | 2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. 120 | 2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. 121 | 2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. 122 | 2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. 123 | 2023-03-01 12:19:26,875 - [Thread-1 (fibonacci_task)] fibonacci of key [3] with result [2] 124 | 2023-03-01 12:19:26,875 - [Thread-2 (fibonacci_task)] fibonacci of key [10] with result [55] 125 | 2023-03-01 12:19:26,875 - [Thread-4 (fibonacci_task)] fibonacci of key [5] with result [5] 126 | 2023-03-01 12:19:26,875 - [Thread-3 (fibonacci_task)] fibonacci of key [7] with result [13] 127 | ``` 128 | 129 | 请注意,首先创建并初始化 `fibonacci_task` 线程,然后它们进入等待状态。 同时,创建 `queue_task` 并填充 `shared_queue`。 最后,`queue_task` 通知 `fibonacci_task` 线程它们可以执行它们的任务。 130 | 131 | 请注意,`fibonacci_task` 线程的执行不遵循顺序逻辑,每次执行的顺序可能不同。 这就是使用线程的一个特点:**非确定性**(non-determinism)。 132 | 133 | ## 完整例子 134 | 135 | 译者注: 136 | 137 | ```python 138 | 139 | #coding: utf-8 140 | 141 | import logging, threading 142 | 143 | from queue import Queue 144 | 145 | logger = logging.getLogger() 146 | logger.setLevel(logging.DEBUG) 147 | formatter = logging.Formatter('%(asctime)s - %(message)s') 148 | 149 | ch = logging.StreamHandler() 150 | ch.setLevel(logging.DEBUG) 151 | ch.setFormatter(formatter) 152 | logger.addHandler(ch) 153 | 154 | fibo_dict = {} 155 | shared_queue = Queue() 156 | input_list = [3, 10, 5, 7] 157 | 158 | queue_condition = threading.Condition() 159 | 160 | def fibonacci_task(condition): 161 | with condition: 162 | while shared_queue.empty(): 163 | logger.info("[%s] - waiting for elements in queue.." % threading.current_thread().name) 164 | condition.wait() 165 | else: 166 | value = shared_queue.get() 167 | a, b = 0, 1 168 | for item in range(value): 169 | a, b = b, a + b 170 | fibo_dict[value] = a 171 | shared_queue.task_done() 172 | logger.debug("[%s] fibonacci of key [%d] with result [%d]" % (threading.current_thread().name, value, fibo_dict[value])) 173 | 174 | def queue_task(condition): 175 | logging.debug('Starting queue_task...') 176 | with condition: 177 | for item in input_list: 178 | shared_queue.put(item) 179 | logging.debug("Notifying fibonacci_task threadsthat the queue is ready to consume..") 180 | condition.notify_all() 181 | 182 | 183 | 184 | 185 | if __name__ == "__main__": 186 | threads = [ 187 | threading.Thread(daemon=True, target=fibonacci_task,args=(queue_condition,)) 188 | for i in range(4) 189 | ] 190 | 191 | [thread.start() for thread in threads] 192 | 193 | prod = threading.Thread(name='queue_task_thread', daemon=True, target=queue_task, args=(queue_condition,)) 194 | prod.start() 195 | 196 | [thread.join() for thread in threads] 197 | ``` 198 | -------------------------------------------------------------------------------- /docs/chapter4/总结.md: -------------------------------------------------------------------------------- 1 | # 总结 2 | 3 | 在本章,我们聚焦于使用线程的理论方法. 通过使用threading模块和concurrent.futures模块,我们实现了上一章所展示的案例,并通过这种方式展示这些模块的机制和灵活性 4 | 5 | 下一章我们会使用`multiprocessing`和`ProcessPoolExecutor`这两个模块来再依次解决这两个问题. 6 | -------------------------------------------------------------------------------- /docs/chapter5/crawling_the_web_using_processPoolExecutor.md: -------------------------------------------------------------------------------- 1 | # 使用ProcessPoolExecutor模块设计网络爬虫 2 | 3 | 正如`concurrent.futures`模块提供了`ThreadPoolExecutor`,方便创建和操作多个线程,进程属于`ProcessPoolExecutor`类。 `ProcessPoolExecutor` 类也包含在 `concurrent.futures` 包中,用于实现我们的并行 `Web` 爬虫。 为了实施这个案例研究,我们创建了一个名为 `process_pool_executor_web_crawler.py` 的 `Python` 模块。 4 | 5 | 代码以前面示例中已知的导入启动,例如`requests`、`Manager` 模块等。 关于任务的定义,以及线程的使用,与上一章的示例相比几乎没有变化,只是现在我们发送数据以通过函数参数形式进行; 参考以下说明: 6 | 7 | `group_urls_task`函数定义如下: 8 | 9 | ```python 10 | def group_urls_task(urls, result_dict, html_link_regex) 11 | ``` 12 | 13 | `crawl_task`函数定义如下: 14 | 15 | ```python 16 | def crawl_task(url, html_link_regex) 17 | ``` 18 | 19 | 现在让我们看一下代码块,其中有一些细微但相关的变化。进入主代码块,我们声明了一个`Manager`类型的对象,现在它将允许**共享队列**,而不仅仅是包含处理结果的字典。为了定义这个包含需要抓取的URL的名为`urls`的队列,我们将使用`Manager.Queue`对象。对于结果字典,我们将使用`Manager.dict`对象,旨在使用一个由代理管理的字典。下面示例代码说明了这些定义。 20 | 21 | ```python 22 | if __name__ == '__main__': 23 | manager = Manager() 24 | urls = manager.Queue() 25 | urls.put("http://br.bing.com/") 26 | urls.put("https://github.com") 27 | result_dict = manager.dict() 28 | ``` 29 | 30 | 然后,我们定义了爬虫阶段要使用的正则表达式,并获得了运行该程序的机器的处理器数量,如以下代码所示: 31 | 32 | ```python 33 | html_link_regex = re.compile('') 34 | number_of_cpus = cpu_count() 35 | ``` 36 | 37 | 在最后一个代码块中,我们可以注意到`concurrent.futures`模块中的API的一致性。下面这块内容正是我们在上一章中提到的使用`ThreadPoolExecutor`的例子中使用的。然而,通过改变内部行为和解决CPU绑定进程的GIL问题,将该类改为`ProcessPoolExecutor`就足够了,而不会破坏代码。检查以下几个代码块;这两块都创建了`ProcessPoolExecutor`,其工作者的限制等于机器中处理器的数量。第一个executor用于将字典中的`URL`以标准的`None`值分组。第二个executor进行抓取阶段。 38 | 39 | 下面是第一个executor的一代码块。 40 | 41 | ```python 42 | with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as group_link_processes: 43 | for i in range(urls.qsize()): 44 | group_link_processes.submit(group_urls_task, urls, result_dict, html_link_regex) 45 | ``` 46 | 47 | 第二个executor的代码块如下: 48 | 49 | ```python 50 | with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as crawler_link_processes: 51 | future_tasks = {crawler_link_processes.submit(crawl_task, url, html_link_regex): url for url in result_dict.keys()} 52 | for future in concurrent.futures.as_completed(future_tasks): 53 | result_dict[future.result()[0]] = future.result()[1] 54 | ``` 55 | 56 | !!! info "" 57 | 58 | 使用 `concurrent.futures` 从多线程模式切换到多进程稍微简单一些。 59 | 60 | 程序运行结果如下图: 61 | 62 | ```shell 63 | $ python process_pool_executor_web_crawler.py 64 | [SpawnProcess-4] putting url [http://www.google.com] in dictionary... 65 | [SpawnProcess-4] putting url [http://br.bing.com/] in dictionary... 66 | [SpawnProcess-4] putting url [https://duckduckgo.com/] in dictionary... 67 | [SpawnProcess-4] putting url [http://br.search.yahoo.com/] in dictionary... 68 | [SpawnProcess-2] putting url [https://github.com/] in dictionary... 69 | [SpawnProcess-11] crawling url [https://duckduckgo.com/] ... 70 | [SpawnProcess-10] crawling url [http://www.google.com] ... 71 | [SpawnProcess-8] crawling url [https://github.com/] ... 72 | [SpawnProcess-7] crawling url [http://br.bing.com/] ... 73 | [SpawnProcess-9] crawling url [http://br.search.yahoo.com/] ... 74 | [http://www.google.com] with links: [http://www.google.com.hk/imghp?hl=zh-TW&tab=wi... 75 | [http://br.bing.com/] with links: [javascript:void(0);... 76 | [https://duckduckgo.com/] with links: [/about... 77 | [http://br.search.yahoo.com/] with links: [https://br.yahoo.com/... 78 | [https://github.com/] with links: [#start-of-content... 79 | ``` 80 | 81 | ## 完整示例 82 | 83 | 译者注: 84 | 85 | ```python 86 | import sys 87 | import re 88 | import queue 89 | from concurrent.futures import ProcessPoolExecutor, as_completed 90 | from multiprocessing import Manager, cpu_count, current_process 91 | 92 | import requests 93 | 94 | 95 | result_dict = {} 96 | 97 | def group_urls_task(urls, result_dict, html_link_regex): 98 | try: 99 | url = urls.get(True, 0.05) # true表示阻塞其他线程访问这个队列,0.05表示阻塞的超时时间 100 | result_dict[url] = None 101 | print("[%s] putting url [%s] in dictionary..." % (current_process().name, url)) 102 | except queue.Empty: 103 | print('Nothing to be done, queue is empty') 104 | 105 | def crawl_task(url, html_link_regex): 106 | links = [] 107 | try: 108 | request_data = requests.get(url) 109 | print("[%s] crawling url [%s] ..." % (current_process().name, url)) 110 | links = html_link_regex.findall(request_data.text) 111 | except: 112 | print(f"error: {sys.exc_info()[0]}") 113 | raise 114 | finally: 115 | return (url, links) 116 | 117 | if __name__ == "__main__": 118 | 119 | manager = Manager() 120 | urls = manager.Queue() 121 | urls.put('http://www.google.com') 122 | urls.put('http://br.bing.com/') 123 | urls.put('https://duckduckgo.com/') 124 | urls.put('https://github.com/') 125 | urls.put('http://br.search.yahoo.com/') 126 | result_dict = manager.dict() 127 | 128 | html_link_regex = re.compile('') 129 | number_of_cpus = cpu_count() 130 | 131 | with ProcessPoolExecutor(max_workers=number_of_cpus) as group_link_processes: 132 | for i in range(urls.qsize()): 133 | group_link_processes.submit(group_urls_task, urls, result_dict, html_link_regex) 134 | 135 | with ProcessPoolExecutor(max_workers=number_of_cpus) as crawler_link_processes: 136 | future_tasks = {crawler_link_processes.submit(crawl_task, url, html_link_regex): url for url in result_dict.keys()} 137 | for future in as_completed(future_tasks): 138 | result_dict[future.result()[0]] = future.result()[1] 139 | 140 | for url, links in result_dict.items(): 141 | print(f"[{url}] with links: [{links[0]}...") 142 | ``` 143 | -------------------------------------------------------------------------------- /docs/chapter5/implementing_multiprocessing_communication.md: -------------------------------------------------------------------------------- 1 | # 实现多进程间通信 2 | 3 | [multiprocessing](http://docs.python.org/3/library/multiprocessing.html)模块允许进程间以两种方式进行通信,这两种方式都基于消息传递机制。 如前所述,由于缺乏同步机制因此不得不采取消息传递机制,因此是数据副本在进程之间交换。 4 | 5 | ## 使用multiprocessing.Pipe模块 6 | 7 | **管道**(pipe)由在两个端点(通信中的两个进程)之间建立通信的机制组成。 这是一种创建通道以便在进程之间交换消息的方法。 8 | 9 | !!! warning "" 10 | 11 | Python 官方文档建议每两个端点使用一个管道,因为不能保证另一个端点同时读取安全。 12 | 13 | 为了举例说明`multiprocessing.Pipe`对象的使用,我们来实现一个创建两个进程A和B的Python程序,进程A发送一个1到10的随机整数值给进程B,进程B会显示它在屏幕上。 现在,让我们一步步介绍这个程序。 14 | 15 | 我们首先导入一些我们程序中需要的包,如下: 16 | 17 | ```python 18 | import os, random 19 | from multiprocessing import Process, Pipe 20 | ``` 21 | 22 | 通过`os`模块的[os.getpid()](http://docs.python.org/3.3/library/os.html)方法使得我们可以获得进程的PID。[os.getpid()](http://docs.python.org/3.3/library/os.html)将以一种透明的方式返回程序的PID,在我们的示例中,`os.getpid()` 调用将以透明形式返回负责运行任务 `producer_task` 和 `consumer_task` 的各自进程的 `PID`。 23 | 24 | 在程序的下一部分,我们将定义`producer_task`函数,除此之外,该函数将使用`random.randint(1, 10)`调用生成一个随机数。这个函数的关键点被称为`conn.send(value)`,它使用`Pipe`在主程序的流量中生成的连接对象,该连接对象(conn)已被作为参数发送到该函数。观察`producer_task`函数的全部内容,如下所示: 25 | 26 | ```python 27 | def producer_task(conn): 28 | value = random.randint(1, 10) 29 | conn.send(value) 30 | print('Value [%d] send by PID [%d]' % (value, os.getpid())) 31 | conn.close() 32 | ``` 33 | 34 | !!! warning "" 35 | 36 | 永远不要忘记总是调用`Pipe`连接的`close()`方法,该连接通过发送方法发送数据。当不再使用时,这对于最终释放与通信通道相关的资源是很重要的。 37 | 38 | 消费者进程要执行的任务非常简单,它的唯一目标是在屏幕上打印接收到的值,接收消费者进程的`PID`。 为了从通信通道获取发送的值,我们使用了 [conn.recv()](http://docs.python.org/dev/library/multiprocessing.html#multiprocessing.Connection.recv){target="_blank"} 调用。 `consumer_task` 函数的实现最终如下所示: 39 | 40 | ```python 41 | def consumer_task(conn): 42 | print('Value [%d] received by PID [%d]' % (conn.recv(), os.getpid())) 43 | ``` 44 | 45 | 我们这个小例子的最后部分实现了对`Pipe()`对象的调用,创建了两个连接对象,将被消费者和生产者进程使用。在这个调用之后,生产者和消费者进程被创建,分别发送`consumer_task`和`producer_task`函数作为目标函数,我们可以在下面的完整代码中看到。 46 | 47 | ```python 48 | if __name__ == '__main__': 49 | producer_conn, consumer_conn = Pipe() 50 | consumer = Process(target=consumer_task,args=(consumer_conn,)) 51 | producer = Process(target=producer_task,args=(producer_conn,)) 52 | 53 | consumer.start() 54 | producer.start() 55 | 56 | consumer.join() 57 | producer.join() 58 | ``` 59 | 60 | 定义完进程后,就该调用`start()`方法来启动执行,并调用`join()`方法,这样主进程就会等待生产者和消费者进程的执行。 61 | 62 | 在下面的截图中,我们可以看到`multiprocessing_pipe.py`程序的输出。 63 | 64 | ```shell 65 | $ python multiprocessing_pipe.py 66 | Value [6] send by PID [95980] 67 | Value [6] received by PID [95979] 68 | ``` 69 | 70 | ## 理解multiprocessing.Queue模块 71 | 72 | 在上一节中,我们分析了管道的概念,通过创建一个通信通道在进程之间建立通信。现在,我们将分析如何有效地建立这种通信,利用`Queue`对象,它在`multiprocessing`模块中实现。`multiprocessing.Queue`的可用接口与`queue.Queue`相当类似。然而,内部实现使用了不同的机制,比如使用了`thread`的内部线程`feeder` ,它将数据从队列的数据缓冲区传输到与目标进程相关的管道。管道和队列机制都利用了**消息传递机制**,这使用户无需使用**同步机制**,从而节省了使用**同步机制**带来的开销。 73 | 74 | !!! warning "" 75 | 76 | 虽然使用`multiprocessing.Queue`的用户不需要使用同步机制,例如`Locks`,但在内部,这些机制被用来在缓冲区和管道之间传输数据,以完成通信。 77 | 78 | ## 完整示例 79 | 80 | 译者注: 81 | 82 | ```python 83 | import os, random 84 | from multiprocessing import Process, Pipe 85 | 86 | def producer_task(conn): 87 | value = random.randint(1, 10) 88 | conn.send(value) 89 | print('Value [%d] send by PID [%d]' % (value, os.getpid())) 90 | conn.close() 91 | 92 | 93 | def consumer_task(conn): 94 | print('Value [%d] received by PID [%d]' % (conn.recv(), os.getpid())) 95 | 96 | if __name__ == '__main__': 97 | producer_conn, consumer_conn = Pipe() 98 | consumer = Process(target=consumer_task,args=(consumer_conn,)) 99 | producer = Process(target=producer_task,args=(producer_conn,)) 100 | 101 | consumer.start() 102 | producer.start() 103 | 104 | consumer.join() 105 | producer.join() 106 | ``` 107 | -------------------------------------------------------------------------------- /docs/chapter5/index.md: -------------------------------------------------------------------------------- 1 | # 使用多进程和进程池 2 | 3 | 上章中,我们学习了如何使用`threading`模块解决两个问题。通过本章的学习,我们将学习如何使用`multiprocessing`模块解决上章的两个问题,我们将使用和上章类似的接口实现。然而,我们会使用多进程机制。 4 | 5 | 本章将覆盖如下一个知识点: 6 | 7 | * 理解进程的概念 8 | * 理解多进程通信 9 | * 使用多进程解决斐波那契数列多输入问题 10 | * 使用`ProcessPoolExecutor`模块设计网络爬虫 11 | -------------------------------------------------------------------------------- /docs/chapter5/understanding_the_concept_of_a_process.md: -------------------------------------------------------------------------------- 1 | # 理解进程的概念 2 | 3 | 我们必须将操作系统中的**进程**(processes)理解为执行中的程序及其资源的**容器**。 与执行中的程序有关的所有内容都可以通过它所代表的进程进行管理——它的**数据区域**(data area)、它的**子进程**(child processes)、它的**资产**(estates)以及它**与其他进程的通信**(communication with other processes)。 4 | 5 | ## 理解进程模型 6 | 7 | **进程**(processes)具有相关的信息和资源,可以对其进行操作和控制。 操作系统有一个称为**进程控制块** (PCB) 的结构,它存储有关进程的信息。 例如,PCB 可能存储以下信息: 8 | 9 | - **Process ID**(PID): 这是唯一的整数值(无符号),用于标识操作系统中的进程。 10 | - **程序计数器**(Program counter): 这包含要执行的下一条程序指令的地址。 11 | - **I/O信息**(I/O information): 这是与进程相关联的打开文件和设备的列表 12 | - **内存分配**(Memory allocation): 这存储有关进程使用和保留的内存空间以及分页表的信息。 13 | - **CPU调度**(CPU scheduling): 这存储有关进程优先级的信息并指向**交错队列**。(staggering queues)。 14 | - **优先级**(Priority:): 这定义了进程在获取 CPU 时的优先级 15 | - **当前状态**(Current state): 这表明进程是就绪(ready)、等待(waiting)还是正在运行(running) 16 | - **CPU 注册表**(CPU registry): 这存储堆栈指针和其他信息。 17 | 18 | ### 定义进程状态 19 | 20 | **进程**(processes)拥有跨越其生命周期的三种状态; 它们如下: 21 | 22 | - **运行状态**(Running): 该进程正在使用 CPU。 23 | - **就绪状态**(Ready): 在进程队列中等待的进程现在可以使用 CPU。 24 | - **等待状态**(Waiting): 进程正在等待与它执行的任务相关的一些 I/O 操作。 25 | -------------------------------------------------------------------------------- /docs/chapter5/using_multiprocessing_to_compute_fibonacci_series_terms_with_multiple_inputs.md: -------------------------------------------------------------------------------- 1 | # 使用多进程解决斐波那契序列多输入问题 2 | 3 | 下面我们将使用多进程解决多输入情况下的斐波那契数列问题,而不是之前我们使用的多线程的方法。 4 | 5 | `multiprocessing_fibonacci.py` 代码使用了 `multiprocessing` 模块,为了运行,它导入了一些基本模块,我们可以在以下代码中观察到: 6 | 7 | ```python 8 | import sys, time, random, re, requests 9 | import concurrent.futures 10 | from multiprocessing import cpu_count, current_process, Manager 11 | ``` 12 | 13 | 前面的章节中已经提到了一些导入; 尽管如此,以下某些导入确实值得特别注意: 14 | 15 | - **cpu_count**: 这是一个允许获取机器中 CPU 数量的函数。 16 | - **current_process**: 这是一个允许获取有关当前进程的信息的函数,例如,它的名称。 17 | - **Manager**: 这是一种允许通过代理在不同进程之间共享 `Python` 对象的类型。(更多信息参考:{target="_blank"}) 18 | 19 | 按照代码,我们可以注意到第一个函数的行为有所不同; 它将在 0-14 次迭代期间以 1 到 20 的间隔生成随机值。 这些值将作为键插入到 `fibo_dict` 中,这是一个由 `Manager` 对象生成的字典。 20 | 21 | !!! warning "" 22 | 23 | 使用消息传递的方法更为常见。 然而,在某些情况下,我们需要在不同进程之间共享一段数据,正如我们在 `fibo_dict` 字典中看到的那样。 24 | 25 | 接下来让我们一起来看`producer_task`方法,如下: 26 | 27 | ```python 28 | def producer_task(q, fibo_dict): 29 | for i in range(15): 30 | value = random.randint(1, 20) 31 | fibo_dict[value] = None 32 | 33 | print("Producer [%s] putting value [%d] into queue.." % (current_process().name, value)) 34 | q.put(value) 35 | ``` 36 | 37 | 下一步是定义函数,该函数将为 `fibo_dict` 中的每个键计算斐波那契数列值。值得注意的是,与上一章中介绍的函数相关的唯一区别是使用 `fibo_dict` 作为参数以允许在不同进程使用它。 38 | 39 | 下面是`consumer_task`方法,如下: 40 | 41 | ```python 42 | def consumer_task(q, fibo_dict): 43 | while not q.empty(): 44 | value = q.get(True, 0.05) 45 | a, b = 0, 1 46 | for item in range(value): 47 | a, b = b, a+b 48 | fibo_dict[value] = a 49 | print("consumer [%s] getting value [%d] from queue..." % (current_process().name, value)) 50 | ``` 51 | 52 | 为了进一步了解代码,我们看看程序的主代码块。在这个主代码块中,定义了以下一些变量: 53 | 54 | - **data_queue**: 该参数由`multiprocessing.Queueu`来创建,是进程安全的 55 | - **number_of_cpus**: 该参数由`multiprocessing.cpu_count`方法获得,获得机器cpu的个数 56 | - **fibo_dict**: 这个字典类型变量从`Manager`实例获得,保存多进程计算结果 57 | 58 | 在代码中,我们创建了一个名为 `producer` 的进程,以使用 `producer_task` 函数使用随机值填充 `data_queue`,如下所示: 59 | 60 | ```python 61 | producer = Process(target=producer_task, args=(data_queue, fibo_dict)) 62 | producer.start() 63 | producer.join() 64 | ``` 65 | 66 | 我们可以注意到`Process`实例的初始化过程和我们之前的`Thread`实例初始化过程类似。初始化函数接收`target`参数作为进程中要执行的函数,和`args`参数作为`target`传入的函数的参数。接下来我们通过`start()`函数开始进程,然后使用`join()`方法,等待`producer`进程执行完毕。 67 | 68 | 在下一个块中,我们定义了一个名为 `consumer_list` 的列表,它将存储已初始化进程的消费者列表。 创建此列表的原因是仅在所有 `worker` 的进程开始后调用 `join()`。 如果为循环中的每个项目调用 `join()` 函数,那么只有第一个 `worker` 会执行该作业,因为下一次迭代将被阻塞,等待当前 `worker` 结束,最后下一个`worker`将没有其他要处理的内容; 以下代码代表了这种情况: 69 | 70 | ```python 71 | consumer_list = [] 72 | number_of_cpus = cpu_count() 73 | 74 | for i in range(number_of_cpus): 75 | consumer = Process(target=consumer_task, args=(data_queue, fibo_dict)) 76 | consumer.start() 77 | consumer_list.append(consumer) 78 | 79 | [consumer.join() for consumer in consumer_list] 80 | ``` 81 | 82 | 最终,我们在 `fibo_dict` 中展示了迭代的结果,如下截图所示: 83 | 84 | ```shell 85 | $ python multiprocessing_fibonacci.py 86 | Producer [Process-2] putting value [8] into queue.. 87 | Producer [Process-2] putting value [10] into queue.. 88 | Producer [Process-2] putting value [19] into queue.. 89 | Producer [Process-2] putting value [6] into queue.. 90 | Producer [Process-2] putting value [17] into queue.. 91 | Producer [Process-2] putting value [18] into queue.. 92 | Producer [Process-2] putting value [19] into queue.. 93 | Producer [Process-2] putting value [17] into queue.. 94 | Producer [Process-2] putting value [18] into queue.. 95 | Producer [Process-2] putting value [4] into queue.. 96 | Producer [Process-2] putting value [6] into queue.. 97 | Producer [Process-2] putting value [7] into queue.. 98 | Producer [Process-2] putting value [9] into queue.. 99 | Producer [Process-2] putting value [4] into queue.. 100 | Producer [Process-2] putting value [19] into queue.. 101 | consumer [Process-4] getting value [8] from queue... 102 | consumer [Process-6] getting value [6] from queue... 103 | consumer [Process-9] getting value [19] from queue... 104 | consumer [Process-13] getting value [6] from queue... 105 | consumer [Process-11] getting value [4] from queue... 106 | consumer [Process-4] getting value [9] from queue... 107 | consumer [Process-8] getting value [18] from queue... 108 | consumer [Process-10] getting value [18] from queue... 109 | consumer [Process-3] getting value [19] from queue... 110 | consumer [Process-5] getting value [10] from queue... 111 | consumer [Process-6] getting value [4] from queue... 112 | consumer [Process-7] getting value [17] from queue... 113 | consumer [Process-14] getting value [17] from queue... 114 | consumer [Process-12] getting value [7] from queue... 115 | consumer [Process-9] getting value [19] from queue... 116 | {8: 21, 10: 55, 19: 4181, 6: 8, 17: 1597, 18: 2584, 4: 3, 7: 13, 9: 34} 117 | ``` 118 | 119 | ## 完整示例 120 | 121 | 译者注: 122 | 123 | ```python 124 | #coding: utf-8 125 | import sys, time, random 126 | import concurrent.futures 127 | from multiprocessing import cpu_count, current_process, Manager, Process, Queue 128 | 129 | def producer_task(q, fibo_dict): 130 | for i in range(15): 131 | value = random.randint(1, 20) 132 | fibo_dict[value] = None 133 | print("Producer [%s] putting value [%d] into queue.." % (current_process().name, value)) 134 | q.put(value) 135 | 136 | def consumer_task(q, fibo_dict): 137 | while not q.empty(): 138 | value = q.get(True, 0.05) 139 | a, b = 0, 1 140 | for item in range(value): 141 | a, b = b, a+b 142 | fibo_dict[value] = a 143 | time.sleep(random.randint(1, 3)) # 由于现代计算机cpu处理太快,这里随机sleep几秒 144 | print("consumer [%s] getting value [%d] from queue..." % (current_process().name, value)) 145 | 146 | if __name__ == "__main__": 147 | fibo_dict = Manager().dict() # 如果替换为 {}, 则没有共享对象的功能,打印出来将是空的。 148 | data_queue = Queue() 149 | 150 | producer = Process(target=producer_task, args=(data_queue, fibo_dict)) 151 | producer.start() 152 | producer.join() 153 | 154 | consumer_list = [] 155 | number_of_cpus = cpu_count() 156 | for i in range(number_of_cpus): 157 | consumer = Process(target=consumer_task, args=(data_queue, fibo_dict)) 158 | consumer.start() 159 | consumer_list.append(consumer) 160 | 161 | [consumer.join() for consumer in consumer_list] 162 | 163 | print(fibo_dict) 164 | 165 | ``` 166 | -------------------------------------------------------------------------------- /docs/chapter5/总结.md: -------------------------------------------------------------------------------- 1 | # 小结 2 | 3 | 本章中我们介绍了多进程的概念,并使用多进程解决了两个小问题,分别是并行计算斐波那契数列值和设计网络爬虫。 4 | 5 | 下一章节我们将使用`parallel Python`模块执行多进程任务,`parallel`模块并不是**python**的内部模块。我们还将学习进程间通信相关的知识,使用`pipes`在进程间通信。 6 | -------------------------------------------------------------------------------- /docs/chapter6/discovering_pp.md: -------------------------------------------------------------------------------- 1 | # 了解PP模块 2 | 3 | 上一节中,我们介绍了直接使用系统调用创建进程间通讯的方法,这是一种很低层的机制. 并且它只在Linux或Unix环境下才有效. 接下来我们会使用一个名为PP的python模块来创建IPC通讯,这种通讯不仅仅可以在本地进程间进行,还能通过计算机网络在物理上彼此分散的进程间进程. 4 | 5 | 关于PP模块的文档并不多,可以在 中找到相关文档和FAQ. API提供了大量的使用该工具的说明. 十分的简洁明了 6 | 7 | 使用PP模块的最大优势在于它提供了一个抽象层. PP模块的重要特征如下所示: 8 | 9 | - 自动探测进程的数量并以此改进负载均衡 10 | - 可以在运行期改变要投入的处理器数量 11 | - 运行时负载均衡 12 | - 全网自动发现资源 13 | 14 | PP模块有两种方式来执行并行代码. 第一种方式基于SMP架构,即在同一台机器上有多个处理器/核心. 第二中方式将网络中的各个机器配置,整合成区块,并将任务分配給这些机器去运行. 无论哪一种方式,进程间消息交换的过程都是抽象的. 这使得我们无需关系其低层的实现方式到底是通过管道还是socket. 我们只需要使用回调函数来通过参数和函数的方式来交换信息就行了. 下面给个例子. 15 | 16 | 在PP的API中有一个名为Server的类,使用该类可以实现在本地和远程的进程间封装和分派任务. Server的构造函数(\__init\__)中有几个参数比较重要: 17 | 18 | - **ncpus**: 该参数用于指定执行任务的工作进程数量. 若没有指定该参数,则会自动根据机器上处理器/核心的数量来创建工作进程的总数,以优化资源的使用。 19 | - **ppservers**: 该参数是一个元组,该元组的元素为**并行Python执行服务器**(Parallel Python Execution Servers - PPES)的名称或IP地址. 一个PPES由连入网络的机器组成. 且该机器通过`ppsever.py`共组运行并等待待执行的任务. 其他参数的说明请参阅 20 | 21 | `Server`类的实例拥有很多方法,其中`submit`方法允许我们分配任务到各个工作进程. `submit`函数具有如下签名: 22 | 23 | ```python 24 | submit(self, func, args=(), depfuncs=(), modules=(), 25 | callback=None, callbackargs=(), group='default', 26 | globals=None) 27 | ``` 28 | 29 | 在`submit`方法中,我们主要关注以下几个参数: 30 | 31 | - **func**: 该函数会被本地进程或远程服务器执行。 32 | - **args**: 该参数提供了了执行`func`函数时的参数。 33 | - **modules**: 该参数说明远程代码(remote code)或进程为了调用func函数,需要导入哪些模块. 例如若被分配的函数用到了time模块, 则在元组参数中,传递方式必须为 `modules=('time',)` 34 | - **callback** :这是执行完`func`函数后的回调函数,`func`的执行结果会作为其函数参数. 常用于对`func`的执行结果作进一步加工 35 | 36 | 还有其他的参数将会在下一节分析代码时进行介绍. 37 | -------------------------------------------------------------------------------- /docs/chapter6/index.md: -------------------------------------------------------------------------------- 1 | # 使用并行 Python 2 | 3 | 在上一章中,我们学习了如何使用 `multiprocessing` 和 `ProcessPoolExecutor` 模块来解决两个案例问题。 本章将介绍命名管道以及如何使用Parallel Python(PP)通过进程执行并行任务。 4 | 5 | 本章会覆盖下面几个知识点: 6 | 7 | * 理解进程间通信 8 | * 了解Parallel Python(PP) 9 | * 在SMP架构上使用PP计算斐波那契序列 10 | * 使用PP创建分布式的网络爬虫 -------------------------------------------------------------------------------- /docs/chapter6/understanding_interprocess_communication.md: -------------------------------------------------------------------------------- 1 | # 理解进程间通讯 2 | 3 | 进程间通信 (Interprocess communication - IPC) 由允许在进程之间交换信息的机制组成。 4 | 5 | 有多种实现 IPC 的方法,通常,它们取决于为运行时环境选择的体系结构。 在某些情况下,例如,进程在同一台机器上运行,我们可以使用各种类型的通信,例如**共享内存**、**消息队列**和**管道**。 例如,当进程物理分布在集群中时,我们可以使用**套接字**(sockets)和**远程过程调用** (Remote Procedure Call - RPC)。 6 | 7 | 在第 5 章,使用 `Multiprocessing` 和 `ProcessPoolExecutor`,我们验证了常规管道的使用等。 我们还研究了具有共同父进程的进程之间的通信。 但是,有时需要在不相关的进程(具有不同父进程的进程)之间进行通信。 我们可能会问自己,不相关进程之间的通信是否可以通过它们的寻址空间来完成。 尽管如此,**一个进程永远不会从另一个进程访问寻址空间**。 因此,我们必须使用称为**命名管道**的机制。 8 | 9 | ## 探索命名管道 10 | 11 | 在 **POSIX** 系统中,例如 **Linux**,我们应该记住一切,绝对一切,都可以总结为文件。对于我们执行的每个任务,在某处都有一个文件,我们还可以找到一个附加到它的文件描述符,它允许我们操作这些文件。 12 | 13 | !!! info "文件描述符" 14 | 15 | 文件描述符是允许用户程序访问文件以进行读/写操作的机制。 通常,文件由唯一的文件描述符引用。 有关文件描述符的更多信息,请访问{target="_blank"}(原文地址为: ,但找不到了,找到了一个替代描述文件描述符的。) 16 | 17 | **命名管道**(Named pipes)不过是允许通过使用与特殊文件相关联的文件描述符进行 IPC 通信的机制,例如,用于写入和读取数据的**先进先出** (FIFO) 方案。 命名管道与常规管道的不同之处在于它们管理信息的方法。 **命名管道**(Named pipes)使用文件系统中的文件描述符和特殊文件,而常规管道是在内存中创建的。 18 | 19 | ## 在python中使用命名管道 20 | 21 | **命名管道**在 Python 中的使用非常简单,我们将通过实现两个执行单向通信的程序来说明这一点。 第一个程序名为`write_to_named_pipe.py`,其功能是在管道中写入一条22字节的消息,通知一个字符串和生成它的进程的PID。 第二个程序称为 `read_from_named_pipe.py`,它将读取消息并显示消息内容,添加其 PID。 22 | 23 | 在执行结束时,`read_from_named_pipe.py` 进程将显示一条形如"I pid [\] received a message => Hello from pid [the PID of writer process"的消息。 24 | 25 | 为了说明在命名管道中写入和读取进程之间的相互依赖性,我们将在两个不同的控制台中执行读取和写入。 但在此之前,让我们分析一下这两个程序的代码。 26 | 27 | ### 往命名管道写入数据 28 | 29 | 在 Python 中,命名管道是通过系统调用实现的。 在下面的代码中,我们将逐行解释 `write_to_named_pipe.py` 程序中代码的功能。 30 | 31 | 我们从 `os` 模块的输入开始,它将提供对系统调用的访问,我们将使用以下代码行: 32 | 33 | ```python 34 | import os 35 | ``` 36 | 37 | 接下来我们会解释__main__代码块,在该代码块中创建了命名管道以及一个用于存储消息的FIFO的特殊文件. __main__代码块中的第一行代码定义了命名管道的标签. 38 | 39 | ```python 40 | named_pipe = "my_pipe" 41 | ``` 42 | 43 | 接下来我们检查该命名管道是否已经存在,若不存在,则调用`mkfifo`系统调用来创建这个命名管道. 44 | 45 | ```python 46 | if not os.path.exists(named_pipe): 47 | os.mkfifo(named_pipe) 48 | ``` 49 | 50 | `mkfifo`调用会创建一个特殊的文件,该文件对通过命名管道读写的消息实现了**FIFO**机制. 51 | 52 | 我们再以一个命名管道和一个行如"Hello from pid [%d]"的消息来作为参数调用函数`write_message`. 该函数会将消息写入到(作为参数传递给它的)命名管道所代表的文件中. `write_message`函数定义如下: 53 | 54 | ```python 55 | def write_message(input_pipe, message): 56 | fd = os.open(input_pipe, os.O_WRONLY) 57 | os.write(fd, (message % str(os.getpid()))) 58 | os.close(fd) 59 | ``` 60 | 61 | 我们可以观察到,在函数的第一行,我们调用一个系统调用:`open`. 该系统调用若成功的话会返回一个文件描述符, 通过该文件描述符我们就能够读写那个`FIFO`文件中的数据. 请注意,我们可以通过`flags`参数控制打开**FIFO**文件的模式. 由于`write_message`函数紧紧需要写数据,因此我们使用如下代码: 62 | 63 | ```python 64 | fd = os.open(input_pipe, os.O_WRONLY) 65 | ``` 66 | 67 | 在成功打开命名管道后,我们使用下面代码写入消息: 68 | 69 | ```python 70 | os.write(fd, (message % os.getpid())) 71 | ``` 72 | 73 | 最后,请一定记着使用`close`关闭通讯渠道,这样才能释放被占用的计算机资源. 74 | 75 | ```python 76 | os.close(fd) 77 | ``` 78 | 79 | ### 从命名管道读取数据 80 | 81 | 我们实现`read_from_pipe.py`来读取命名管道. 当然,改程序也需要借助`os`模块才能操作命名管道. 改程序的主要代码很简单: 首先,我们定义了所使用命名管道的标签,该标签需要与写进程所用的命名管道同名. 82 | 83 | ```python 84 | named_pipe = "my_pipe" 85 | ``` 86 | 87 | 然后,我们调用`read_message`函数,该函数会读取`write_to_named_pipe.py`写入的内容. `read_message`函数的定义如下: 88 | 89 | ```python 90 | # 此处原文应该有错 91 | 92 | def read_message(input_pipe): 93 | fd = os.open(input_pipe, os.O_RDONLY) 94 | message = "I pid [%d] received a message => %s" % (os.getpid(), os.read(fd, 22)) 95 | os.close(fd) 96 | return message 97 | ``` 98 | 99 | `open`调用不需要再介绍。 这里的新事物是我们的读取调用,它以字节为单位执行数量的读取。 在我们的例子中,如果给出了文件描述符,它就是 `22` 个字节。 消息被读取后,由函数返回。 最后,必须执行`close`调用以关闭通信通道。 100 | 101 | !!! info "" 102 | 103 | 要验证已打开文件描述符的有效性。需要由用户处理在使用文件描述符和命名管道时产生的相关异常。 104 | 105 | 最终,下面的截屏显示了`write_to_named_pip`和`read_from_named_pipe`程序的执行结果. 106 | 107 | ```shell 108 | >$ python write_to_named_pipe.py 109 | 110 | ``` 111 | 112 | ```shell 113 | >$ python read_from_pipe.py 114 | I pid [61032] received a message => Hello 61017 115 | ``` 116 | 117 | ## 完整示例 118 | 119 | 译者注: 120 | 121 | ```python 122 | # write_to_named_pip.py 123 | 124 | import os 125 | import sys 126 | 127 | 128 | def write_message(input_pipe, message): 129 | fd = os.open(input_pipe, os.O_WRONLY) 130 | os.write(fd, (message % str(os.getpid())).encode()) # 管道通信为字节,这里需要转码 131 | os.close(fd) 132 | 133 | 134 | if __name__ == "__main__": 135 | named_pipe = "my_pipe" 136 | 137 | if not os.path.exists(named_pipe): 138 | os.mkfifo(named_pipe) 139 | 140 | write_message(named_pipe, "Hello %s") 141 | 142 | ``` 143 | 144 | ```python 145 | # read_from_named_pipe.py 146 | 147 | import os 148 | import sys 149 | 150 | 151 | def read_message(input_pipe): 152 | fd = os.open(input_pipe, os.O_RDONLY) 153 | message = "I pid [%d] received a message => %s" % ( 154 | os.getpid(), 155 | os.read(fd, 22).decode(), # 管道通信为字节,这里需要转码 156 | ) 157 | os.close(fd) 158 | return message 159 | 160 | 161 | if __name__ == "__main__": 162 | named_pipe = "my_pipe" 163 | 164 | if not os.path.exists(named_pipe): 165 | os.mkfifo(named_pipe) 166 | 167 | print(read_message(named_pipe)) 168 | 169 | ``` 170 | -------------------------------------------------------------------------------- /docs/chapter6/using_pp_to_calculate_the_fibonacci_series_term_on_smp_architecture.md: -------------------------------------------------------------------------------- 1 | # 在SMP架构上使用pp模块计算斐波那契序列 2 | 3 | 是时候开始行动了! 让我们解决涉及在 SMP 架构中使用 PP 的多个输入的斐波那契数列的案例研究。 我正在使用配备双核处理器和四个线程的笔记本电脑。 4 | 5 | 我们将为这个实现只导入两个模块,`os` 和 `pp`。`os` 模块将仅用于获取正在执行的进程的 `PID`。 我们将有一个名为 `input_list` 的列表,其中包含要计算的值和一个用于对结果进行分组的字典,我们将其称为 `result_dict`。 然后,我们转到代码块如下: 6 | 7 | ```python 8 | import os, pp 9 | 10 | input_list = [4, 3, 8, 6, 10] 11 | result_dict = {} 12 | ``` 13 | 14 | 然后,我们定义一个名为 `fibo_task` 的函数,它将由**并行进程**执行。 它将是我们通过 `Server` 类的提交方法传递的 `func` 参数。 该函数与前几章相比没有重大变化,除了现在通过使用元组封装参数中接收的值以及包含 `PID` 和计算的 `Fibonacci` 项的消息来完成返回。 看看下面的完整函数: 15 | 16 | ```python 17 | def fibo_task(value): 18 | a, b = 0, 1 19 | for item in range(value): 20 | a, b = b, a + b 21 | message = "the fibonacci calculated by pid %d was %d" % (os.getpid(), a) 22 | 23 | return (value, message) 24 | ``` 25 | 26 | 下一步是定义我们的回调函数,我们将其称为 `aggregate_results`。 一旦 `fibo_task` 函数返回其执行结果,就会调用回调函数。 它的实现非常简单,只显示一条状态消息,随后在 `result_dict` 中生成一个输入,其中包含传递给 `fibo_dict` 函数的值作为键,结果是计算斐波那契项的过程返回的消息。 以下代码是`aggregate_results`函数的完整实现: 27 | 28 | ```python 29 | def aggregate_results(result): 30 | print "Computing results with PID [%d]" % os.getpid() 31 | 32 | result_dict[result[0]] = result[1] 33 | ``` 34 | 35 | 现在,我们有两个函数要定义。 我们必须创建一个 `Server` 类的实例来分派任务。 以下代码行创建一个服务器实例: 36 | 37 | ```python 38 | job_server = pp.Server() 39 | ``` 40 | 41 | 在前面的示例中,我们使用标准值作为参数。 在下一节中,我们将使用一些可用的参数。 42 | 43 | 现在我们有了 `Server` 类的一个实例,让我们迭代 `input_list` 的每个值,通过提交调用分派 `fibo_task` 函数,将需要导入的模块作为参数传递给 `args` 元组中的输入值,以便函数 正确执行并且回调注册 `aggregate_results`。 参考以下代码块: 44 | 45 | ```python 46 | for item in input_list: 47 | job_server.submit(fibo_task, (item,), modules=('os',), 48 | callback=aggregate_results) 49 | ``` 50 | 51 | 最后,我们必须等到所有派发的任务结束。 因此,我们可以使用`Server`类的`wait`方法,如下: 52 | 53 | ```python 54 | job_server.wait() 55 | ``` 56 | 57 | !!! info "" 58 | 59 | 除了使用回调函数之外,还有另一种方法可以获得已执行函数的返回值。 `submit` 方法返回一个对象类型,`pp._Task`,当执行结束时,它包含了执行的结果。 60 | 61 | 最后,我们将通过字典迭代打印条目的结果,如下所示: 62 | 63 | ```python 64 | print "Main process PID [%d]" % os.getpid() 65 | for key, value in result_dict.items(): 66 | print "For input %d, %s" % (key, value) 67 | ``` 68 | 69 | 以下屏幕截图说明了程序的输出: 70 | 71 | ```shell 72 | $ python feibonacci_pp_smp.py 73 | Computing results with PID [21058] 74 | Computing results with PID [21058] 75 | Computing results with PID [21058] 76 | Computing results with PID [21058] 77 | Computing results with PID [21058] 78 | Main process PID [21058] 79 | For input 4, the fibonacci calculated by pid 21059 was 3 80 | For input 3, the fibonacci calculated by pid 21060 was 2 81 | For input 6, the fibonacci calculated by pid 21062 was 8 82 | For input 8, the fibonacci calculated by pid 21061 was 21 83 | For input 10, the fibonacci calculated by pid 21065 was 55 84 | ``` 85 | 86 | ## 完整示例 87 | 88 | 译者注: `feibonacci_pp_smp.py` 89 | 90 | ```python 91 | import os, pp 92 | 93 | input_list = [4, 3, 8, 6, 10] 94 | result_dict = {} 95 | 96 | def fibo_task(value): 97 | a, b = 0, 1 98 | for item in range(value): 99 | a, b = b, a + b 100 | message = "the fibonacci calculated by pid %d was %d" % (os.getpid(), a) 101 | 102 | return (value, message) 103 | 104 | def aggregate_results(result): 105 | print("Computing results with PID [%d]" % os.getpid()) 106 | 107 | result_dict[result[0]] = result[1] 108 | 109 | job_server = pp.Server() 110 | 111 | for item in input_list: 112 | job_server.submit(fibo_task, (item,), modules=('os',), # 这里增加新的模块需要添加进来。 113 | callback=aggregate_results) 114 | 115 | job_server.wait() 116 | 117 | print("Main process PID [%d]" % os.getpid()) 118 | 119 | for key, value in result_dict.items(): 120 | print("For input %d, %s" % (key, value)) 121 | ``` 122 | -------------------------------------------------------------------------------- /docs/chapter6/using_pp_to_make_a_distributed_web_crawler.md: -------------------------------------------------------------------------------- 1 | # 使用PP创建分布式网络爬虫 2 | 3 | 现在我们已经使用 `PP` 并行执行代码来调度本地进程,现在是验证代码是否以分布式方式执行的时候了。 为此,我们将使用以下三台不同的机器: 4 | 5 | - Iceman-Thinkpad-X220: Ubuntu 13.10 6 | - Iceman-Q47OC-500P4C: Ubuntu 12.04 LTS 7 | - Asgard-desktop: Elementary OS 8 | 9 | 我们将在如上列举的三台机器上测试pp组件在分布式环境下的使用。对此,我们实现了分布式网络爬虫。`web_crawler_pp_cluster.py`方法中,将`input_list`列举的URL分发到本地以及远端进程执行,`web_crawler_pp_cluster.py`中的回调函数将组织这些URL以及以及通过它们找到的前三个连接(URL)并进行分组。 10 | 11 | 让我们逐步分析代码以了解如何找到解决此问题的方法。 首先,我们将`import`必要的模块并定义要使用的数据结构。 与上一节一样,我们将创建一个 `input_list` 和一个包含最终处理结果的字典`result_dict`。 参考以下代码: 12 | 13 | ```python 14 | import os, re, requests, pp 15 | 16 | ppurl_list = ['http://www.google.com/','http://gizmodo.uol.com.br/', 17 | 'https://github.com/', 'http://br.search.yahoo.com/', 18 | 'http://www.python.org/','http://www.python.org/psf/'] 19 | 20 | result_dict = {} 21 | ``` 22 | 23 | 现在,我们的 `aggregate_results` 函数将再次成为我们的回调函数,与斐波那契项的示例相比变化不大。 我们只是更改了要插入字典中的消息的格式,以及这个回调的返回将是一个包含执行它的进程的 `PID`、执行它的主机名和找到的前三个链接组成的元组。 参考`aggregate_results`函数如下: 24 | 25 | ```python 26 | def aggregate_results(result): 27 | print("Computing results in main process PID [%d]" % os.getpid()) 28 | message = "PID %d in hostname [%s] the following links were found: %s" % (result[2], result[3], result[1]) 29 | result_dict[result[0]] = message 30 | ``` 31 | 32 | 下一步是定义 `crawl_task` 函数,它将由 `Server` 类的实例调度。 该功能与前面章节中介绍的功能类似,旨在收集作为参数接收的 `URL` 显示的页面中的现有链接。 唯一的区别是返回是一个元组。 参考以下代码: 33 | 34 | ```python 35 | def crawl_task(url): 36 | html_link_regex = re.compile('') 37 | request_data = requests.get(url) 38 | 39 | links = html_link_regex.findall(request_data.text)[:3] 40 | return (url, links, os.getpid(), os.uname()[1]) 41 | ``` 42 | 43 | 在`main`方法和`callback`方法定义之后,我们需要初始化`Server`类实例,以至于能够在分布式环境下执行网络爬虫任务。我们注意到`pp.Server`类有三个参数,第一个参数是执行`Server`类方法的`IP`或`hostname`,我们的例子中,除了本机之外,还需要定义另外两台机器的`IP`和`hostname`,定义如下所示: 44 | 45 | ```python 46 | ppservers = ("192.168.25.21", "192.168.25.9") 47 | ``` 48 | 49 | !!! info "" 50 | 51 | 如果您不想通知并希望自动发现可用于接收任务的机器,请在 `ppservers` 元组中使用 `*` 字符串。 52 | 53 | 定义标识服务器的元组。 我们将创建一个 `Server` 实例,如下所示: 54 | 55 | ```python 56 | job_dispatcher = pp.Server(ncpus=1, ppservers=ppservers, socket_timeout=60000) 57 | ``` 58 | 59 | 值得注意的是,与前面的示例相比有一些变化。 首先,我们将值 `1` 传递给了 `ncpus` 参数。 这将导致 `PP` 创建单个本地进程,并在必要时将其他任务分派给远程机器。 定义的第二个参数是我们在上一步中创建的服务器的元组。 最后,我们为通信中使用的套接字定义了一个超时值,仅用于测试目的。 目标是避免网络超时而关闭通道。 60 | 61 | 创建 `Server` 类的实例后,就可以分派我们的函数来执行了。 让我们遍历每个 `URL` 并将它们传递给 `Server` 实例的`submit`方法,如下所示: 62 | 63 | ```python 64 | for url in ppurl_list: 65 | job_dispatcher.submit(crawl_task, (url,), 66 | modules=('os', 're', 'requests',), 67 | callback=aggregate_results) 68 | ``` 69 | 70 | 与之前计算斐波那契数列的示例相比,重要的变化是发送执行所需的模块。 71 | 72 | !!! info "" 73 | 74 | 你一定在想为什么元组模块中没有传`PP`模块。 很简单; PP 执行环境已经为我们做了这个导入。 毕竟,它需要在远程节点中执行此操作。 75 | 76 | 为了完成我们的并行和分布式网络爬虫,我们必须等到执行结束才能显示它们的输出。 请注意,最后,`Server` 类的 `print_stats` 方法中有一个新元素,它显示了一些有趣的执行统计信息,如下所示: 77 | 78 | ```python 79 | job_dispatcher.wait() 80 | 81 | print "\nMain process PID [%d]\n" % os.getpid() 82 | for key, value in result_dict.items(): 83 | print "** For url %s, %s\n" % (key, value) 84 | job_dispatcher.print_stats() 85 | ``` 86 | 87 | 在执行程序之前,我们需要在远程机器上初始化 `ppserver.py` 实用程序; `ppserver.py –a –d` 是这里使用的命令,其中 `–a` 是自动发现的选项,允许未指定 `IP` 地址的客户端找到服务器。 另一个参数是 `-d`,它通过日志显示有关服务器活动如何执行的信息。 88 | 89 | 让我们按以下顺序可视化输出: 90 | 91 | 1. 首先,以下屏幕截图显示了主节点中创建和分发任务的阶段: 92 | ![1](../imgs/6-02.png) 93 | 2. 然后,初始化 `ppservers.py` 服务器并在以下屏幕截图中看到处理任务(从 iceman-Q47OC500P4C 的 `ppserver.py` 输出和从 `asgard-desktop` 的 `ppserver.py` 输出)。 94 | 3. 在前面的屏幕截图中,值得注意的是统计信息带来了有趣的信息,例如分配到不同目的地的任务数量、每个任务的时间以及每个目的地的总数。 前面屏幕截图中的另一个相关点是回调函数仅在主进程中执行,即调度任务中的那些。 因此,切记不要让回调任务过重,因为它们可能会消耗主节点的过多资源; 这显然取决于每个案例的具体情况。 95 | 4. 下面的截图是的`debug`模式下`ppserver.py`脚本在`iceman-Q470C-500P4C`机器上的的执行日志。 96 | 97 | ![1](../imgs/6-03.png) 98 | 99 | 5. 下面截图是`debug`模式下`ppserver.py`脚本在`asgard-desktop`机器上的执行日志。 100 | ![1](../imgs/6-04.png) 101 | 102 | ## 完整示例 103 | 104 | 译者注: 由于机器性能原因或网络原因,未能还原作者执行场景。 105 | 106 | `fibonacci_pp_cluster.py` 107 | 108 | ```python 109 | import os, re, requests, pp 110 | 111 | 112 | ppurl_list = ['http://www.google.com/','http://gizmodo.uol.com.br/', 113 | 'https://github.com/', 'http://br.search.yahoo.com/', 114 | 'http://www.python.org/','http://www.python.org/psf/'] 115 | 116 | result_dict = {} 117 | 118 | def aggregate_results(result): 119 | print("Computing results in main process PID [%d]" % os.getpid()) 120 | message = "PID %d in hostname [%s] the following links were found: %s" % (result[2], result[3], result[1]) 121 | result_dict[result[0]] = message 122 | 123 | def crawl_task(url): 124 | html_link_regex = re.compile('') 125 | request_data = requests.get(url) 126 | 127 | links = html_link_regex.findall(request_data.text)[:3] 128 | 129 | return (url, links, os.getpid(), os.uname()[1]) 130 | 131 | 132 | ppservers = ("192.168.25.21", "192.168.25.9") 133 | 134 | job_dispatcher = pp.Server(ncpus=1, ppservers=ppservers, socket_timeout=60000) 135 | 136 | for url in ppurl_list: 137 | job_dispatcher.submit(crawl_task, (url,), 138 | modules=('os', 're', 'requests', ), 139 | callback=aggregate_results) 140 | 141 | job_dispatcher.wait() 142 | 143 | print("\nMain process PID [%d]\n" % os.getpid()) 144 | for key, value in result_dict.items(): 145 | print("** For url %s, %s\n" % (key, value)) 146 | job_dispatcher.print_stats() # 显示了一些有趣的执行统计信息 147 | ``` 148 | -------------------------------------------------------------------------------- /docs/chapter6/总结.md: -------------------------------------------------------------------------------- 1 | 2 | # 小结 3 | 4 | 我们研究了使用更低级资源在进程之间建立没有直接关系的进程间通信。 此外,我们还了解了使用 `PP` 模块,这有助于我们抽象本地进程(包括分布式进程)之间的通信。 `PP` 是构建简单、小型、并行和分布式 Python 应用程序的便捷工具。 5 | 6 | 在下一章中,我们将学习如何使用名为 `Celery` 的模块以并行和分布式的方式执行任务。 -------------------------------------------------------------------------------- /docs/chapter7/defining_queues_by_task_types.md: -------------------------------------------------------------------------------- 1 | # 根据任务类型定义队列 2 | 3 | 负责计算 `Fibonacci` 的任务已实现并正在运行。 我们可以看到所有任务都被发送到 `Celery` 的默认队列中。 但是,有多种方法可以将任务路由到不同的队列; 让我们在服务器端重构我们的架构,并从客户端实现所谓的路由任务。 我们将为每种类型的任务指定队列。 4 | 5 | 在服务器端启动`Celery`服务器的那一刻,我们会建立三个不同的队列。 这些现在将被worker看到和消费。 `Fibonacci` 任务的队列是 `fibo_queue`,平方根任务的 `sqrt_queue`,Web 爬虫任务的 `webcrawler_queue`。 但是,将任务分开有什么好处呢? 让我们列举一些: 6 | 7 | - 它将相同类型的任务分组,使它们的监控更容易 8 | - 它定义了专用于消费特定队列的`worker`,从而提高了性能 9 | - 它在具有性能更好的机器上建立任务更重的队列 10 | 11 | !!! info "" 12 | 13 | 前面几点本书不会解释,但我们可以通过初始化 Celery 服务器甚至在网络中分配具有专用队列的代理来实现负载平衡。 我建议您使用 Celery 尝试这种集群风格。 14 | 15 | 要在服务器中设置队列,我们只需要使用以下命令启动 `Celery`: 16 | 17 | ```shell 18 | $celery –A tasks –Q sqrt_queue,fibo_queue,webcrawler_queue worker --loglevel=info 19 | ``` 20 | 21 | 下图是在服务端截图: 22 | 23 | ```log 24 | -------------- [queues] 25 | .> fibo_queue exchange=fibo_queue(direct) key=fibo_queue 26 | .> sqrt_queue exchange=sqrt_queue(direct) key=sqrt_queue 27 | .> webcrawler_queue exchange=webcrawler_queue(direct) key=webcrawler_queue 28 | ``` 29 | 30 | 在转到下一个示例之前,让我们将现有任务的发送路由到它们的队列。 在服务器端,在 `task_dispatcher.py` 模块中,我们将更改 `send_task` 调用,以便下次分派任务时,它们将被定向到不同的队列。 我们现在将按如下方式更改 `sqrt_task` 调用: 31 | 32 | ```python 33 | app.send_task('tasks.sqrt_task', args=(value,), queue='sqrt_queue', routing_key='sqrt_queue') 34 | ``` 35 | 36 | 然后,修改`fibo_task`调用,如下: 37 | 38 | ```python 39 | app.send_task('tasks.fibo_task', args=(x,), queue='fibo_queue', routing_key='fibo_queue') 40 | ``` 41 | 42 | !!! info "" 43 | 44 | 如果有兴趣监控队列、统计任务数量或者其他,请参考`Celery`文档 {target="_blank"}。 45 | 46 | 在任何情况用`Redis`,`redis-cli`都可以作为一个工具。队列、任务、workders都可以被监控,详见 {target="_blank"}. 47 | -------------------------------------------------------------------------------- /docs/chapter7/dispatching_a_simple_task.md: -------------------------------------------------------------------------------- 1 | # 分发简单任务 2 | 3 | 在之前,我们已经建立好环境。下面测试一下环境,发送一个计算平方根的任务。 4 | 5 | 定义任务模块`tasks.py`。在开始,导入必须的模块。 6 | 7 | ```python 8 | from math import sqrt 9 | from celery import Celery 10 | ``` 11 | 12 | 然后,创建`Celery`实例,代表客户端应用: 13 | 14 | ```python 15 | app = Celery('tasks', broker='redis://192.168.25.21:6379/0') 16 | ``` 17 | 18 | 在初始化时我们传入了模块的名称和`broker`的地址。 19 | 20 | 然后,启动`result backend`,如下: 21 | 22 | ```python 23 | app.config.CELERY_RESULT_BACKEND = 'redis://192.168.25.21:6379/0' 24 | 25 | # 较新的版本(v5.2.7)直接填充在celery app的初始化参数中. 26 | app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') 27 | ``` 28 | 29 | 用`@app.tack`装饰器定义任务: 30 | 31 | ```python 32 | @app.task 33 | def sqrt_task(value): 34 | return sqrt(value) 35 | ``` 36 | 37 | 到此,我们完成了`tasks.py`模块的定义,我们需要初始化服务端的`workers`。我们创建了一个单独的目录叫做`8397_07_broker`。拷贝`tasks.py`模块到这个目录,运行如下命令: 38 | 39 | ```shell 40 | $celery –A tasks worker –-loglevel=INFO 41 | ``` 42 | 43 | 上述命令初始化了**Clery Server**,`—A`代表`Celery`应用。下图是初始化的部分截图 44 | 45 | ```shell 46 | $# celery -A tasks worker --loglevel=INFO 47 | /opt/celery_env/lib/python3.9/site-packages/celery/platforms.py:840: SecurityWarning: You're running the worker with superuser privileges: this is 48 | absolutely not recommended! 49 | 50 | Please specify a different user using the --uid option. 51 | 52 | User information: uid=0 euid=0 gid=0 egid=0 53 | 54 | warnings.warn(SecurityWarning(ROOT_DISCOURAGED.format( 55 | 56 | -------------- celery@ch1.nauu.com v5.2.7 (dawn-chorus) 57 | --- ***** ----- 58 | -- ******* ---- Linux-3.10.0-957.el7.x86_64-x86_64-with-glibc2.17 2023-03-06 16:12:10 59 | - *** --- * --- 60 | - ** ---------- [config] 61 | - ** ---------- .> app: tasks:0x7fe5cbea9b80 62 | - ** ---------- .> transport: redis://localhost:6379/0 63 | - ** ---------- .> results: redis://localhost/0 64 | - *** --- * --- .> concurrency: 2 (prefork) 65 | -- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker) 66 | --- ***** ----- 67 | -------------- [queues] 68 | .> celery exchange=celery(direct) key=celery 69 | 70 | 71 | [tasks] 72 | . tasks.square_root 73 | 74 | [2023-03-06 16:12:10,866: INFO/MainProcess] Connected to redis://localhost:6379/0 75 | [2023-03-06 16:12:10,871: INFO/MainProcess] mingle: searching for neighbors 76 | [2023-03-06 16:12:11,897: INFO/MainProcess] mingle: all alone 77 | [2023-03-06 16:12:11,929: INFO/MainProcess] celery@ch1.nauu.com ready. 78 | ``` 79 | 80 | 现在,**Celery Server**等待接收任务并且发送给`workers`。 81 | 82 | 下一步就是在客户端创建应用调用`tasks`。 83 | 84 | !!! info "" 85 | 86 | 上述步骤不能忽略,因为下面会用在之前创建的东西。 87 | 88 | 在客户端机器,我们有**celery_env**虚拟环境,现在创建一个`task_dispatcher.py`模块很简单,如下步骤; 89 | 90 | 1. 导入logging模块来显示程序执行信息,导入Celery模块: 91 | 92 | ```python 93 | import logging 94 | from celery import Celery 95 | ``` 96 | 97 | 2. 下一步是创建Celery实例,和服务端一样: 98 | 99 | ```python 100 | #logger configuration... 101 | app = Celery('tasks', broker='redis://192.168.25.21:6379/0') 102 | app.conf.CELERY_RESULT_BACKEND = 'redis://192.168.25.21:6397/0' 103 | ``` 104 | 105 | 由于我们在接下的内容中要复用这个模块来实现任务的调用,下面我们创建一个方法来封装`sqrt_task(value)`的发送,我们将创建`manage_sqrt_task(value)`方法: 106 | 107 | ```python 108 | def manage_sqrt_task(value): 109 | result = app.send_task('tasks.sqrt_task', args=(value,)) 110 | logging.info(result.get()) 111 | ``` 112 | 113 | 从上述代码我们发现客户端应用不需要知道服务端的实现。通过**Celery**类中的`send_task`方法,我们传入`module.task`格式的字符串和以元组的方式传入参数就可以调用一个任务。最后,我们看一看`log`中的结果。 114 | 在`__main__`中,我们调用了`manage_sqrt_task(value)`方法: 115 | 116 | ```python 117 | if __name__ == '__main__': 118 | manage_sqrt_task(4) 119 | ``` 120 | 121 | 下面的截图是执行`task_dispatcher.py`文件的结果: 122 | 123 | ```shell 124 | [2023-03-06 16:18:45,481: INFO/MainProcess] Task tasks.sqrt_task[3ecab729-f1cb-4f29-bb47-b713b2e563ed] received 125 | [2023-03-06 16:18:45,500: INFO/ForkPoolWorker-2] Task tasks.sqrt_task[3ecab729-f1cb-4f29-bb47-b713b2e563ed] succeeded in 0.015412827953696251s: 2.0 126 | ``` 127 | 128 | 在客户端,通过`get()`方法得到结果,这是通过`send_task()`返回的`AsyncResult`实例中的重要特征。结果如下图: 129 | 130 | ```shell 131 | $# python task_dispatcher.py 132 | 2023-03-06 16:26:05,841 - 2.0 133 | ``` 134 | 135 | ## 完整案例 136 | 137 | `tasks.py` 138 | 139 | ```python 140 | from math import sqrt 141 | from celery import Celery 142 | 143 | app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') 144 | 145 | 146 | @app.task 147 | def sqrt_task(value): 148 | return sqrt(value) 149 | ``` 150 | 151 | `task_dispatcher.py` 152 | 153 | ```python 154 | import logging 155 | from celery import Celery 156 | 157 | logger = logging.getLogger() 158 | logger.setLevel(logging.DEBUG) 159 | formatter = logging.Formatter('%(asctime)s - %(message)s') 160 | 161 | ch = logging.StreamHandler() 162 | ch.setLevel(logging.DEBUG) 163 | ch.setFormatter(formatter) 164 | logger.addHandler(ch) 165 | 166 | app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') 167 | 168 | def manage_sqrt_task(value): 169 | result = app.send_task('tasks.sqrt_task', args=(value,)) 170 | logger.info(result.get()) 171 | 172 | 173 | if __name__ == '__main__': 174 | print(manage_sqrt_task(4)) 175 | ``` 176 | -------------------------------------------------------------------------------- /docs/chapter7/index.md: -------------------------------------------------------------------------------- 1 | # 使用Celery分发任务 2 | 3 | 在上一章中,我们了解并使用了**并行 Python**(parallel Python)。 我们看到了案例研究的实施,包括斐波那契数列项和使用并行 Python 模块的 Web 爬虫。 我们学习了如何使用管道在进程之间建立通信,以及如何在网络中的不同机器之间分配进程。 在本章中,我们将研究如何使用 Celery 框架在网络中的不同机器之间分发任务。 4 | 5 | 在本章中,我们将讨论以下主题: 6 | 7 | - 理解 Celery 8 | - 理解 Celery 的架构 9 | - 搭建环境 10 | - 分派一个简单的任务 11 | - 使用 Celery 获取斐波那契数列项 12 | - 使用 Celery 制作分布式网络爬虫 13 | -------------------------------------------------------------------------------- /docs/chapter7/setting_up_the_environment.md: -------------------------------------------------------------------------------- 1 | # 建立环境 2 | 3 | 在本节中,我们将在 `Linux` 中设置两台机器。 第一个,主机名 `foshan`,将执行客户端角色,应用程序 `Celery` 将在其中调度要执行的任务。 另一台主机名为 `Phoenix` 的机器将执行**代理**(broker)、**结果后端**(result backend)和worker使用的队列的角色。 4 | 5 | ## 配置客户端机器 6 | 7 | 让我们开始设置客户端机器。 在这台机器上,我们将使用 `pyvenv` 工具设置一个 `Python 3.3` 的虚拟环境。 `pyvenv` 的目标是不使用额外的模块污染操作系统中存在的 `Python`,而是将每个项目所需的开发环境分开。 我们将执行以下命令来创建我们的虚拟环境: 8 | 9 | ```shell 10 | $pyvenv celery_env 11 | ``` 12 | 13 | 上述命令在当前路径创建一个名为`celery_env`的文件夹,里面包含所有Python开发环境必须的结构。下图是该目录所包含的内容: 14 | 15 | ```shell 16 | # 这里使用的最新的python venv模块 17 | $# ./Python-3.9.14/python -m venv celery_env 18 | $# ls celery_env/ 19 | bin include lib lib64 pyvenv.cfg 20 | ``` 21 | 22 | 在创建了虚拟环境之后,我们就可以开始工作并安装需要使用的包。然而,首先我们得激活这个环境,执行以下命名: 23 | 24 | ```shell 25 | $# source celery_env/bin/activate 26 | ``` 27 | 28 | 当命令行提示符改变了,例如在左边出现`celery_env`,就说明激活完成。所有你安装的包都只在这个目录下有效,而不是在整个系统中有效。 29 | 30 | ```shell 31 | (celery_env) $# ls celery_env/ 32 | bin include lib lib64 pyvenv.cfg 33 | ``` 34 | 35 | !!! info "" 36 | 37 | 用`--system-site-packages`标识可以创建能够访问系统`site-packages`的虚拟环境,但是不推荐使用。 38 | 39 | 现在,我们有一个虚拟环境,假设已经安装好了`setuptools`或者`pip`。下面为客户端安装必须的包,如下命令: 40 | 41 | ```shell 42 | $pip install celery 43 | ``` 44 | 45 | 下图是已经安装好的framework v3.1.9,将在本书中使用该版本。 46 | 47 | ```shell 48 | # 由于当前(2023)python2已不再支持,顾这里安装的最新版本v5.2.7 49 | (celery_env) $# python 50 | Python 3.9.14 (main, Sep 19 2022, 12:04:09) 51 | [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux 52 | Type "help", "copyright", "credits" or "license" for more information. 53 | >>> import celery 54 | >>> celery.VERSION 55 | version_info_t(major=5, minor=2, micro=7, releaselevel='', serial='') 56 | >>> 57 | ``` 58 | 59 | 现在我们要在**Celery**中安装支持的**Redis**,这样客户端就可以通过`broker`传输消息了。用如下命令: 60 | 61 | ```shell 62 | $pip install celery[redis] 63 | ``` 64 | 65 | 现在我们的客户端环境配置好了,在开始编码之前,我们必须配置好服务器端的环境。 66 | 67 | ## 配置服务器 68 | 69 | 为了配置服务器,我们首先安装**Redis**,**Redis**将作为`broker`和`result backend`。使用如下命令: 70 | 71 | ```shell 72 | $sudo apt-get install redis-server 73 | ``` 74 | 75 | 启动Redis: 76 | 77 | ```shell 78 | $redis-server 79 | ``` 80 | 81 | 如果成功,会出现类似下图中的输出 82 | 83 | ```log 84 | 2905:C 06 Mar 15:53:46.571 * supervised by systemd, will signal readiness 85 | _._ 86 | _.-``__ ''-._ 87 | _.-`` `. `_. ''-._ Redis 3.2.12 (00000000/0) 64 bit 88 | .-`` .-```. ```\/ _.,_ ''-._ 89 | ( ' , .-` | `, ) Running in standalone mode 90 | |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 91 | | `-._ `._ / _.-' | PID: 2905 92 | `-._ `-._ `-./ _.-' _.-' 93 | |`-._`-._ `-.__.-' _.-'_.-'| 94 | | `-._`-._ _.-'_.-' | http://redis.io 95 | `-._ `-._`-.__.-'_.-' _.-' 96 | |`-._`-._ `-.__.-' _.-'_.-'| 97 | | `-._`-._ _.-'_.-' | 98 | `-._ `-._`-.__.-'_.-' _.-' 99 | `-._ `-.__.-' _.-' 100 | `-._ _.-' 101 | `-.__.-' 102 | 103 | 2905:M 06 Mar 15:53:46.574 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. 104 | 2905:M 06 Mar 15:53:46.574 # Server started, Redis version 3.2.12 105 | 2905:M 06 Mar 15:53:46.574 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect. 106 | 2905:M 06 Mar 15:53:46.574 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. 107 | 2905:M 06 Mar 15:53:46.574 * The server is now ready to accept connections on port 6379 108 | ``` 109 | -------------------------------------------------------------------------------- /docs/chapter7/understanding_celery.md: -------------------------------------------------------------------------------- 1 | # 理解Celery 2 | 3 | **Celery** 是一个框架,该框架提供机制来简化构建分布式系统的过程。 Celery 框架通过在作为网络互连的机器或本地网络之间交换消息来使用**工作单元**(tasks)分布的概念。 任务是 **Celery** 中的关键概念; 我们必须分发的任何类型的工作都必须事先封装在任务中。 4 | 5 | ## 为什么使用Celery 6 | 7 | 它以透明的方式在分布在 Internet 上的工作人员或本地工作人员之间分配任务 8 | 9 | Celery有如下优点: 10 | 11 | - 它以透明的方式在网络上分布的worker或本地网络之间分配任务 12 | - 它通过设置(进程、线程、Gevent、Eventlet)以一种简单的方式改变了worker的并发性 13 | - 支持同步、异步、周期、定时任务 14 | - 它会在出现错误时重新执行任务 15 | 16 | !!! info "" 17 | 18 | 很多开发者都认为**同步任务**(synchronous tasks)和**实时任务**(real-time tasks)是一样的,实际上它们是完全不同的。对于**实时任务**,它有一个时间窗口,任务执行必须在`Deadline`之前完成。如果经过分析,任务在时间窗口内完成不了,那么它将被终止或者暂停直到下次能够完成,而**同步任务**是当任务执行完后才返回结果。 19 | -------------------------------------------------------------------------------- /docs/chapter7/understanding_celery_architecture.md: -------------------------------------------------------------------------------- 1 | # 理解Celery架构 2 | 3 | Celery架构基于**可插拔组件**(pluggable components)和根据选择的**消息传输**(代理)(message transport(broker))协议实现的消息交换机制。下图说明了这一点: 4 | 5 | ![1](../imgs/7-01.png) 6 | 7 | 现在,让我们详细的介绍Celery的每个组件。 8 | 9 | ## 处理任务 10 | 11 | 在上图中的*client*组件,有创建和分派任务到brokers的方法。 12 | 13 | 分析如下示例代码来演示通过使用`@app.task`装饰器来定义一个任务,它可以被一个**Celery**应用的实例访问,下面代码展示了一个简单的`Hello World app`: 14 | 15 | ```python 16 | @app.task 17 | def hello_world(): 18 | return "Hello I'm a celery task" 19 | ``` 20 | 21 | !!! info "" 22 | 23 | 任何可执行的方法或对象都可以成为任务 (Any callable can be a task.) 24 | 25 | 正如我们前面提到的,有几种类型的任务:`同步`、`异步`、`定期`和`计划`。 当我们执行任务调用时,它会返回一个 `AsyncResult` 类型的实例。 `AsyncResult` 对象是一个对象,它允许检查任务状态、它的结束,并且很明显,它在存在时返回。 但是,要使用此机制,另一个组件(结果后端)必须处于活动状态。 这将在本章中进一步解释。 要分派任务,我们应该使用任务的以下一些方法: 26 | 27 | - `delay(arg, kwarg=value)` : 这是调用 `apply_async` 方法的快捷方式。 28 | - `apply_async((arg,), {'kwarg': value})` : 这允许为任务的执行设置一系列有趣的参数。 其中一些如下: 29 | - `countdown` : 默认任务是立即执行,该参数设置经过`countdown`秒之后执行。 30 | - `expires` : 代表经过多长时间终止。 31 | - `retry` : 此参数决定在连接或发送任务失败的情况下,是否必须重新发送。 32 | - `queue` : 该任务所处的任务队列。 33 | - `serializer` : 这表示磁盘中任务序列化的数据格式,一些示例包括 json、yaml 等。 34 | - `link` : 如果发送的任务成功执行,这将链接一个或多个要执行的任务。 35 | - `link_error` : 这将在任务执行失败的情况下链接一个或多个要执行的任务。 36 | - `apply((arg,), {'kwarg': value})` : 这会以同步方式在本地进程中执行任务,从而阻塞直到结果准备就绪为止。 37 | 38 | !!! info "" 39 | 40 | Celery 还提供了伴随任务状态的机制,这对于跟踪和映射处理的真实状态非常有用。 有关内置任务状态的更多信息,请访问{target="_blank"} 41 | 42 | ## 理解消息转发(broker) 43 | 44 | `broker`绝对是 **Celery** 中的关键组成部分。 通过它,我们可以发送和接收消息并与`worker`沟通。 **Celery** 支持大量的代理。 然而,对于其中一些,并不是所有的 `Celery` 机制都得到了实现。 就功能而言最完整的是 `RabbitMQ` 和 `Redis`。 在本书中,我们将使用 `Redis` 作为`broker`和结果后端。 `broker`的功能是在发送任务的客户端应用程序和执行任务的工作线程之间提供一种通信方式。 这是通过使用任务队列完成的。 我们可以有几台带有代理的网络机器等待接收消息以供`workers`使用。 45 | 46 | ## 理解workers 47 | 48 | `Workers`负责执行接收到的任务。**Celery**提供了一系列的机制,我们可以选择最合适的方式来控制`workers`的行为。这些机制如下: 49 | 50 | - **并发模式**(Concurrency mode):例如**进程**、**线程**、**协程**(Eventlet)和**Gevent**。 51 | - **远程控制**(Remote control):使用这种机制,可以通过高优先级队列发送消息到某个特定的`worker`来改变行为,包括在**运行时**(runtime)。 52 | - **撤销任务**(Revoking tasks):使用这种机制,我们可以指示一个或多个`worker`忽略一个或多个任务的执行。 53 | 54 | 如果需要,可以在运行时设置甚至更改更多功能。 比如`worker`在一段时间内执行的任务数,`worker`从哪个`queue`中消耗的时间最多等等。 有关`worker`的更多信息,请访问{target="_blank"} 55 | 56 | ## 理解result backends 57 | 58 | **结果后端**(result backend)组件的作用是存储返回给客户端应用程序的任务的状态和结果。 从 `Celery` 支持的结果后端,比较出彩的有 `RabbitMQ`、`Redis`、`MongoDB`、`Memcached` 等。 前面列出的每个**结果后端**(result backend)都有优点和缺点。 有关详细信息,请参阅 {target="_blank"}。 59 | 60 | 现在,我们对 `Celery` 架构及其组件有了一个大致的了解。 因此,让我们建立一个开发环境来实现一些例子。 61 | -------------------------------------------------------------------------------- /docs/chapter7/using_celery_to_make_a_distributed_web_crawler.md: -------------------------------------------------------------------------------- 1 | # 用Celery来构建一个分布式网络爬虫系统 2 | 3 | 现在我们将用`Celery`构建网络爬虫。我们已经有了`webcrawler_queue`,负责`hcrawler`任务。然而,在服务器端,我们将在`tasks.py`模块创建`crawl_task`任务。 4 | 5 | 首先,导入`re`(正则表达式)和`requests`(HTTP lib)模块,代码如下: 6 | 7 | ```python 8 | import re 9 | import requests 10 | ``` 11 | 12 | 然后,定义正则表达式,和之前的章节一样; 13 | 14 | ```python 15 | hTML_link_regex = re.compile('') 16 | ``` 17 | 18 | 然后,替换`crawl_task`方法,添加`@app.task`装饰器,修改返回信息,如下: 19 | 20 | ```python 21 | @app.task 22 | def crawl_task(url): 23 | request_data = requests.get(url) 24 | links = html_link_regex.findall(request_data.text) 25 | message = "The task %s found the following links %s.." %(url, links) 26 | return message 27 | ``` 28 | 29 | `links`列表不一定要和下图匹配: 30 | 31 | ![1](../imgs/7-12.png) 32 | 33 | 然后让我们再次向上滚动 `Celery` 并查看。 此时,随着我们的新任务加载,是时候在客户端的 `task_dispatcher.py` 模块中实现名为 `crawl_task` 的任务了。 34 | 35 | 首先,我们需要列出数据的输入`url_list`。代码如下: 36 | 37 | ```python 38 | url_list = ['http://www.baidu.com', 39 | 'http://cn.bing.com', 40 | 'http://www.qq.com', 41 | 'http://www.github.com', 42 | ] 43 | ``` 44 | 45 | 创建`manage_crawl_task`方法。 46 | 47 | ```python 48 | def manage_crawl_task(url_list): 49 | async_result_dict = {url: app.send_task('tasks.crawl_task', 50 | args=(url,), queue='webcrawler_queue', 51 | routing_key='webcrawler_queue') for url in url_list} 52 | for key, value in async_result_dict.items(): 53 | if value.ready(): 54 | logger.info("%s -> %s" % (key, value.get())) 55 | else: 56 | logger.info("The task [%s] is not ready" % value.task_id) 57 | ``` 58 | 59 | 和之前创建的`manage_fibo_task`方法一样,`async_result_dict`字典包含当前URL和`AsyncResult`结果。然后我们检查任务的状态获取任务结果。 60 | 61 | 现在我们在`__main__`中调用该方法: 62 | 63 | ```python 64 | 65 | if __name__ == '__main__': 66 | #manage_sqrt_task(4) 67 | #manage_fibo_task(input_list) 68 | manage_crawl_task(url_list) 69 | ``` 70 | 71 | 运行`task_dispatcher.py`代码,在服务器端有如下输出: 72 | 73 | ```shell 74 | 75 | ``` 76 | 77 | ![1](../imgs/7-13.png) 78 | 79 | 最后,客户端的输出如下: 80 | 81 | ![1](../imgs/7-14.png) 82 | 83 | **Celery**是一个强大的工具,在本章我们只是用到了基本的东西。更多的内容建议自己在真实的项目中动手去尝试。 84 | 85 | ## 完整案例 86 | 87 | `tasks.py` 88 | 89 | ```python 90 | from math import sqrt 91 | from celery import Celery 92 | import re 93 | import requests 94 | 95 | app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') 96 | # app.config.CELERY_RESULT_BACKEND = 'redis://192.168.99.89:6379/0' 97 | 98 | 99 | @app.task 100 | def sqrt_task(value): 101 | return sqrt(value) 102 | 103 | @app.task 104 | def fibo_task(value): 105 | a, b = 0,1 106 | for item in range(value): 107 | a, b = b, a + b 108 | message = "The Fibonacci calculated with task id %s was %d" % (fibo_task.request.id, a) 109 | return (value, message) 110 | 111 | html_link_regex = re.compile('') 112 | 113 | @app.task 114 | def crawl_task(url): 115 | request_data = requests.get(url) 116 | links = html_link_regex.findall(request_data.text) 117 | message = "The task %s found the following links %s.." %(url, links) 118 | 119 | return message 120 | ``` 121 | 122 | `tasks_dispatcher.py` 123 | 124 | ```python 125 | import logging 126 | from celery import Celery 127 | from celery.result import AsyncResult 128 | from typing import Dict 129 | 130 | logger = logging.getLogger() 131 | logger.setLevel(logging.DEBUG) 132 | formatter = logging.Formatter('%(asctime)s - %(message)s') 133 | 134 | ch = logging.StreamHandler() 135 | ch.setLevel(logging.DEBUG) 136 | ch.setFormatter(formatter) 137 | logger.addHandler(ch) 138 | 139 | app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') 140 | 141 | def manage_sqrt_task(value): 142 | result = app.send_task('tasks.sqrt_task', args=(value,), queue='sqrt_queue', routing_key='sqrt_queue') 143 | logger.info(result.get()) 144 | 145 | 146 | 147 | def manage_fibo_task(value_list): 148 | async_result_dict: Dict[int, AsyncResult] = {x: app.send_task('tasks.fibo_task',args=(x,), queue='fibo_queue', routing_key='fibo_queue') for x in value_list} 149 | 150 | for key, value in async_result_dict.items(): 151 | if value.ready(): 152 | logger.info("Value [%d] -> %s" % (key, value.get()[1])) 153 | else: 154 | logger.info("Task [%s] is not ready" % value.task_id) 155 | 156 | 157 | def manage_crawl_task(url_list): 158 | async_result_dict: Dict[str, AsyncResult] = {url: app.send_task('tasks.crawl_task', args=(url,), queue='webcrawler_queue',routing_key='webcrawler_queue') for url in url_list} 159 | for key, value in async_result_dict.items(): 160 | if value.ready(): 161 | logger.info("%s -> %s" % (key, value.get())) 162 | else: 163 | logger.info("The task [%s] is not ready" % value.task_id) 164 | 165 | url_list = [ 166 | 'http://www.baidu.com', 167 | 'http://cn.bing.com', 168 | 'http://www.qq.com', 169 | 'http://www.github.com', 170 | 'http://br.search.yahoo.com' 171 | ] 172 | 173 | if __name__ == '__main__': 174 | input_list = [4, 3, 8, 6, 10] 175 | # print(manage_sqrt_task(4)) 176 | # print(manage_fibo_task(input_list)) 177 | print(manage_crawl_task(url_list)) 178 | ``` 179 | -------------------------------------------------------------------------------- /docs/chapter7/using_celery_to_obtain_a_fibonacci_series_term.md: -------------------------------------------------------------------------------- 1 | # 使用 Celery 获取斐波那契数列项 2 | 3 | 让我们再次去分配我们的多个输入,以计算第 `n` 个斐波那契项,每个项都以分布式方式计算。 计算 `Fibonacci` 的函数相对于前面的章节会有一些变化。 变化很小; 现在我们有了 `@app.task` 装饰器和返回消息中的一个小改动。 4 | 5 | 在代理所在的服务器计算机中的 `tasks.py` 模块(之前创建)中,我们将停止执行 `Celery(Ctrl + C`)并添加 `fibo_task` 任务。 这是通过使用以下代码完成的: 6 | 7 | ```python 8 | @app.task 9 | def fibo_task(value): 10 | a, b = 0,1 11 | for item in range(value): 12 | a, b = b, a + b 13 | message = "The Fibonacci calculated with task id %s was %d" % (fibo_task.request.id, a) 14 | return (value, message) 15 | ``` 16 | 17 | 通过`ask.reaquest.id`得到任务的ID,请求对象是`task`的对象,`task`对象提供了`task`执行的上下文。通过上下文可以得到`task`的ID等信息。 18 | 19 | 在`tasks.py`模块加入了新的任务之后,再一次初始化**Celery**,结果如下图: 20 | 21 | ```log 22 | - *** --- * --- .> concurrency: 2 (prefork) 23 | -- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker) 24 | --- ***** ----- 25 | -------------- [queues] 26 | .> celery exchange=celery(direct) key=celery 27 | 28 | 29 | [tasks] 30 | . tasks.fibo_task 31 | . tasks.sqrt_task 32 | 33 | [2023-03-06 17:05:34,402: INFO/MainProcess] Connected to redis://localhost:6379/0 34 | ``` 35 | 36 | 现在我们把`fibo_task`任务装载到**Celery server**,我们将在客户端实现对该任务的调用。 37 | 38 | 在`task_dispatcher.py`模块,我们会申明`input_list`,如下: 39 | 40 | ```python 41 | input_list = [4, 3, 8, 6, 10] 42 | ``` 43 | 44 | 和前面的做法一样,定义`manage_fibo_task`方法: 45 | 46 | 正如我们在上一节创建的 `sqrt_task` 任务中所做的那样,我们将创建一个方法来组织我们的调用而不污染 `__main__` 块。 我们将此函数命名为 `manage_fibo_task`。 如以下实现: 47 | 48 | ```python 49 | def manage_fibo_task(value_list): 50 | async_result_dict = {x: app.send_task('tasks.fibo_task', 51 | args=(x,)) for x in value_list} 52 | 53 | for key, value in async_result_dict.items(): 54 | logger.info("Value [%d] -> %s" % (key, value.get()[1])) 55 | ``` 56 | 57 | 在 `manage_fibo_task` 函数中,我们创建了一个名为 `async_result_dict` 的字典,填充相同的键值对。 `key` 是作为参数传递的项,用于获取 `Fibonacci` 的无数项,`value` 是从调用 `send_task` 方法返回的 `AsyncResult` 的实例。 通过这种方法,我们可以监控任务的状态和结果。 58 | 59 | 最后,遍历字典得到输入值和输出结果并封装成字典。`AsyncResult`类的`get()`函数可以让我们获取处理结果。 60 | 61 | `get()`方法会阻塞进程。一个好的方法是调用`ready()`方法来检查结果是否返回了。 62 | 63 | 可能会注意到 `get()` 函数可能不会立即返回结果,因为处理仍在进行。 在客户端调用 `get()` 方法可以阻止调用之后的处理。 将调用结合到 `ready()` 方法是个好主意,这样可以检查是否准备好获取结果。 64 | 65 | 因此,结果展示循环可以修改为如下代码: 66 | 67 | ```python 68 | for key, value in async_result_dict.items(): 69 | if value.ready(): 70 | logger.info("Value [%d] -> %s" % (key, value.get()[1])) 71 | else: 72 | logger.info("Task [%s] is not ready" % value.task_id) 73 | ``` 74 | 75 | Depending on the type of task to be executed, there may be a considerable delay in the result. Therefore, by calling get() without considering the return status, we can block the code running at the point where the get() function was called. To tackle this, we should define an argument called timeout in the get(timeout=x) method. So, by minimizing this blocking, we can prevent tasks from having problems in returning results, which would impact the running of the execution for an indefinite time. 76 | 77 | 根据要执行的任务类型,结果可能会有相当长的延迟。 因此,通过调用 `get()` 而不考虑返回状态,我们可以阻止代码运行在 `get()` 函数被调用的地方。 为了解决这个问题,我们应该在 `get(timeout=x)` 方法中定义一个名为 `timeout` 的参数。 因此,通过最小化这种阻塞,我们可以防止任务在返回结果时出现问题,这会无限期地影响执行的运行。 78 | 79 | 最后,我们添加了对 `manage_fibo_task` 函数的调用,作为参数传递给我们的 `input_list`。 代码如下: 80 | 81 | ```python 82 | if __name__ == '__main__': 83 | #manage_sqrt_task(4) 84 | manage_fibo_task(input_list) 85 | ``` 86 | 87 | 当我们执行`task_dispatcher.py`中的代码时,可以在旁边看到如下输出服务器: 88 | 89 | ```shell 90 | $# python task_dispatcher.py 91 | 2023-03-06 17:20:38,902 - Value [4] -> The Fibonacci calculated with task id 03328f4d-8226-4a15-853d-8b8ab5833b72 was 3 92 | 2023-03-06 17:20:38,904 - Value [3] -> The Fibonacci calculated with task id 4ea527de-0a96-4c6c-ac25-8e4a01a0e919 was 2 93 | 2023-03-06 17:20:38,906 - Task [448d8127-763c-4025-84a1-e05e9979841a] is not ready 94 | 2023-03-06 17:20:38,909 - Task [f639a24f-cbf5-403d-b243-4c5c54f5b77a] is not ready 95 | 2023-03-06 17:20:38,909 - Task [4e2999a7-bae8-454c-9f70-8b513dd0844e] is not ready 96 | ``` 97 | 98 | 在客户端有如下输出: 99 | 100 | ```shell 101 | -- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker) 102 | --- ***** ----- 103 | -------------- [queues] 104 | .> celery exchange=celery(direct) key=celery 105 | 106 | 107 | [tasks] 108 | . tasks.fibo_task 109 | . tasks.sqrt_task 110 | 111 | [2023-03-06 17:20:35,572: INFO/MainProcess] Connected to redis://localhost:6379/0 112 | [2023-03-06 17:20:35,578: INFO/MainProcess] mingle: searching for neighbors 113 | [2023-03-06 17:20:36,599: INFO/MainProcess] mingle: all alone 114 | [2023-03-06 17:20:36,616: INFO/MainProcess] celery@ch1.nauu.com ready. 115 | [2023-03-06 17:20:38,859: INFO/MainProcess] Task tasks.fibo_task[03328f4d-8226-4a15-853d-8b8ab5833b72] received 116 | [2023-03-06 17:20:38,877: INFO/MainProcess] Task tasks.fibo_task[4ea527de-0a96-4c6c-ac25-8e4a01a0e919] received 117 | [2023-03-06 17:20:38,884: INFO/MainProcess] Task tasks.fibo_task[448d8127-763c-4025-84a1-e05e9979841a] received 118 | [2023-03-06 17:20:38,890: INFO/MainProcess] Task tasks.fibo_task[f639a24f-cbf5-403d-b243-4c5c54f5b77a] received 119 | [2023-03-06 17:20:38,896: INFO/MainProcess] Task tasks.fibo_task[4e2999a7-bae8-454c-9f70-8b513dd0844e] received 120 | [2023-03-06 17:20:38,898: INFO/ForkPoolWorker-2] Task tasks.fibo_task[03328f4d-8226-4a15-853d-8b8ab5833b72] succeeded in 0.03570139221847057s: (4, 'The Fibonacci calculated with task id 03328f4d-8226-4a15-853d-8b8ab5833b72 was 3') 121 | [2023-03-06 17:20:38,903: INFO/ForkPoolWorker-1] Task tasks.fibo_task[4ea527de-0a96-4c6c-ac25-8e4a01a0e919] succeeded in 0.02462736703455448s: (3, 'The Fibonacci calculated with task id 4ea527de-0a96-4c6c-ac25-8e4a01a0e919 was 2') 122 | [2023-03-06 17:20:38,920: INFO/ForkPoolWorker-2] Task tasks.fibo_task[448d8127-763c-4025-84a1-e05e9979841a] succeeded in 0.014719393104314804s: (8, 'The Fibonacci calculated with task id 448d8127-763c-4025-84a1-e05e9979841a was 21') 123 | [2023-03-06 17:20:38,928: INFO/ForkPoolWorker-2] Task tasks.fibo_task[4e2999a7-bae8-454c-9f70-8b513dd0844e] succeeded in 0.0018890555948019028s: (10, 'The Fibonacci calculated with task id 4e2999a7-bae8-454c-9f70-8b513dd0844e was 55') 124 | [2023-03-06 17:20:38,931: INFO/ForkPoolWorker-1] Task tasks.fibo_task[f639a24f-cbf5-403d-b243-4c5c54f5b77a] succeeded in 0.012269522994756699s: (6, 'The Fibonacci calculated with task id f639a24f-cbf5-403d-b243-4c5c54f5b77a was 8') 125 | ``` 126 | 127 | ## 完整示例 128 | 129 | `tasks.py` 130 | 131 | ```python 132 | from math import sqrt 133 | from celery import Celery 134 | 135 | app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') 136 | # app.config.CELERY_RESULT_BACKEND = 'redis://192.168.99.89:6379/0' 137 | 138 | 139 | @app.task 140 | def sqrt_task(value): 141 | return sqrt(value) 142 | 143 | @app.task 144 | def fibo_task(value): 145 | a, b = 0,1 146 | for item in range(value): 147 | a, b = b, a + b 148 | message = "The Fibonacci calculated with task id %s was %d" % (fibo_task.request.id, a) 149 | return (value, message) 150 | ``` 151 | 152 | `tasks_dispatcher.py` 153 | 154 | ```python 155 | import logging 156 | from celery import Celery 157 | from celery.result import AsyncResult 158 | from typing import Dict 159 | 160 | logger = logging.getLogger() 161 | logger.setLevel(logging.DEBUG) 162 | formatter = logging.Formatter('%(asctime)s - %(message)s') 163 | 164 | ch = logging.StreamHandler() 165 | ch.setLevel(logging.DEBUG) 166 | ch.setFormatter(formatter) 167 | logger.addHandler(ch) 168 | 169 | app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') 170 | 171 | def manage_sqrt_task(value): 172 | result = app.send_task('tasks.sqrt_task', args=(value,)) 173 | logger.info(result.get()) 174 | 175 | 176 | 177 | def manage_fibo_task(value_list): 178 | async_result_dict: Dict[int, AsyncResult] = {x: app.send_task('tasks.fibo_task',args=(x,)) for x in value_list} 179 | 180 | for key, value in async_result_dict.items(): 181 | if value.ready(): 182 | logger.info("Value [%d] -> %s" % (key, value.get()[1])) 183 | else: 184 | logger.info("Task [%s] is not ready" % value.task_id) 185 | 186 | if __name__ == '__main__': 187 | input_list = [4, 3, 8, 6, 10] 188 | # print(manage_sqrt_task(4)) 189 | print(manage_fibo_task(input_list)) 190 | ``` 191 | -------------------------------------------------------------------------------- /docs/chapter7/总结.md: -------------------------------------------------------------------------------- 1 | # 小结 2 | 3 | 在本章中,我们讨论了 `Celery` 分布式任务队列。 我们还通过示意图透视了它的架构,分析了它的关键组件,并了解到如何设置环境以使用 `Celery` 构建基本应用程序。 如果单独讲`Celery`可以写一本书,通过本章希望你对`Celery`有了一个基本的认识。 4 | 5 | 在下一章中,我们将学习 `asyncio` 模块以及如何以异步方式执行程序。 我们还将简要介绍`协程`(coroutines),并学习如何将它们与 `asyncio` 一起使用。 6 | -------------------------------------------------------------------------------- /docs/chapter8/index.md: -------------------------------------------------------------------------------- 1 | # 异步编程 2 | 3 | 在上一章中,我们学习了如何使用 Celery 框架分发任务以及在通过网络连接的不同机器上并行计算。 现在,我们将探索异步编程、事件循环和协程,它们是 Python 3.4 版中可用的 `asyncio` 模块中的特色资源。 我们还将学习将它们与执行器结合使用。 4 | 5 | 在本章中,我们将介绍: 6 | 7 | - 阻塞、非阻塞和异步操作 8 | - 了解事件循环 9 | - 使用`asyncio`(异步)框架 10 | -------------------------------------------------------------------------------- /docs/chapter8/使用asyncio.md: -------------------------------------------------------------------------------- 1 | # 使用asyncio 2 | 3 | 我们可以将 `asyncio` 定义为一个模块,用于重启 Python 中的异步编程。 `asyncio` 模块允许使用以下元素的组合来实现异步编程: 4 | 5 | - **Event loop**: 这已在上一节中定义。 `asyncio` 模块允许每个进程有一个事件循环。 6 | - **Coroutines(协程)**: 正如`asyncio`官方文档中所说,“**协程是一种遵循一定约定的生成器**”。 它最有趣的特性是它可以**在执行期间挂起**以等待外部处理(I/O 中的某些例程)完成, 并在外部处理完成后又可以从原来的位置恢复执行。 7 | - **Futures**: `asyncio` 模块定义了自己的对象 `Future`。 `Futures` 代表一个尚未完成的处理过程。 8 | - **Tasks**: 这是 `asyncio.Future` 的子类,用于**封装**和**管理**协程。 9 | 10 | 除了这些机制之外,`asyncio` 还为应用程序的开发提供了一系列其他功能,例如传输和协议,它们允许使用 `TCP`、`SSL`、`UDP` 和`管道`等通过通道进行通信。 有关 `asyncio` 的更多信息,请访问 {target="_blank"}。 11 | 12 | ## 理解coroutines和futures 13 | 14 | 为了能够在`asyncio`中定义一个`coroutine`,我们使用`@asyncio.coroutine`装饰器,并且我们必须利用`yield from`语法来暂停`coroutine`,以便执行一个I/O操作或者其他可能阻塞**事件循环**的计算。但是这种暂停和恢复的机制是如何工作的呢?`Coroutine`与`asyncio.Future`对象一起工作。我们可以把这个操作总结如下: 15 | 16 | - 初始化协程,并在内部实例化一个 `asyncio.Future` 对象或将其作为参数传递给协程。 17 | - 在到达使用 `yield from` 的协程点时,协程将暂停以等待在 `yield from` 中引发的计算。 `yield from instance` 等待 `yield from` 的构造。 18 | - 当 `yield from` 中引发的计算结束后,协程执行协程相关的 `asyncio.Future` 对象的 `set_result()` 方法,告诉事件循环可以恢复协程。 19 | 20 | !!! info "" 21 | 22 | 当我们使用 `asyncio.Task` 对象封装协程时,我们不需要显式使用 `asyncio.Future` 对象,因为 `asyncio.Task` 对象已经是 `asyncio.Future` 的子类。 23 | 24 | ## 使用coroutine和asyncio.Future 25 | 26 | 下面是使用`coroutine`和`asyncio.Future`对象的一些例子: 27 | 28 | ```python 29 | import asyncio 30 | 31 | @asyncio.coroutine 32 | def sleep_coroutine(f): 33 | yield from asyncio.sleep(2) 34 | f.set_result("Done!") 35 | ``` 36 | 37 | 在上述代码中,我们定义了名为 `sleep_coroutine` 的协程,它接收一个 `asyncio.Future`对象 作为参数。 在`sleep_coroutine`中,我们的协程将运行 `asyncio.sleep(2)` 导致暂停执行并休眠 2 秒; 我们必须观察到 `asyncio.sleep` 函数已经与 `asyncio` 兼容。 因此,它作为未来返回; 然而,由于教学原因,我们包含了作为参数传递的 `asyncio.Future` 对象,以说明如何通过 `asyncio.Future.set_result()` 在协程中显式完成恢复。 38 | 39 | 最终,我们有了我们的主函数,我们在其中创建了我们的 `asyncio.Future` 对象,并在 `loop = asyncio.get_event_loop()` 行中,我们从 `asyncio` 创建了一个事件循环实例来执行我们的协程,如下代码所示: 40 | 41 | ```python 42 | if __name__ == '__main__': 43 | future = asyncio.Future() 44 | loop = asyncio.get_event_loop() 45 | loop.run_until_complete(sleep_coroutine(future)) 46 | ``` 47 | 48 | !!! info "" 49 | 50 | 任务和协程仅在**事件循环**(event loop)执行时执行。 51 | 52 | 在最后一行,`loop.run_until_complete(sleep_coroutine(future))`,我们要求我们的事件循环一直运行直到我们的协程完成它的执行。 这是通过 `BaseEventLoop` 类中提供的 `BaseEventLoop.run_until_complete` 方法完成的。 53 | 54 | !!! info "" 55 | 56 | 在 `asyncio` 中恢复协程的魔法在于 `asyncio.Future` 对象的 `set_result` 方法。 所有要恢复的协程都需要等待`asyncio.Future`执行`set_result`方法。 所以,`asyncio` 的事件循环会知道计算已经结束,它可以恢复协程。 57 | 58 | ## 使用asyncio.Task 59 | 60 | 如前所述,`asyncio.Task` 类是 `asyncio.Future` 的子类,旨在管理协程。 让我们检查一个名为 `asyncio_task_sample.py` 的示例代码,其中将创建多个 `asyncio.Task` 对象并在 `asyncio` 的事件循环中分派以执行: 61 | 62 | ```python 63 | import asyncio 64 | 65 | @asyncio.coroutine 66 | def sleep_coro(name, seconds=1): 67 | print("[%s] coroutine will sleep for %d second(s)…" % (name, seconds)) 68 | yield from asyncio.sleep(seconds) 69 | print("[%s] done!" % name) 70 | ``` 71 | 72 | 我们的协程称为 `sleep_coro`,将接收两个参数:`name`,它将用作我们协程的标识符,以及标准值为 1 的`seconds`,它将指示协程将暂停多少秒。 73 | 74 | 在主函数中,我们定义了一个列表,其中包含三个类型为 `asyncio.Task` 的对象,名为 `Task-A`,它将休眠 `10` 秒,以及 `Task-B` 和 `Task-C`,它们将分别休眠 `1` 秒。 请参见以下代码: 75 | 76 | ```python 77 | if __name__ == '__main__': 78 | tasks = [asyncio.Task(sleep_coro('Task-A', 10)), 79 | asyncio.Task(sleep_coro('Task-B')), 80 | asyncio.Task(sleep_coro('Task-C')) 81 | ] 82 | loop = asyncio.get_event_loop() 83 | loop.run_until_complete(asyncio.gather(*tasks)) 84 | ``` 85 | 86 | !!! info "由于学习时,python 3已更新到3.10.2 固如下写法也可以" 87 | 88 | ```python 89 | # python3.10.2 版如下, 使用async、await 关键字 90 | import asyncio 91 | 92 | async def sleep_coro(name, seconds=1): 93 | print("[%s] coroutine will sleep for %d second(s)…" % (name, seconds)) 94 | await asyncio.sleep(seconds) 95 | print("[%s] done!" % name) 96 | 97 | 98 | if __name__ == "__main__": 99 | tasks = [ 100 | asyncio.Task(sleep_coro("Task-A", 10)), 101 | asyncio.Task(sleep_coro("Task-B")), 102 | asyncio.Task(sleep_coro("Task-C")), 103 | ] 104 | loop = asyncio.get_event_loop() 105 | loop.run_until_complete(asyncio.gather(*tasks)) 106 | ``` 107 | 108 | 仍然在主函数中,我们使用 `BaseEventLoop` 定义事件循环。 `run_until_complete` 函数; 然而,这个函数接收的参数不超过一个协程,而是一个对 `asyncio.gather` 的调用,它是作为未来返回的函数,附加接收到的协程或未来列表的结果作为参数。 `asyncio_task_sample.py` 程序的输出如以下屏幕截图所示: 109 | 110 | ![1](../imgs/7-15.png) 111 | 112 | 值得注意的是,程序的输出按照声明的顺序显示正在执行的任务; 但是,它们都不能阻止事件循环。 这是因为 `Task-B` 和 `Task-C` 睡眠较少并且在 `Task-A` 睡眠 `10` 倍并首先被调度之前结束。 `Task-A` 阻塞事件循环的场景是灾难性的。 113 | 114 | ## 使用与asyncio不兼容的库 115 | 116 | `asyncio` 模块在 `Python` 社区中仍然是最新的。 一些库仍然不完全兼容。 让我们重构上一节示例 `asyncio_task_sample.py` 并将函数从 `asyncio.sleep` 更改为 `time.sleep`。 在不作为未来返回的时间模块中休眠并检查其行为。 我们将 `yield from asyncio.sleep(seconds)` 行更改为 `yield from time.sleep(seconds)`。我们显然需要导入时间模块来使用新的睡眠。 运行该示例,请注意以下屏幕截图中显示的输出中的新行为: 117 | 118 | ![1](../imgs/7-16.png) 119 | 120 | 我们可以注意到协程正常初始化,但是由于 `yield from` 语法等待协程或 `asyncio.Future` 而发生错误,并且 `time.sleep` 在其结束时没有生成任何东西。 那么,在这些情况下我们应该如何处理呢? 答案很简单: 我们需要一个 `asyncio.Future` 对象,然后重构我们的示例。 121 | 122 | 首先,让我们创建一个函数,该函数将创建一个 `asyncio.Future` 对象以将其返回到 `sleep_coro` 协程中的 `yield from present`。 `sleep_func`函数如下: 123 | 124 | ```python 125 | def sleep_func(seconds): 126 | f = asyncio.Future() 127 | time.sleep(seconds) 128 | f.set_result("Future done!") 129 | return f 130 | ``` 131 | 132 | 请注意,`sleep_func` 函数在结束时会执行 `f.set_result("Future done!")` 在 `future cause` 中放置一个虚拟结果,因为此计算不会生成具体结果; 它只是一个睡眠功能。 然后,返回一个 `asyncio.Future` 对象,`yield from` 期望它恢复 `sleep_coro` 协程。 以下屏幕截图说明了修改后的 `asyncio_task_sample.py` 程序的输出: 133 | 134 | ![1](../imgs/7-17.png) 135 | 136 | 现在所有已分派的任务都执行无误。 可是等等! 上一个屏幕截图中显示的输出仍然有问题。 请注意,执行顺序内部有些奇怪,因为 **Task-A** 休眠了 10 秒,并在随后两个仅休眠 1 秒的任务开始之前结束。 也就是说,我们的事件循环被任务阻塞了。 这是使用不与 asyncio 异步工作的库或模块的结果。 137 | 138 | 解决此问题的一种方法是将阻塞任务委托给 `ThreadPoolExecutor`(请记住,如果处理受 **I/O** 限制,则此方法效果很好;如果受 **CPU** 限制,请使用 `ProcessPoolExecutor`。为了我们的舒适,`asyncio` 以一种非常简单的方式支持此机制. 让我们再次重构我们的 `asyncio_task_sample.py` 代码,以便在不阻塞**事件循环**的情况下执行任务。 139 | 140 | 首先,我们必须删除 `sleep_func` 函数,因为它不再是必需的。 对 `time.sleep` 的调用将由 `BaseEventLoop.run_in_executor` 方法完成。然后让我们按照以下方式重构我们的 `sleep_coro` 协程: 141 | 142 | ```python 143 | @asyncio.coroutine 144 | def sleep_coro(name, loop, seconds=1): 145 | future = loop.run_in_executor(None, time.sleep, seconds) 146 | 147 | print("[%s] coroutine will sleep for %d second(s)..." % (name, seconds)) 148 | yield from future 149 | print("[%s] done!" % name) 150 | ``` 151 | 152 | !!! info "由于学习时,python 3已更新到3.10.2 固如下写法也可以" 153 | 154 | ```python 155 | # python3.10.2 版如下, 使用async、await 关键字 156 | import asyncio 157 | 158 | async def sleep_coro(name, loop, seconds=1): 159 | future = loop.run_in_executor(None, time.sleep, seconds) 160 | print("[%s] coroutine will sleep for %d second(s)…" % (name, seconds)) 161 | await future 162 | print("[%s] done!" % name) 163 | 164 | 165 | if __name__ == "__main__": 166 | loop = asyncio.get_event_loop() 167 | tasks = [ 168 | asyncio.Task(sleep_coro2("Task-A", loop, 10)), 169 | asyncio.Task(sleep_coro2("Task-B", loop)), 170 | asyncio.Task(sleep_coro2("Task-C", loop)), 171 | ] 172 | 173 | loop.run_until_complete(asyncio.gather(*tasks)) 174 | ``` 175 | 176 | 值得注意的是,协程接收到一个新参数,该参数将是我们在主函数中创建的事件循环,以便使用 `ThreadPoolExecutor` 来响应相同的执行结果。 177 | 178 | 在这之后,我们有下一行: 179 | 180 | ```python 181 | future = loop.run_in_executor(None, time.sleep, seconds) 182 | ``` 183 | 184 | 在上一行中,调用了 `BaseEventLoop.run_in_executor` 函数,它的第一个参数是一个[执行器](https://docs.python.org/3.4/library/concurrent.futures.html#concurrent.futures.Executor)。 如果它通过 `None`,它将使用 `ThreadPoolExecutor` 作为默认值。 第二个参数是一个回调函数,在本例中是 `time.sleep` 函数,代表我们要完成的计算,最后我们可以传递回调参数。 185 | 186 | 请注意,`BaseEventLoop.run_in_executor` 方法返回一个 `asyncio.Future` 对象。 然而,通过返回的 `future` 调用 `yield from` 就足够了,并且我们的协程已经准备好了。 187 | 188 | 记住,我们需要改变程序的主函数,将**事件循环传**(event loop)递给`sleep_coro`。 189 | 190 | ```python 191 | if __name__ == '__main__': 192 | loop = asyncio.get_event_loop() 193 | tasks = [asyncio.Task(sleep_coro('Task-A', loop, 10)), 194 | asyncio.Task(sleep_coro('Task-B', loop)), 195 | asyncio.Task(sleep_coro('Task-C', loop))] 196 | loop.run_until_complete(asyncio.gather(*tasks)) loop.close() 197 | ``` 198 | 199 | 让我们看看下面截图中显示的重构后的代码执行情况。 200 | 201 | ![1](../imgs/7-18.png) 202 | 203 | 我们得到了它!结果是一致的,事件循环没有被`time.sleep`函数的执行阻塞。 204 | -------------------------------------------------------------------------------- /docs/chapter8/总结.md: -------------------------------------------------------------------------------- 1 | # 小结 2 | 3 | 在本章节,我们学习了异步、阻塞、非阻塞编程。为了了解这些行为,我们使用`asyncio`模块的基本机制写了一些例子。 4 | 5 | `asyncio`模块是对`python`异步编程进行革命的一个尝试。Guido Van Rossum 在探索性选择和提取基本机制为这些选择提供清晰的API方面非常成功。`yield from`语法产生是为了增强一些使用协程的程序的表现力,使程序员免去写回调函数的负担。除此之外,`asyncio`模块拥有与其它应用程序集成的能力。 6 | 7 | 快到本书的结束了,写这本书还是很有挑战性的,希望它对你有所帮助。本书中有很多东西没有介绍,比如 `IPython`, `mpi4py`, `Greenlets`, `Eventlets`, 等等。 8 | 9 | 基于本书提供的内容,你可以自己做实验比较不同的工具。几乎在本书的每一个章节都用了两个相关例子来介绍,这也说明了Python可以在不改变核心代码的基础上灵活的替换不同的工具。 10 | 11 | 我们已经学习了一些全局解释器锁 (**GIL**) 和一些绕过 **GIL** 副作用的变通方法。 相信主要的 Python 实现(**CPython**)不会解决与 **GIL** 相关的问题; 只有未来才能揭示这一点。 **GIL** 是 **Python** 社区中一个困难且反复出现的话题。 另一方面,我们有 **PyPy** 实现,它带来了 **JIT** 和其他性能改进。 如今,**PyPy** 团队正在尝试将软件事务内存 (**STM**) 用于 **PyPy**,旨在移除 **GIL**。 12 | -------------------------------------------------------------------------------- /docs/chapter8/理解事件循环.md: -------------------------------------------------------------------------------- 1 | # 理解事件循环 2 | 3 | 为了理解事件循环的概念,我们需要理解构成其内部结构的元素。 4 | 5 | 我们将使用术语**资源描述符**来代指**套接字描述符**和**文件描述符**。 6 | 7 | ## 轮询函数 8 | 9 | 轮询技术由不同的操作系统实现,旨在监视一个或多个**资源描述符**的状态,且轮询技术由系统功能负责实现。 **轮询函数**构成了事件循环的基础,我们经常发现这些模型被称为**准备就绪通知方案(RN - Readiness Notification)**,因为轮询功能通知对事件感兴趣的程序,同时**资源描述符**也已准备好进行交互; 然而,感兴趣的程序可能会或不会完成所需的操作。 10 | 11 | 例如,在 Linux 方面,我们有以下轮询函数: 12 | 13 | - `select()`: *POSIX* 实现存在一些缺点,如下所示: 14 | 15 | - 要监视的**资源描述符**数量有限制。 16 | - 复杂度为 `O(n)`,其中 `n` 表示连接的客户端数量,这使得服务器无法同时处理多个客户端。 17 | 18 | - `poll()`: `select()`的增强版,有以下特点: 19 | 20 | - 允许监视更大范围的资源描述符 21 | - 和`select()`一样的`O(n)`时间复杂度 22 | - 允许更多类型的监控事件 23 | - 和`select()`相比比,可以复用**entry**数据 24 | 25 | - `epoll()`: 这是一个强大的 Linux 实现,具有恒定时间复杂度 `O(1)` 的吸引人的特性。 `epoll()` 函数提供了两种行为来通过 [epoll_wait()][epoll_wait]{target="_blank"} 调用来监视事件。 为了定义这两种行为,让我们想象这样一种场景:**生产者**在套接字(具有关联的**套接字描述符**)中写入数据,而**消费者**等待完成数据读取: 26 | - **水平触发**(Level-triggered):当消费者完成对 `epoll_wait()` 的调用时,它将立即获得该资源描述符的状态并返回给请求的事件,指示执行读取操作的可能性(在我们的例子中)。 因此,水平触发的行为与事件的状态直接相关,而不是事件本身。 27 | - **边缘触发**(Edge-triggered):只有当套接字中的写入事件结束并且数据可用时,对 `epoll_wait()` 的调用才会返回。 因此,在边缘触发的行为中,重点是事件本身已经发生,而不是执行任何事件的可能性。 28 | 29 | !!! info "" 30 | 31 | 在其他平台上,也有可用的轮询功能,例如用于 BSD 和 Mac OS X 的 `kqueue`。 32 | 33 | 轮询函数对于创建具有可以并发方式管理多个操作的单个线程的应用程序很有用。 例如,[Tornado Web 服务器](http://www.tornadoweb.org/en/stable/overview.html){target="_blank"}是使用非阻塞 I/O 编写的,作为轮询功能,它分别支持 `epoll` 和 `kqueue for Linux` 和 `BSD/Mac OS X`。 34 | 35 | 轮询函数工作步骤如下: 36 | 37 | 1. 一个`poller`对象被创建. 38 | 2. 我们可以在`poller`中注册或不注册一个或多个资源描述符。 39 | 3. 轮询函数在创建的`poller`对象中执行。 40 | 41 | !!! info "" 42 | 43 | `Poller`是一个提供使用轮询方法的抽象接口 44 | 45 | ## 使用事件循环 46 | 47 | 我们可以将事件循环定义为简化版的使用轮询函数监视事件的抽象。 在内部,事件循环使用`poller`对象,消除了程序员控制添加、删除和控制事件的任务的责任。 48 | 49 | 事件循环,一般来说,利用回调函数来处理事件的发生; 例如,给定一个资源描述符A,当A中发生写事件时,会有一个回调函数。 下面列举一些用Python实现事件循环的应用示例: 50 | 51 | - Tornado web server ( {target="_blank"} ) 52 | - Twisted ( {target="_blank"} ) 53 | - asyncio ( {target="_blank"} ) 54 | - Gevent ( {target="_blank"} ) 55 | - Eventlet ( {target="_blank"} ) 56 | 57 | [epoll_wait]: http://refspecs.linux-foundation.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/libc-epoll-wait-1.html 58 | -------------------------------------------------------------------------------- /docs/chapter8/理解阻塞非阻塞和异步操作.md: -------------------------------------------------------------------------------- 1 | # 理解阻塞非阻塞和异步操作 2 | 3 | 了解任务执行的不同方法对于建模和构思可扩展的解决方案极为重要。 了解何时使用**异步**、**阻塞**和**非阻塞**操作可以对系统的响应时间产生巨大的影响。 4 | 5 | ## 理解阻塞操作 6 | 7 | 在阻塞操作的情况下,我们用在银行柜台接待客户的情境来举例子。 当呼叫特定客户的号码时,收银员的所有注意力都集中在这个特定的客户身上。 在满足当前特定客户的需求之前,收银员不能同时接待另一个客户。 现在,考虑到这一点,想象一家只有两名收银员的银行机构,当每小时有 100 名顾客涌入时; 然后我们就会面临一个处理流程的问题。 这个例子说明了处理的阻塞操作,当一个任务需要等待另一个任务结束时,阻塞了其对资源的访问。 8 | 9 | !!! info "" 10 | 11 | 在处理阻塞操作时,请求者阻塞结果直到它的请求被完成。 12 | 13 | ## 理解非阻塞操作 14 | 15 | 一般情况下很容易将非阻塞操作与异步操作混淆; 但是,它们是不同的概念,但他们却可以很好地协同工作,并且经常以这种方式使用。 让我们再用一个现实世界的情境来说明这种情况。 回到银行情境中,想象一下,在等待服务的客户中,有一个客户X需要提取一笔收益,但是暂时没有收益。 同时收银员不会拒绝对其他客户的服务,直到可以提取收益为止,而只是向客户 X 发出信号,让他在另一个时间或另一个日期再来。 16 | 17 | !!! info "" 18 | 19 | 非阻塞操作是这样一种操作,它在出现**最小阻塞信号**时返回一个**控制代码**或**异常**,告诉请求者稍后重试。 20 | 21 | ## 理解异步操作 22 | 23 | 回到银行机构的例子,设想每个出职员有10个助手来执行需要较长时间的任务;现在考虑我们的机构有两个职员,每个人有10个助手。随着客户的到来,如果客户X有一个可能无限期阻塞队列的请求,这个请求就会被派给一个助理,这个助理会在后台做这个工作,当客户X的答案准备好了,就会直接找他,这样职员就可以腾出手来处理后面客户的请求,而不必阻塞前面的客户的需求了。 24 | 25 | !!! info "" 26 | 27 | 异步操作通过**回调**、**协程**等机制通知请求结束。 28 | 29 | **回调函数**是在特定条件发生时调用的函数。它通常用于处理异步执行的结果。 30 | -------------------------------------------------------------------------------- /docs/files/Parallel Programming with Python.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/files/Parallel Programming with Python.pdf -------------------------------------------------------------------------------- /docs/imgs/1-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/1-01.png -------------------------------------------------------------------------------- /docs/imgs/1-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/1-02.png -------------------------------------------------------------------------------- /docs/imgs/1-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/1-03.png -------------------------------------------------------------------------------- /docs/imgs/2-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/2-01.png -------------------------------------------------------------------------------- /docs/imgs/2-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/2-02.png -------------------------------------------------------------------------------- /docs/imgs/2-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/2-03.png -------------------------------------------------------------------------------- /docs/imgs/3-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/3-01.png -------------------------------------------------------------------------------- /docs/imgs/3-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/3-02.png -------------------------------------------------------------------------------- /docs/imgs/6-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/6-02.png -------------------------------------------------------------------------------- /docs/imgs/6-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/6-03.png -------------------------------------------------------------------------------- /docs/imgs/6-04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/6-04.png -------------------------------------------------------------------------------- /docs/imgs/7-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-01.png -------------------------------------------------------------------------------- /docs/imgs/7-11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-11.png -------------------------------------------------------------------------------- /docs/imgs/7-12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-12.png -------------------------------------------------------------------------------- /docs/imgs/7-13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-13.png -------------------------------------------------------------------------------- /docs/imgs/7-14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-14.png -------------------------------------------------------------------------------- /docs/imgs/7-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-15.png -------------------------------------------------------------------------------- /docs/imgs/7-16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-16.png -------------------------------------------------------------------------------- /docs/imgs/7-17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-17.png -------------------------------------------------------------------------------- /docs/imgs/7-18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hellowac/parallel-programming-with-python-zh/9a134fab5a284ef306f090889b6482532d92b65d/docs/imgs/7-18.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # parallel programming with python 2 | 3 | 使用 python 进行并行编程 4 | 5 | 使用强大的 python 环境开发高效的并行系统 6 | 7 | -- Jan Palach 8 | 9 | ## 关于作者 10 | 11 | Jan Palach 从事软件开发已有 13 年,曾与科学界合作使用 C++、Java 和 Python 技术的私人公司的可视化和后端。 Jan 拥有巴西里约热内卢 Estácio de Sá 大学的信息系统学位,以及巴拉那州立联邦理工大学的软件开发研究生学位。 12 | 目前,他在一家实施 C++ 系统的电信行业的私营公司担任高级系统分析师; 但是,他喜欢尝试使用 Python 和 Erlang 来获得乐趣——这是他的两大技术爱好。 13 | 他天生好奇,喜欢挑战和学习新技术、结识新朋友以及了解不同的文化。 14 | 15 | ## 致谢 16 | 17 | 我不知道在如此紧迫的期限内写一本书有多难,我生活中发生了很多其他事情。 我不得不把写作融入我的日常生活中, 18 | 照顾我的家人、空手道课程、工作、暗黑破坏神 III 等等。 这项任务并不容易; 然而,考虑到根据我的经验,我已经专注于最重要的事情,我希望我已经产生了高质量的内容来取悦大多数读者。 19 | 20 | 我要致谢的人名单很长,我只需要一本书就可以了。 所以,我要感谢一些我经常联系的人,他们以直接或间接的方式在整个探索过程中帮助了我。 21 | 22 | 我的妻子 Anicieli Valeska de Miranda Pertile,我选择与她分享我的爱并收集牙刷直到生命的尽头,是她让我有时间创作这本书,并没有让我在我想我的时候放弃 做不到。 在我作为一个人的成长过程中,我的家人一直对我很重要,并教会了我善良的道路。 23 | 24 | 我要感谢 Fanthiane Ketrin Wentz,她不仅是我最好的朋友,还指导我学习武术,教会我一生都将秉持的价值观——她是我的榜样。 Lis Marie Martini,亲爱的朋友,为本书提供了封面,她是一位了不起的摄影师和动物爱好者。 25 | 26 | 非常感谢我以前的英语老师、审校者和校对者玛丽娜·梅洛 (Marina Melo),她在本书的写作过程中提供了帮助。 感谢审稿人和我的好友 Vitor Mazzi 和 Bruno Torres,他们对我的职业发展做出了巨大贡献,现在仍然如此。 27 | 28 | 特别感谢 Rodrigo Cacilhas、Bruno Bemfica、Rodrigo Delduca、Luiz Shigunov、Bruno Almeida Santos、Paulo Tesch (corujito)、Luciano Palma、Felipe Cruz 以及我经常与之谈论技术的其他人。 特别感谢 Turma B. 29 | 30 | 非常感谢 Guido Van Rossum 创建了 Python,它将编程变成了令人愉快的事情; 我们需要更多这样的东西,而不是设置/获取。 31 | 32 | ## 关于审稿人 33 | 34 | **Cyrus Dasadia** 在 AOL 和 InMobi 等组织担任 Linux 系统管理员已有十多年。 他目前正在开发 CitoEngine,这是一种完全用 Python 编写的开源警报管理服务。 35 | 36 | **Wei Di** 是 eBay 研究实验室的研究科学家,专注于大型电子商务应用的高级计算机视觉、数据挖掘和信息检索技术。 她的兴趣包括大规模数据挖掘、商品销售中的机器学习、电子商务的数据质量、搜索相关性以及排名和推荐系统。 她在模式识别和图像处理方面也有多年的研究经验。 她于 2011 年在普渡大学获得博士学位,重点研究数据挖掘和图像分类。 37 | 38 | **Michael Galloy** 在 Tech-X Corporation 担任研究数学家,参与使用 IDL 和 Python 进行科学可视化。 在此之前,他曾为 Research Systems, Inc.(现为 Exelis Visual Information Solutions)教授各级 IDL 编程和咨询工作五年。 他是 Modern IDL (modernidl.idldev.com) 的作者,并且是多个开源项目的创建者/维护者,包括 IDLdoc、mgunit、dist_tools 和 cmdline_tools。 他为他的网站 michaelgalloy.com 撰写了 300 多篇关于 IDL、科学可视化和高性能计算的文章。 他是 NASA 的首席研究员,资助了使用 IDL 进行远程数据探索以实现 IDL 中的 DAP 绑定,以及使用现代图形卡加速曲线拟合的快速模型拟合工具套件。 39 | 40 | **Ludovic Gasc** 是欧洲知名的开源 VoIP 和统一通信公司 Eyepea 的高级软件集成工程师。 在过去的五年中,Ludovic 基于 Python(Twisted 和现在的 AsyncIO)和 RabbitMQ 为电信开发了冗余分布式系统。 41 | 42 | 他还是多个 Python 库的贡献者。 有关这方面的更多信息和详细信息,请参阅 。 43 | 44 | **Kamran Husain** 在计算机行业工作了大约 25 年,为电信和石油行业编程、设计和开发软件。 他喜欢在空闲时间涉足漫画。 45 | 46 | **Bruno Torres** 已经工作了十多年,解决了多个领域的各种计算问题,涉及客户端和服务器端应用程序的组合。 Bruno 拥有巴西里约热内卢 Universidade Federal Fluminense 的计算机科学学位。 47 | 48 | 在数据处理、电信系统以及应用程序开发和媒体流方面工作后,他从 Java 和 C++ 数据处理系统开始开发了许多不同的技能,通过解决电信行业的可扩展性问题和使用 Lua 简化大型应用程序定制,到 为移动设备和支持系统开发应用程序。 49 | 50 | 目前,他在一家大型媒体公司工作,开发了许多通过 Internet 为桌面浏览器和移动设备传送视频的解决方案。 51 | 52 | 他热衷于学习不同的技术和语言、结识新朋友,并热爱解决计算问题的挑战。 53 | 54 | ## 前言 55 | 56 | 几个月前,也就是 2013 年,Packt Publishing 的专业人士联系我,让我写一本关于使用 Python 语言进行并行编程的书。 我以前从未想过要写一本书,也不知道即将进行的工作; 构思这项工作会有多复杂,以及在我目前的工作中将它融入我的工作时间表会有什么感觉。 虽然我考虑了几天这个想法,但我最终还是接受了这个任务,并对自己说这将是一次大量的个人学习,也是一个向全世界的观众传播我的 Python 知识的绝好机会,因此 ,希望在我这一生的旅程中留下宝贵的遗产。 57 | 58 | 这项工作的第一部分是概述其主题。 取悦所有人并不容易; 但是,我相信我已经在这本迷你书提出的主题上取得了很好的平衡,我打算在其中介绍结合理论和实践的 Python 并行编程。 我在这项工作中冒了风险。 我使用了一种新的格式来展示如何解决问题,其中示例在第一章中定义,然后使用本书中提供的工具来解决。 我认为这是一种有趣的格式,因为它允许读者分析和质疑 Python 提供的不同模块。 59 | 60 | 所有章节都结合了一些理论,从而构建了上下文,为您提供一些基本知识来理解文本的实际部分。 我真诚地希望这本书对那些探索 Python 并行编程世界的人有用,因为我一直努力专注于高质量的写作。 61 | 62 | ## 本书涵盖的内容 63 | 64 | 第 1 章,上下文化并行、并发和分布式编程,涵盖了并行编程模型的概念、优点、缺点和含义。 此外,本章还公开了一些 Python 库来实现并行解决方案。 65 | 66 | 第 2 章,设计并行算法,介绍了有关设计并行算法的一些技术的讨论。 67 | 68 | 第 3 章,识别可并行化问题,介绍了一些问题示例,并分析了这些问题是否可以拆分为并行部分。 69 | 70 | 第 4 章,使用 threading 和 concurrent.futures 模块,解释了如何使用 threading 和 concurrent.futures 模块实现第 3 章中提出的每个问题,识别可并行化的问题。 71 | 72 | 第 5 章,使用 Multiprocessing 和 ProcessPoolExecutor,介绍如何使用 multiprocessing 和 ProcessPoolExecutor 实现第 3 章中提出的每个问题,识别可并行化的问题。 73 | 74 | 第 6 章,使用并行 Python,介绍如何使用并行 Python 模块实现第 3 章中提出的每个问题,识别可并行化的问题。 75 | 76 | 第 7 章,使用 Celery 分配任务,解释了如何使用 Celery 分布式任务队列实现第 3 章中提出的每个问题,识别可并行化的问题。 77 | 78 | 第 8 章,异步执行,解释了如何使用 asyncio 模块和有关异步编程的概念。 79 | 80 | ## 这本书需要你掌握什么 81 | 82 | Python 编程的先前知识是必要的,因为 Python 教程不会包含在本书中。 欢迎了解并发和并行编程,因为本书是为刚开始从事此类软件开发的开发人员设计的。 关于软件,有必要获得以下内容: 83 | 84 | - 第 8 章“异步处理”需要 Python 3.3 和 Python 3.4(仍在开发中) 85 | - 需要读者选择的任何代码编辑器 86 | - 应安装并行 Python 模块 1.6.4 87 | - 第 5 章使用 Multiprocessing 和 ProcessPoolExecutor 需要 Celery 框架 3.1 88 | - 需要读者选择的任何操作系统 89 | 90 | ## 这本书是给谁的 91 | 92 | 本书是关于使用 Python 进行并行编程的紧凑讨论。 它为初级和中级 Python 开发人员提供工具。 本书适合那些愿意全面了解使用 Python 开发并行/并发软件并学习不同 Python 替代方案的人。 到本书结束时,您将使用各章中提供的信息扩大您的工具箱。 93 | 94 | ## 惯例 95 | 96 | 在本书中,您会发现许多区分不同类型信息的文本风格。 以下是这些样式的一些示例,以及对它们含义的解释。 97 | 98 | 文中代码如下:“为了举例说明multiprocessing.Pipe对象的使用,我们将实现一个创建两个进程A和B的Python程序。” 99 | 100 | 这段代码如下: 101 | 102 | ```python 103 | def producer_task(conn): 104 | value = random.randint(1, 10) 105 | conn.send(value) 106 | print('Value [%d] sent by PID [%d]' % (value, os.getpid())) 107 | conn.close() 108 | ``` 109 | 110 | 任何命令行输入或输出都写成如下: 111 | 112 | **`$celery –A tasks –Q sqrt_queue,fibo_queue,webcrawler_queue worker --loglevel=info`** 113 | 114 | ## 英文原文 115 | 116 | 参考: [Parallel Programming with Python](./files/Parallel%20Programming%20with%20Python.pdf){target="_blank"} 117 | -------------------------------------------------------------------------------- /mkdocs.yaml: -------------------------------------------------------------------------------- 1 | site_name: python并发编程-中文版 2 | repo_url: https://github.com/hellowac/parallel-programming-with-python-zh/tree/docs 3 | repo_name: hellowac/parallel-programming-with-python-zh 4 | 5 | # 配置主题,在mkdocs.yml文件下 6 | theme: 7 | name: material 8 | language: zh 9 | palette: # 文档颜色, https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ 10 | scheme: default # 配色方案 11 | primary: default # 主色 12 | accent: red # 强调色 13 | features: 14 | # - navigation.tabs # 顶部导航 15 | # - navigation.sections # 全部展开(非折叠状态) 16 | # - navigation.expand # 子目录展开 17 | - navigation.indexes # 带章节索引页 18 | - navigation.top # 返回顶部按钮 19 | - toc.follow 20 | 21 | # 插件 22 | plugins: 23 | - glightbox 24 | - search: 25 | separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' 26 | - git-revision-date-localized: # 支持文档创建时间显示, https://github.com/timvink/mkdocs-git-revision-date-localized-plugin 27 | locale: zh 28 | enable_creation_date: true 29 | type: date 30 | 31 | # markdown解析扩展 32 | markdown_extensions: 33 | - tables 34 | - admonition 35 | - attr_list 36 | - md_in_html # https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/#footnotes 37 | # pymdownx 扩展,参考:https://facelessuser.github.io/pymdown-extensions/ 38 | - pymdownx.inlinehilite # 单行高亮, 参考: https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#highlighting-specific-lines 39 | - pymdownx.critic # 支持部分字段格式化,参考:https://squidfunk.github.io/mkdocs-material/reference/formatting/ 40 | - pymdownx.highlight # 支持代码块高亮显示 41 | - pymdownx.snippets 42 | - pymdownx.details 43 | - pymdownx.superfences: # 注释 : 44 | # preserve_tabs: true 45 | custom_fences: 46 | - name: mermaid 47 | class: mermaid 48 | format: !!python/name:pymdownx.superfences.fence_code_format 49 | - pymdownx.caret # 下划线, 上标 : https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/?h=caret#caret-mark-tilde 50 | # https://facelessuser.github.io/pymdown-extensions/extensions/caret/ 51 | - pymdownx.mark # 标记 :https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/?h=caret#caret-mark-tilde 52 | # https://facelessuser.github.io/pymdown-extensions/extensions/mark/ 53 | - pymdownx.tilde # 删除线, 下标 参考: https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/?h=caret#caret-mark-tilde 54 | # https://facelessuser.github.io/pymdown-extensions/extensions/tilde/ 55 | - pymdownx.arithmatex: 56 | generic: true 57 | 58 | # 扩展支持, 支持数学符号 59 | extra_javascript: 60 | - javascripts/mathjax.js 61 | - https://polyfill.io/v3/polyfill.min.js?features=es6 62 | - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js 63 | - https://unpkg.com/mermaid@9.4.0/dist/mermaid.min.js 64 | # - https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs 65 | 66 | # 页面导航 67 | nav: 68 | - python并发编程: index.md 69 | - 第一章 并行、并发以及分布式编程的对比分析: 70 | - chapter1/index.md 71 | - 为什么使用并行编程: chapter1/why_use_parallel_programming.md 72 | - 探索并行化的几种方式: chapter1/exploring_common_forms_of_parallelization.md 73 | - 并行编程间的通信: chapter1/communicating_in_parallel_programming.md 74 | - 识别并行编程的问题: chapter1/identifying_parallel_programming_problems.md 75 | - 发现Python并行编程的工具: chapter1/discovering_Pythons_parallel_programming_tools.md 76 | - 小心PythonGIL: chapter1/taking_care_of_GIL.md 77 | - 小结: chapter1/summary.md 78 | - 第二章 设计并行算法: 79 | - chapter2/index.md 80 | - 分治技术: chapter2/分治技术.md 81 | - 使用数据分解: chapter2/使用数据分解.md 82 | - 用管道分解任务: chapter2/用管道分解任务.md 83 | - 处理和映射: chapter2/处理和映射.md 84 | - 小结: chapter2/总结.md 85 | - 第三章 设计并行算法: 86 | - chapter3/index.md 87 | - 从多个输入中得到斐波那契最大的值: chapter3/从多个输入中得到斐波那契最大的值.md 88 | - 爬取网页: chapter3/爬取网页.md 89 | - 小结: chapter3/总结.md 90 | - 第四章 使用threading和concurrent.futures模块: 91 | - chapter4/index.md 92 | - 什么是线程: chapter4/defining_threads.md 93 | - 使用threading模块来为多个输入同时计算Fibonacci序列: chapter4/using_threading_to_obtain_the_Fibonacci_series_term_with_multiple_inputs.md 94 | - 使用concurrent.futures模块爬取web信息: chapter4/crawling_the_web_using_the_concurrent_futures_module.md 95 | - 小结: chapter4/总结.md 96 | - 第五章 使用多进程和进程池: 97 | - chapter5/index.md 98 | - 理解进程的概念: chapter5/understanding_the_concept_of_a_process.md 99 | - 理解多进程通信: chapter5/implementing_multiprocessing_communication.md 100 | - 使用多进程解决斐波那契数列多输入问题: chapter5/using_multiprocessing_to_compute_fibonacci_series_terms_with_multiple_inputs.md 101 | - 使用ProcessPoolExecutor模块设计网络爬虫: chapter5/crawling_the_web_using_processPoolExecutor.md 102 | - 小结: chapter5/总结.md 103 | - 第六章 使用并行 Python: 104 | - chapter6/index.md 105 | - 理解进程间通信: chapter6/understanding_interprocess_communication.md 106 | - 了解Parallel Python(PP): chapter6/discovering_pp.md 107 | - 在SMP架构上使用PP计算斐波那契序列: chapter6/using_pp_to_calculate_the_fibonacci_series_term_on_smp_architecture.md 108 | - 使用PP创建分布式的网络爬虫: chapter6/using_pp_to_make_a_distributed_web_crawler.md 109 | - 小结: chapter6/总结.md 110 | - 第七章 使用Celery分发任务: 111 | - chapter7/index.md 112 | - 理解 Celery: chapter7/understanding_celery.md 113 | - 理解 Celery 的架构: chapter7/understanding_celery_architecture.md 114 | - 搭建环境: chapter7/setting_up_the_environment.md 115 | - 分派一个简单的任务: chapter7/dispatching_a_simple_task.md 116 | - 使用 Celery 获取斐波那契数列项: chapter7/using_celery_to_obtain_a_fibonacci_series_term.md 117 | - 根据任务类型定义队列: chapter7/defining_queues_by_task_types.md 118 | - 使用 Celery 制作分布式网络爬虫: chapter7/using_celery_to_make_a_distributed_web_crawler.md 119 | - 小结: chapter7/总结.md 120 | - 第八章 异步编程: 121 | - chapter8/index.md 122 | - 理解阻塞非阻塞和异步操作: chapter8/理解阻塞非阻塞和异步操作.md 123 | - 理解事件循环: chapter8/理解事件循环.md 124 | - 使用asyncio: chapter8/使用asyncio.md 125 | - 小结: chapter8/总结.md --------------------------------------------------------------------------------