diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..010a5fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +temp.py +.DS_Store \ No newline at end of file diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..c746f50 --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,11 @@ +{ + "MD007": false, + "MD046": false, + "MD013": false, + "MD024": false, + "MD051": false, + "MD036": false, + "MD029": false, + "MD033": false, + "MD049": false +} \ No newline at end of file diff --git a/README.md b/README.md index 790f229..2ed9d57 100644 --- a/README.md +++ b/README.md @@ -1,119 +1,13 @@ -# Parallel-Programming-with-Python -Parallel Programming with Python,a book about parallel in python,We will translate it into Chinese. +# 使用python并发编程 -话说在ICT实习的时期,因感兴趣于Python,于是和两位朋友 [@tanghaodong25](https://github.com/tanghaodong25) 以及 [@gorlf](https://github.com/gorlf) 商量翻译一本Python相关的书籍,挑来选去感觉Parallel Programming with Python比较赞,于是拍板翻译之,另外感谢[@lujun9972](https://github.com/lujun9972)的加入,由于平时开发比较忙加上比较懒,耗时几个月才完成~ +这是我在学习python的路上,在github上淘到的一本电子书,虽然出版时间有点久了,但知识是无价的,并且较新的技术或理念也是在原有知识基础上发展起来的。故放在这,为了尊重原翻译者,固新建了一个分支`docs`,如有访问原翻译的需求,请访问[master](https://github.com/hellowac/parallel-programming-with-python-zh/tree/master)分支。 -## Contributors +在参考原文的基础上,完善了原仓库中部分对python编程术语的描述,以及补充了部分图表截图等等,主要如下: -* [@尐鱼](https://github.com/Voidly) -* [@tanghaodong25](https://github.com/tanghaodong25) -* [@gorlf](https://github.com/gorlf) -* [@lujun9972](https://github.com/lujun9972) +1. 完善了部分章节的翻译描述。 +2. 补充了部分章节的截图。 +3. 使用python的mkdocs工具文档化了电子书,并且利用github pages 发布出来方便复习和分享。 -另外因水平有限,翻译中难免有所疏漏与瑕疵,欢迎指出,欢迎吐槽~若要进行转载请注明作者,毕竟翻译也是很花费时间与精力的不是,所以请尊重我们的劳动成果~ - -## [第一章:并行、并发分布式编程对比分析](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E5%B9%B6%E8%A1%8C%E3%80%81%E5%B9%B6%E5%8F%91%E4%B8%8E%E5%88%86%E5%B8%83%E5%BC%8F%E7%BC%96%E7%A8%8B%E7%9A%84%E5%AF%B9%E6%AF%94%E5%88%86%E6%9E%90.md) - -* #### [为什么使用并行编程](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E4%B8%BA%E4%BB%80%E4%B9%88%E4%BD%BF%E7%94%A8%E5%B9%B6%E8%A1%8C%E7%BC%96%E7%A8%8B.md) - -* #### [探索并行化的几种模式](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E6%8E%A2%E7%B4%A2%E5%B9%B6%E8%A1%8C%E5%8C%96%E7%9A%84%E5%87%A0%E7%A7%8D%E6%A8%A1%E5%BC%8F.md) - -* #### [并行编程间的通信](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E5%9C%A8%E5%B9%B6%E8%A1%8C%E7%BC%96%E7%A8%8B%E9%80%9A%E4%BF%A1.md) - -* #### [识别并行编程的问题](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E8%AF%86%E5%88%AB%E5%B9%B6%E8%A1%8C%E7%BC%96%E7%A8%8B%E7%9A%84%E9%97%AE%E9%A2%98.md) - -* #### [发现Python并行编程的工具](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E5%8F%91%E7%8E%B0Python%E5%B9%B6%E8%A1%8C%E7%BC%96%E7%A8%8B%E7%9A%84%E5%B7%A5%E5%85%B7.md) - -* #### [小心Python GIL](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E5%B0%8F%E5%BF%83Python%20GIL.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%80%E7%AB%A0/%E6%80%BB%E7%BB%93.md) - - -## [第二章:设计并行算法](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%8C%E7%AB%A0/%E8%AE%BE%E8%AE%A1%E5%B9%B6%E8%A1%8C%E7%AE%97%E6%B3%95.md) - -* #### [分治技术](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%8C%E7%AB%A0/%E5%88%86%E6%B2%BB%E6%8A%80%E6%9C%AF.md) - -* #### [使用数据分解](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%8C%E7%AB%A0/%E4%BD%BF%E7%94%A8%E6%95%B0%E6%8D%AE%E5%88%86%E8%A7%A3.md) - -* #### [用管道分解任务](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%8C%E7%AB%A0/%E7%94%A8%E7%AE%A1%E9%81%93%E5%88%86%E8%A7%A3%E4%BB%BB%E5%8A%A1.md) - -* #### [处理和映射](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%8C%E7%AB%A0/%E5%A4%84%E7%90%86%E5%92%8C%E6%98%A0%E5%B0%84.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%8C%E7%AB%A0/%E6%80%BB%E7%BB%93.md) - - -## [第三章:识别一个可并行的问题](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%89%E7%AB%A0/%E8%AF%86%E5%88%AB%E4%B8%80%E4%B8%AA%E5%8F%AF%E5%B9%B6%E8%A1%8C%E7%9A%84%E9%97%AE%E9%A2%98.md) - -* #### [从多个输入中得到斐波那契最大的值](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%89%E7%AB%A0/%E4%BB%8E%E5%A4%9A%E4%B8%AA%E8%BE%93%E5%85%A5%E4%B8%AD%E5%BE%97%E5%88%B0%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%9C%80%E5%A4%A7%E7%9A%84%E5%80%BC.md) - -* #### [爬取网页](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%89%E7%AB%A0/%E7%88%AC%E5%8F%96%E7%BD%91%E9%A1%B5.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%89%E7%AB%A0/%E6%80%BB%E7%BB%93.md) - - -## [第四章:使用threading和concurrent.futures模块](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%9B%9B%E7%AB%A0/ReadMe.md) - -* #### [定义threading模块](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%9B%9B%E7%AB%A0/%E5%AE%9A%E4%B9%89threading%E6%A8%A1%E5%9D%97.md) - -* #### [使用多线程解决斐波那契序列多输入问题](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%9B%9B%E7%AB%A0/%E4%BD%BF%E7%94%A8threading%E6%A8%A1%E5%9D%97%E8%A7%A3%E5%86%B3%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97%E5%A4%9A%E8%BE%93%E5%85%A5%E9%97%AE%E9%A2%98.md) - -* #### [使用网络爬虫实现python并发模块](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%9B%9B%E7%AB%A0/%E4%BD%BF%E7%94%A8%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB%E5%AE%9E%E7%8E%B0python%E5%B9%B6%E5%8F%91%E6%A8%A1%E5%9D%97.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%9B%9B%E7%AB%A0/%E6%80%BB%E7%BB%93.md) - - -## [第五章:使用多进程和进程池](https://github.com/Voidly/Parallel-Programming-with-Python/tree/master/%E7%AC%AC%E4%BA%94%E7%AB%A0) - -* #### [理解进程的定义](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%94%E7%AB%A0/%E7%90%86%E8%A7%A3%E8%BF%9B%E7%A8%8B%E7%9A%84%E5%AE%9A%E4%B9%89.md) - -* #### [实现多进程间通信](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%94%E7%AB%A0/%E5%AE%9E%E7%8E%B0%E5%A4%9A%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1.md) - -* #### [使用多进程解决斐波那契序列多输入问题](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%94%E7%AB%A0/%E4%BD%BF%E7%94%A8%E5%A4%9A%E8%BF%9B%E7%A8%8B%E8%A7%A3%E5%86%B3%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97%E5%A4%9A%E8%BE%93%E5%85%A5%E9%97%AE%E9%A2%98.md) - -* #### [使用进程池实现网络爬虫](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%94%E7%AB%A0/%E4%BD%BF%E7%94%A8%E8%BF%9B%E7%A8%8B%E6%B1%A0%E5%AE%9E%E7%8E%B0%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%BA%94%E7%AB%A0/%E6%80%BB%E7%BB%93.md) - - -## [第六章:利用并行Python](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AD%E7%AB%A0/ReadMe.md) - -* #### [理解进程间通信](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AD%E7%AB%A0/%E7%90%86%E8%A7%A3%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1.md) - - -* #### [发现pp模块](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AD%E7%AB%A0/%E5%8F%91%E7%8E%B0pp%E6%A8%A1%E5%9D%97.md) - -* #### [在SMP架构上使用pp模块计算斐波那契序列](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AD%E7%AB%A0/%E5%9C%A8SMP%E6%9E%B6%E6%9E%84%E4%B8%8A%E4%BD%BF%E7%94%A8pp%E6%A8%A1%E5%9D%97%E8%AE%A1%E7%AE%97%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E5%BA%8F%E5%88%97.md) - -* #### [使用pp模块创建分布式网络爬虫](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AD%E7%AB%A0/%E4%BD%BF%E7%94%A8pp%E6%A8%A1%E5%9D%97%E5%88%9B%E5%BB%BA%E5%88%86%E5%B8%83%E5%BC%8F%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AD%E7%AB%A0/%E6%80%BB%E7%BB%93.md) - - -## [第七章:用Celery分发任务](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E7%94%A8Celery%E6%9D%A5%E5%88%86%E5%8F%91%E4%BB%BB%E5%8A%A1.md) - -* #### [理解Celery](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E7%90%86%E8%A7%A3Celery.md) - -* #### [理解Celery架构](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E7%90%86%E8%A7%A3Celery%E6%9E%B6%E6%9E%84.md) - -* #### [建立环境](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E5%BB%BA%E7%AB%8B%E7%8E%AF%E5%A2%83.md) - -* #### [分发简单任务](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E5%88%86%E5%8F%91%E7%AE%80%E5%8D%95%E4%BB%BB%E5%8A%A1.md) - -* #### [用Celery来获得斐波那契数列的项](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E7%94%A8Celery%E6%9D%A5%E8%8E%B7%E5%BE%97%E6%96%90%E6%B3%A2%E9%82%A3%E5%A5%91%E6%95%B0%E5%88%97%E7%9A%84%E9%A1%B9.md) - -* #### [根据任务类型定义队列](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E6%A0%B9%E6%8D%AE%E4%BB%BB%E5%8A%A1%E7%B1%BB%E5%9E%8B%E5%AE%9A%E4%B9%89%E9%98%9F%E5%88%97.md) - -* #### [用Celery来构建一个分布式网络爬虫系统](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E7%94%A8Celery%E6%9D%A5%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%88%86%E5%B8%83%E5%BC%8F%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB%E7%B3%BB%E7%BB%9F.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E4%B8%83%E7%AB%A0/%E6%80%BB%E7%BB%93.md) - -## [第八章:异步做的事](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AB%E7%AB%A0/%E5%BC%82%E6%AD%A5%E7%9A%84%E5%81%9A%E4%BA%8B.md) - -* #### [理解阻塞非阻塞和异步操作](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AB%E7%AB%A0/%E7%90%86%E8%A7%A3%E9%98%BB%E5%A1%9E%E9%9D%9E%E9%98%BB%E5%A1%9E%E5%92%8C%E5%BC%82%E6%AD%A5%E6%93%8D%E4%BD%9C.md) - -* #### [理解事件循环](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AB%E7%AB%A0/%E7%90%86%E8%A7%A3%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF.md) - -* #### [使用asyncio](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AB%E7%AB%A0/%E4%BD%BF%E7%94%A8asyncio.md) - -* #### [总结](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AB%E7%AB%A0/%E6%80%BB%E7%BB%93.md) +## 在线阅读 +阅读: [python并发编程-zh](https://hellowac.github.io/parallel-programming-with-python-zh/) diff --git a/docs/chapter1/communicating_in_parallel_programming.md b/docs/chapter1/communicating_in_parallel_programming.md new file mode 100644 index 0000000..6af057b --- /dev/null +++ b/docs/chapter1/communicating_in_parallel_programming.md @@ -0,0 +1,24 @@ +# 在并行编程通信 + +在并行编程中,被派去执行任务的**wokers**通常需要建立沟通,以便合作解决问题。 在大多数情况下,这种通信的建立方式是可以在**worker**之间交换数据。 在**并行编程**方面,有两种通信形式更广为人知:**共享状态**和**消息传递**。 在以下各节中,将对两者进行简要说明。 + +## 理解共享状态 + +**worker**之间最著名的一种交流方式是**共享状态**。 **共享状态**似乎简单易用,但有很多缺陷,因为其中一个进程对**共享资源**进行的无效操作会影响所有其他进程,从而产生不良结果。 由于显而易见的原因,这也使得程序无法在多台机器之间分发。 + +为了说明这一点,我们将使用一个真实的案例。 假设你是某家银行的客户,而这家银行只有一名出纳员。 当你去银行时,你必须排队等待机会。 进入队列后,您会注意到一次只有一位顾客可以使用收银员,收银员不可能同时接待两位顾客而不会出错。 计算提供了以受控方式访问数据的方法,并且有多种技术,例如**互斥锁**(mutex)。 + +**互斥锁**(Mutex)可以理解为一个特殊的过程变量,**表示访问数据的可用性级别**。 也就是说,在我们现实生活中的例子中,客户有一个号码,在特定的时刻,这个号码会被激活,收银员将专门为这个客户服务。 在流程结束时,这位顾客将为下一位顾客腾出收银员,依此类推。 + +!!! info "" + + 在某些情况下,程序运行时数据在变量中具有常量值,并且共享数据仅用于读取目的。 因此,访问控制不是必需的,因为它永远不会出现完整性问题。 + +## 理解信息传递 + +当我们旨在避免源自共享状态的数据访问控制和同步问题时使用**消息传递**(Message passing)。 **消息传递**由运行进程中的消息交换机制组成。 每当我们开发具有分布式体系结构的程序时,它都非常常用,其中放置它们的网络中的**消息交换**是必要的。 例如,Erlang 等语言使用此模型在其并行架构中实现通信。 一旦在每次消息交换时都复制了数据,就不可能出现访问并发方面的问题。 尽管内存使用率似乎高于共享内存状态,但使用此模型也有优势。 它们如下: + +* 没有数据访问并发. +* 消息可以在本地(各种进程)或分布式环境中交换. +* 这使得可伸缩性问题发生的可能性降低,并实现了不同系统的互操作性. +* 总的来说,按照程序员的方式维护起来很容易. \ No newline at end of file diff --git a/docs/chapter1/discovering_Pythons_parallel_programming_tools.md b/docs/chapter1/discovering_Pythons_parallel_programming_tools.md new file mode 100644 index 0000000..5aff391 --- /dev/null +++ b/docs/chapter1/discovering_Pythons_parallel_programming_tools.md @@ -0,0 +1,27 @@ +# 发现Python并行编程的工具 + +由 Guido Van Rossum 创建的 Python 语言是一种多范式、多用途的语言。 由于其强大的简单性和易于维护,它已在世界范围内被广泛接受。 它也被称为包含电池的语言。 模块种类繁多,使用起来更顺畅。 在并行编程中,Python 具有简化实现的内置和外部模块。 本书是基于Python3.X的。 + +## Python的threading模块 + +Python的**threading**模块为模块 **_thread** 提供了一个抽象层,它是一个较低级别的模块。 它提供的功能可以帮助程序员完成基于线程开发并行系统的艰巨任务。 **threading**模块的官方文档可以在{target="_blank"}中找到。 + +## Python的mutliprocess模块 + +**multiprocessing** 模块旨在为使用基于进程的并行性提供一个简单的 API。 该模块类似于线程模块,它简化了进程之间的交替,没有太大的困难。基于进程的方法在 Python 用户社区中非常流行,因为它是回答有关使用 CPU 绑定线程和 Python 中存在的 GIL 的问题的替代方法。 **multiprocessing**模块的官方文档可以在以下位置找到: + +## Python的parallel模块 + +**parallel Python** 是外部模块,它提供了丰富的 API,这些API利用进程的方法创建并行和分布式系统。该模块是轻量级并且易安装的,并可与其他 Python 程序集成。 可以在 找到 **parallel Python** 模块。 在所有功能中,我们可能会强调以下内容: + +* 自动检测最佳配置 +* 在运行时可以更改许多工作进程的状态 +* 动态的负载均衡 +* 容错性 +* 自动发现计算资源 + +## Celery分布式任务队列 + +**Celery** 是一个出色的 Python 模块,用于创建分布式系统并具有出色的文档。 它在并发形式上使用了至少三种不同类型的方法来执行任务——multiprocessing、Eventlet 和 Gevent。 然而,这项工作将集中精力于多处理方法的使用。 而且,只需要通过配置就能实现进程间的互相通信,它将作为一项课题研究,以便读者能够与他/她自己的实验进行比较。 + +Celery模块可以在官方的项目页面{target="_blank"}得到。 diff --git a/docs/chapter1/exploring_common_forms_of_parallelization.md b/docs/chapter1/exploring_common_forms_of_parallelization.md new file mode 100644 index 0000000..783f57c --- /dev/null +++ b/docs/chapter1/exploring_common_forms_of_parallelization.md @@ -0,0 +1,43 @@ +# 探索并行化的几种模式 + +当我们试图定义**并行系统**的主要方案时,有疑惑很正常。常常会有人提到**并发系统**和**并行系统**,这两个术语看起来像是讲的同一件事。然而实际上有着轻微的差异。 + +## 并发编程 + +在**并发编程**中,我们有一个场景,一个程序派发了几个 workers,这些工人争着使用 CPU 来运行一个任务。 在争执发生的阶段由 CPU 调度器控制,其功能是定义在特定时刻哪个 worker 适合使用资源。 在大多数情况下,**CPU** 调度程序运行进程的排序任务非常快,以至于我们可能会产生伪并行的印象。 因此,**并发编程**是**并行编程**的抽象。 + +!!! info "" + + 并行系统争夺同一CPU来运行任务 + +下图显示了一个**并发编程**方案: + +![1](../imgs/1-01.png) + +## 并行编程 + +**并行编程**可以被定义为一种方案,在这种方案中,程序数据创建workers以在多核环境中同时运行特定任务,而无需在他们之间并发访问 CPU。 + +!!! info "" + + 并行系统同时运行任务。 + +下面的图显示了**并行编程**的概念: + +![1](../imgs/1-02.png) + +## 分布式编程 + +**分布式编程**旨在通过在物理上分离的计算机(节点)之间交换数据来共享处理的可能性。 + +由于多种原因,**分布式编程**变得越来越流行; 他们的探索如下: + +* **容错性**(Fault-tolerance):由于系统是分散的,我们可以将处理分配给网络中的不同机器,从而在不影响整个系统功能的情况下对特定机器进行单独维护。 +* **横向扩展**(Horizontal scalability):一般来说,我们可以增加分布式系统的处理能力。 我们可以连接新设备而无需中止正在执行的应用程序。 可以说,与垂直可扩展性相比,它更便宜、更简单。 +* **云计算**:随着硬件成本的降低,我们需要这类业务的增长,我们可以获得以共同合作方式运行的大型机器集群,并以对用户透明的方式运行程序。 + +> 分布式系统在物理上分离的节点中运行任务。 + +下图显示了一个分布式系统的方案: + +![1](../imgs/1-03.png) diff --git a/docs/chapter1/identifying_parallel_programming_problems.md b/docs/chapter1/identifying_parallel_programming_problems.md new file mode 100644 index 0000000..7858a7c --- /dev/null +++ b/docs/chapter1/identifying_parallel_programming_problems.md @@ -0,0 +1,53 @@ +# 识别并行编程的问题 + +勇敢的键盘战士在并行编程幽灵居住的土地上作战时可能会遇到一些经典问题。 当没有经验的程序员使用结合了共享状态的 worker 时,许多这些问题会更频繁地发生。 其中一些问题将在以下各节中进行描述。 + +## 死锁 + +**死锁**(Deadlock)是指两个或多个 worker 无限期地等待资源释放的情况,由于某种原因,该资源被同一组的 worker 阻塞。 为了更好地理解,我们将使用另一个真实案例。 想象一下入口处有一扇旋转门的银行。 客户 A 转向一侧,这将允许他进入银行,而客户 B 试图通过这个旋转门的入口侧离开银行,这样两个客户都会被困在推门处,但无处可去。 这种情况在现实生活中会很滑稽,但在编程中会很悲惨。 + +!!! info "" + + **死锁**(Deadlock)是一种现象,其中进程等待释放任务条件的发生,但这种情况永远不会发生 + +## 饥饿 + +这个问题是由于一个或者多个进程不公平的竞争所引起的副作用,这会花费更多的时间来执行任务。想象有一组进程,A进程正在执行繁重的任务,而且这个任务还有数据处理优先级。现在,想象一下,高优先级的进程A持续不断的占用CPU,而低优先级的进程B将永远没有机会。因此可以说进程B在CPU周期中是饥饿的。 + +> **饥饿**(Starvation)是由于进程排名策略调整不当造成的。 + +## 竞态条件 + +当一个进程的结果取决于执行结果的顺序,而这个顺序由于缺乏同步机制而被打破时,我们就会面临竞态条件。 它们是由在大型系统中极难过滤的问题引起的。 例如,一对夫妇有一个联名账户; 操作前的初始余额为 100 美元。 下表显示了常规情况,其中有保护机制和预期事实的顺序,以及结果: + +!!! info "常规操作而不会出现静态条件" + + | 丈夫 | 妻子 | 账户余额(美元) | + | -------- | -------- | ---------------- | + | | | 100 | + | 读取余额 | | 100 | + | 存款20 | | 100 | + | 结束操作 | | 120 | + | | 读取余额 | 120 | + | | 取款10 | 120 | + | | 结束操作 | 110 | + +在下表中,有问题的场景出现了。假设账户没有同步机制,并且操作的顺序也和预期不一样。 + +!!! info "类比在联合账户和竞争条件下平衡问题" + + | 丈夫 | 妻子 | 账户余额(美元) | + | --------------------- | --------------------- | ---------------- | + | | | 100 | + | 读取余额 | | 100 | + | 取款100 | | 100 | + | | 读取余额 | 100 | + | | 取款10 | 100 | + | 结束操作
更新余额 | | 0 | + | | 结束操作
更新余额 | 90 | + +由于在操作的顺序中意外的缺乏同步,最终结果存在明显的不一致。并行编程的特征之一是不确定性。无法预见两个 worker 将在什么时候运行,甚至谁先运行。 因此,同步机制必不可少。 + +!!! info "" + + **不确定性**(Non-determinism),如果与缺乏同步机制相结合,可能会导致竞争条件问题 diff --git a/docs/chapter1/index.md b/docs/chapter1/index.md new file mode 100644 index 0000000..be51976 --- /dev/null +++ b/docs/chapter1/index.md @@ -0,0 +1,24 @@ +# 并行、并发以及分布式编程的对比分析 + +并行编程可以被定义为一种模型,这个模型旨在创造一种能与**被准备用于同时执行代码指令的环境相兼容**的程序。并行技术被用于软件开发还不是很长。几年前,处理器在其他组件中只有一个**算术逻辑单元** (**ALU**),它在一个时间空间内一次只能执行一条指令。 多年来,只考虑了一个以**赫兹**(Hz)为单位的时钟,以确定处理器在给定时间间隔内可以处理的指令数。 时钟数量越多,就 **KHz**(每秒千次操作)、**MHz**(每秒百万次操作)和当前的 **GHz**(每秒十亿次操作)而言,可能执行的指令越多。 + +总而言之,每个周期提供给处理器的指令越多,执行速度就越快。 在80 年代,*Intel 80386* 出现了革命性的处理器,它允许以先发制人的方式执行任务,也就是说,可以定期中断一个程序的执行,为另一个程序提供处理器时间; 这意味着基于*时间分片*(time-slicing)的**伪并行**(pseudo-parallelism)。 + +在 80 年代后期,*Intel 80486* 实现了流水线系统(pipelining system),实际上将执行阶段划分为不同的子阶段。 实际上,在处理器的一个周期中,我们可以在每个子阶段同时执行不同的指令。 + +上一节中提到的所有进步都导致了性能的多项改进,但这还不够,因为我们面临着一个微妙的问题,最终会成为所谓的**摩尔定律** ({target="_blank"}). + +探寻满足时钟高能耗的过程最终与物理限制发生冲突; 处理器会消耗更多的能量,从而产生更多的热量。 此外,还有另一个同样重要的问题:便携式计算机市场在 20 世纪 90 年代加速发展。 因此,拥有能够使这些设备的电池在远离插头的地方持续足够长的时间的处理器是极其重要的。 来自不同制造商的多种技术和处理器系列诞生了。 在服务器和大型机方面,Intel 值得一提的是其 Core(R 产品系列,即使只有一个物理芯片,它也可以通过模拟多个处理器的存在来欺骗操作系统。 + +在 Core(R)系列中,处理器进行了重大的内部更改,并采用了称为核心(core)的组件,这些组件具有自己的 **ALU** 和缓存 **L2** 和 **L3**,以及执行指令的其他元素。 这些核心,也称为**逻辑处理器**(logical processors),允许我们同时并行执行同一程序的不同部分,甚至不同的程序。 *age core* 通过优于其前身的功率处理实现了更低的能源使用。 由于内核并行工作,模拟独立的处理器,我们可以拥有一个多核芯片和一个较低的时钟,从而根据任务获得比具有更高时钟的单核芯片更好的性能。 + +当然,如此多的改进已经改变了我们进行软件设计的方式。 今天,我们必须考虑并行性来设计合理利用资源而不浪费资源的系统,从而为用户提供更好的体验并节省个人计算机和处理中心的资源。 **并行编程**比以往任何时候都更多地出现在开发人员的日常生活中,而且显然,它永远不会倒退。 + +本章节包含以下几个主题: + +* 为什么使用并行编程? +* 介绍并行化的常见形式 +* 在并行编程中通信 +* 识别并行编程的问题 +* 发现Python的并行编程工具 +* 小心Python的**全局解释器锁**(Global Interpreter Lock - GIL) diff --git "a/\347\254\254\344\270\200\347\253\240/\346\200\273\347\273\223.md" b/docs/chapter1/summary.md similarity index 96% rename from "\347\254\254\344\270\200\347\253\240/\346\200\273\347\273\223.md" rename to docs/chapter1/summary.md index 6e678de..19a7ddd 100644 --- "a/\347\254\254\344\270\200\347\253\240/\346\200\273\347\273\223.md" +++ b/docs/chapter1/summary.md @@ -1,5 +1,5 @@ -##总结 +# 总结 在这一章节,我们学了一些并行编程的概念,并学习了一些模型的优点以及缺点。在谈到并行性的时候对于一些问题或者潜在问题进行了一些简短的解释。我们也对Python的一些内置和外部的模块进行了简短的介绍,这让开发人员在建立并行系统时会更加轻松。 -在下一章节,我们会学习一些设计并行算法的技术。 \ No newline at end of file +在下一章节,我们会学习一些设计并行算法的技术。 diff --git a/docs/chapter1/taking_care_of_GIL.md b/docs/chapter1/taking_care_of_GIL.md new file mode 100644 index 0000000..0128b2b --- /dev/null +++ b/docs/chapter1/taking_care_of_GIL.md @@ -0,0 +1,5 @@ +# 小心Python GIL + +**GIL** 是一种用于实现标准 Python(称为 CPython)的一种机制,以避免由不同线程同时执行字节码。 Python 中 **GIL** 的存在是该语言用户之间激烈讨论的一个原因。 选择 **GIL** 是为了保护 **CPython** 解释器使用的内部内存,它没有为线程的并发访问实现同步机制。 无论如何,当我们决定使用线程时,**GIL** 会导致问题,而这些线程往往受 **CPU** 限制。 例如,**I/O** 线程不在 **GIL** 的范围之内。 也许该机制对 Python 的演变带来的好处多于对它的伤害。 显然,我们不能仅将速度视为判断某事好坏的单一论据。 + +在某些情况下,使用多进程配合**消息传递**能更好的平衡可维护性、可扩展性以及性能之间的关系。 可即便如此,在某些情况下确实需要线程,这将被 **GIL** 制服。 在这些情况下,可以做的就是用C语言编写一些代码作为扩展,并将它们嵌入到Python程序中。 因此,还有其他选择; 由开发人员分析真正的必要性。 那么,问题来了:一般来说,**GIL** 是反派吗?重要的是要记住,**PyPy** 团队正在研究 **STM** 实现,以便从 **Python** 中删除 **GIL**。 有关该项目的更多详细信息,请访问 {target="_blank"}。 diff --git a/docs/chapter1/why_use_parallel_programming.md b/docs/chapter1/why_use_parallel_programming.md new file mode 100644 index 0000000..372fd93 --- /dev/null +++ b/docs/chapter1/why_use_parallel_programming.md @@ -0,0 +1,9 @@ +# 为什么使用并行编程 + +自从计算系统发展以来,它们已经开始提供一个能使我们以并行的方式运行特定程序的独立部分的机制,从而增强响应和总体性能。 此外,我们可以很容易的就验证配备有多个处理器以及多核的机器。那么,为什么不利用这种架构呢? + +并行编程在所有系统开发环境中都是可实现的,从智能手机和平板电脑到研究中心的重型计算。 并行编程的坚实基础将使开发人员能够优化应用程序的性能。 这会增强用户体验以及计算资源的消耗,从而减少完成复杂任务的处理时间。 + +举一个并行性的例子,让我们想象一个场景,在这个场景中,有一些任务,其中一个任务是从数据库中检索一些信息,而这个数据库规模又很大。再假如,这个应用还需要顺序执行,在这个应用中,这些任务必须以一定的逻辑顺序,一个接一个的执行。当用户请求数据时,在返回的数据没有结束之前,其它系统将一直被阻塞。然而,利用并行编程,我们将会创造一个新的worker来在数据库中查询信息,而不会阻塞这个应用的其它功能,从而提高它的使用。 + +举一个并行性的例子,让我们想象一个场景,在这个场景中,除了其他任务之外,应用程序还从数据库中查询信息,并且这个数据库有相当大的规模。 还要考虑,应用程序中的任务是按逻辑顺序一个接一个地执行的。 当用户请求数据时,系统的其余部分将被阻塞,直到数据返回未结束。 然而,利用并行编程,我们将被允许创建一个新的 **worker**,它将在这个数据库中查找信息而不阻塞应用程序中的其他功能,从而增强它的使用。 diff --git "a/\347\254\254\344\272\214\347\253\240/\350\256\276\350\256\241\345\271\266\350\241\214\347\256\227\346\263\225.md" b/docs/chapter2/index.md similarity index 89% rename from "\347\254\254\344\272\214\347\253\240/\350\256\276\350\256\241\345\271\266\350\241\214\347\256\227\346\263\225.md" rename to docs/chapter2/index.md index 3e026a0..dd0bf74 100644 --- "a/\347\254\254\344\272\214\347\253\240/\350\256\276\350\256\241\345\271\266\350\241\214\347\256\227\346\263\225.md" +++ b/docs/chapter2/index.md @@ -1,4 +1,4 @@ -##设计并行算法 +# 设计并行算法 在开发并行系统时,在你编码之前,有几个方面你必须要留意。为了在任务中获得成功,从一开始将并行的问题列出来是至关重要的。在这一章节,我们将接触一些技术方面的解决方案。 @@ -7,4 +7,4 @@ - 分治技术 - 数据分解 - 用管道分解任务 -- 处理和映射 \ No newline at end of file +- 处理和映射 diff --git "a/docs/chapter2/\344\275\277\347\224\250\346\225\260\346\215\256\345\210\206\350\247\243.md" "b/docs/chapter2/\344\275\277\347\224\250\346\225\260\346\215\256\345\210\206\350\247\243.md" new file mode 100644 index 0000000..c8df557 --- /dev/null +++ "b/docs/chapter2/\344\275\277\347\224\250\346\225\260\346\215\256\345\210\206\350\247\243.md" @@ -0,0 +1,11 @@ +# 使用数据分解 + +并行化问题的方法之一是通过**数据分解**。想象一下有这么一个场景,在这个场景中我们要以标量4乘以一个2x2矩阵(这个矩阵被称为矩阵A).在一个顺序执行系统中,我们将一个接一个的执行每个乘法的操作,最后生成所有指令的最终结果。根据矩阵A的大小,这个问题的顺序解决方案可能是旷日持久的。然而,当数据分解被应用的时候,我们可以想象矩阵A被分解为一个一个小的部分,这些分片数据被相关的workers以并行的方式接受并处理。下图以一个2x2矩阵乘以一个标量值的例子说明了数据分解应用的概念: + +![1](../imgs/2-02.png) + +上图中出现的矩阵相乘的问题有一定的对称性,每个必要的操作的结果是由一个单独的worker执行的,而且每个worker执行同样数量的操作来解决问题。然而,在现实世界中,worker的数量和已分解的数据数量的关系是不对称的,这将直接影响解决方案的性能。最后,每个worker所产生的结果必须整合起来以便使程序最终输出意义结果。为了进行这种整合,workers之间需要进行**信息交换**或是**共享状态**。 + +!!! info "" + + 数据分解的粒度选择可能会影响解决方案的性能。 diff --git "a/docs/chapter2/\345\210\206\346\262\273\346\212\200\346\234\257.md" "b/docs/chapter2/\345\210\206\346\262\273\346\212\200\346\234\257.md" new file mode 100644 index 0000000..7f4be98 --- /dev/null +++ "b/docs/chapter2/\345\210\206\346\262\273\346\212\200\346\234\257.md" @@ -0,0 +1,7 @@ +# 分治技术 + +当你面对一个复杂的问题时,要做的第一件事就是分解问题,以确定可以独立处理的部分。 通常,解决方案中的可**并行化部分**是可以分割和分布式处理的部分,以便由不同的worker处理。 分而治之的技术涉及**递归**地分割**域**(domain),直到找到并解决完整问题的不可分割的单元。 **归并排序**、**快速排序**等排序算法都可以通过这种方式解决。 + +下图显示了**归并排序**在六个元素的向量中的应用,可以看到使用了**分而治之技术**: + +![1](../imgs/2-01.png) diff --git "a/\347\254\254\344\272\214\347\253\240/\345\244\204\347\220\206\345\222\214\346\230\240\345\260\204.md" "b/docs/chapter2/\345\244\204\347\220\206\345\222\214\346\230\240\345\260\204.md" similarity index 59% rename from "\347\254\254\344\272\214\347\253\240/\345\244\204\347\220\206\345\222\214\346\230\240\345\260\204.md" rename to "docs/chapter2/\345\244\204\347\220\206\345\222\214\346\230\240\345\260\204.md" index d3d1d14..7943760 100644 --- "a/\347\254\254\344\272\214\347\253\240/\345\244\204\347\220\206\345\222\214\346\230\240\345\260\204.md" +++ "b/docs/chapter2/\345\244\204\347\220\206\345\222\214\346\230\240\345\260\204.md" @@ -1,4 +1,4 @@ -##处理和映射 +# 处理和映射 workers的数量并不足以在一个单一的步骤里解决一个特定的问题。因此前面的章节给出的分解技术是很有必要的。然而分解技术不应该被随意的应用,有一些因素会影响解决方案的性能。在数据或者任务分解之后,我们应该问这么一个问题:“我们应该在workers中如何划分进程的负载来获得比较好的性能?”这不是一个很好回答的问题,因为这取决于正在研究的问题。 @@ -7,14 +7,14 @@ workers的数量并不足以在一个单一的步骤里解决一个特定的问 * 识别独立的任务 * 识别需要数据交换的任务 -###识别独立的任务 +## 识别独立的任务 在系统中识别独立的任务将允许我们在不同的workers之间分配任务,因为这些任务不需要持续的通信。因为不需要一个数据单元,所以任务可以在不同的workers间执行而不会影响其它任务的执行。 -###识别需要数据交换的任务 +## 识别需要数据交换的任务 将需要相互通讯的任务组合起来放到单个worker中可以提高性能。当有大的通信负载的时候的时候这个真的可以提高性能,因为它能减少任务间信息交换的开销。 -###平衡负载 +## 平衡负载 -在并行解决方案中一个典型的问题是如何为不同的工作单元分配计算资源。我们越是将任务分配给不同的workers处理,我们将是需要越多的通信。另一方面,我们越是将任务组合起来分配給一个worker,与通信相关的开销越小。然而,我们可能会增加空转,也就是说,浪费了计算的能力。在并行编程中,浪费并不好。此外,越是将数据聚合在一个worker中,越会减少通过简单的添加更多的设备而增加计算能力的可扩展的灵活性。在一个基于通信的架构(轻微的数据聚合)中,为集群或者网格简单的增加机器从而提升处理性能甚至不需要中断正在运行的系统。 +在并行解决方案中一个典型的问题是如何为不同的工作单元分配计算资源。我们**越是将任务分配给不同的workers处理,我们将是需要越多的通信**。另一方面,我们越是将任务组合起来分配給一个worker,与通信相关的开销越小。然而,我们可能会**增加空转**,也就是说,**浪费了计算的能力**。在并行编程中,浪费并不好。此外,越是将数据聚合在一个worker中,越会减少通过简单的添加更多的设备而增加计算能力的可扩展的灵活性。在一个**基于通信的架构**(轻微的数据聚合)中,**为集群或者网格简单的增加机器从而提升处理性能甚至不需要中断正在运行的系统。** diff --git "a/\347\254\254\344\272\214\347\253\240/\346\200\273\347\273\223.md" "b/docs/chapter2/\346\200\273\347\273\223.md" similarity index 96% rename from "\347\254\254\344\272\214\347\253\240/\346\200\273\347\273\223.md" rename to "docs/chapter2/\346\200\273\347\273\223.md" index df5079c..a20dfe1 100644 --- "a/\347\254\254\344\272\214\347\253\240/\346\200\273\347\273\223.md" +++ "b/docs/chapter2/\346\200\273\347\273\223.md" @@ -1,4 +1,4 @@ -##总结 +# 总结 在这一章节,我们讨论了一些创建并行解决方案的方法。你的注意力应重点放在在不同的workers之间划分处理负载,考虑聚合而不是数据。 diff --git "a/docs/chapter2/\347\224\250\347\256\241\351\201\223\345\210\206\350\247\243\344\273\273\345\212\241.md" "b/docs/chapter2/\347\224\250\347\256\241\351\201\223\345\210\206\350\247\243\344\273\273\345\212\241.md" new file mode 100644 index 0000000..5554e7f --- /dev/null +++ "b/docs/chapter2/\347\224\250\347\256\241\351\201\223\345\210\206\350\247\243\344\273\273\345\212\241.md" @@ -0,0 +1,18 @@ +# 用管道分解任务 + +**管道技术**用于必须以协作方式执行以解决问题而组织任务的一种技术。 **管道**(pipeline)将大型任务分解为以并行方式运行的较小的独立任务。 管道模型可以比作汽车工厂的装配线,其中底盘是输入的原材料。 随着原材料经过不同的生产阶段,几个worker依次执行不同的动作,直到过程结束,这样我们才能准备好汽车。 该模型与开发的顺序范式非常相似; 任务一个接一个地对数据执行,通常,一个任务得到一个输入,这是前一个任务的结果。 那么这个模型与顺序技术有什么区别呢? 管道技术的每个阶段都有自己的worker,并且他们以并行的方式处理问题。 + +计算上下文中的一个示例可能是系统批量处理图像并将提取到数据库中的数据持久化。 我们将得到以下事实序列: + +* 接受输入的图像并且以对这些图片以并行的方式进行排列,这些图片将在第二阶段进行处理 +* 解析图像,并且有用的信息将会被送到第三阶段 +* 在第三阶段,过滤器被并行的应用在图像上 +* 来自第三阶段的数据结果被保存在数据库中 + +!!! info "" + + **管道技术**的每个阶段都以独立的方式与自己的worker一起执行。 但是,它建立了数据通信机制,以便可以交换信息。 + +下图展示了管道的概念: + +![1](../imgs/2-03.png) diff --git "a/\347\254\254\344\270\211\347\253\240/\350\257\206\345\210\253\344\270\200\344\270\252\345\217\257\345\271\266\350\241\214\347\232\204\351\227\256\351\242\230.md" b/docs/chapter3/index.md similarity index 89% rename from "\347\254\254\344\270\211\347\253\240/\350\257\206\345\210\253\344\270\200\344\270\252\345\217\257\345\271\266\350\241\214\347\232\204\351\227\256\351\242\230.md" rename to docs/chapter3/index.md index 97f4995..168985f 100644 --- "a/\347\254\254\344\270\211\347\253\240/\350\257\206\345\210\253\344\270\200\344\270\252\345\217\257\345\271\266\350\241\214\347\232\204\351\227\256\351\242\230.md" +++ b/docs/chapter3/index.md @@ -1,4 +1,4 @@ -##识别一个可并行的问题 +# 识别一个可并行的问题 前一章我们从不同角度探讨了并行方面的一些问题。现在我们将分析一些具体的问题,这些将在具体实现时自始至终会对我们有指导的作用。 diff --git "a/docs/chapter3/\344\273\216\345\244\232\344\270\252\350\276\223\345\205\245\344\270\255\345\276\227\345\210\260\346\226\220\346\263\242\351\202\243\345\245\221\346\234\200\345\244\247\347\232\204\345\200\274.md" "b/docs/chapter3/\344\273\216\345\244\232\344\270\252\350\276\223\345\205\245\344\270\255\345\276\227\345\210\260\346\226\220\346\263\242\351\202\243\345\245\221\346\234\200\345\244\247\347\232\204\345\200\274.md" new file mode 100644 index 0000000..e212912 --- /dev/null +++ "b/docs/chapter3/\344\273\216\345\244\232\344\270\252\350\276\223\345\205\245\344\270\255\345\276\227\345\210\260\346\226\220\346\263\242\351\202\243\345\245\221\346\234\200\345\244\247\347\232\204\345\200\274.md" @@ -0,0 +1,27 @@ +# 从多个输入中得到斐波那契最大的值 + +众所周知,斐波那契数列被定义如下: + +F(n)={0, if n=01, if n=1F(n1)+F(n2),ifn>1 + +实际上,按照从0到10计算斐波那契的值,结果将会是0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 和55. + +用迭代法计算斐波那契最高的值的Python代码如下: + +```python +def fibonacci(input): + a, b = 0, 1 + for item in range(input): + a, b = b, a + b + return a +``` + +斐波那契函数为一个特定的输入的数据计算一个最大的斐波那契值。让我们想象一个场景,在这个场景中,需要计算斐波那契的值,而本网站将会从一个用户那里接收到几个输入。假设用户提供一个数组的值作为输入,因此使这些计算按顺序将会很有趣。但是假使是100万个用户同时发出请求那么将会怎么样?在这种情况下,一些用户不得不等待很长一段时间才能得到他们的答案。 + +让我们只考虑先前文中的Python实现的斐波那契函数代码。我们怎么在有一个数组数据输入的情况下使用并行性来实现它?在前一章已经展示过多种实现的方式,这里我们使用其中一种方式-**数据分解**。我们可以将数组分为几个单元,每个单元关联一个任务然后被一个worker执行。下图描述了一个建议的解决方案: + +![1](../imgs/3-01.png) + +!!! info "" + + 作为对读者的建议,请完成实施缓存计算值机制的练习,以避免浪费 CPU 时间。 我们推荐像 [memcached](http://memcached.org/){target="_blank"} 这样的缓存数据库。 diff --git "a/docs/chapter3/\346\200\273\347\273\223.md" "b/docs/chapter3/\346\200\273\347\273\223.md" new file mode 100644 index 0000000..f613a20 --- /dev/null +++ "b/docs/chapter3/\346\200\273\347\273\223.md" @@ -0,0 +1,5 @@ +# 总结 + +在这一章节,我们学习了并行中出现的常见问题以及可能的解决方案。下面将会展示任何使用不同的Python库来解决上面展示的问题。 所提出的问题下面将使用不同的并行 Python 库来实现解决方案。 + +在下一章节,当使用`threading`模块,我们专注于的线程的解决方案,在用到`mutliprocess`模块时,解决方案也会涉及到使用多进程,以此类推。 diff --git "a/docs/chapter3/\347\210\254\345\217\226\347\275\221\351\241\265.md" "b/docs/chapter3/\347\210\254\345\217\226\347\275\221\351\241\265.md" new file mode 100644 index 0000000..cc90705 --- /dev/null +++ "b/docs/chapter3/\347\210\254\345\217\226\347\275\221\351\241\265.md" @@ -0,0 +1,17 @@ +# 爬取网页 + +本书要研究的另一个问题是并行网络爬虫的实现。 网络爬虫由浏览网络以搜索页面信息的计算机程序组成。 要分析的场景是一个序列网络爬虫由可变数量的**统一资源定位器**(Uniform Resource Locators - URLs)提供的问题,它必须搜索提供的每个 URL 中的所有链接。 假设输入的 URL 数量可能比较多,我们可以通过以下方式规划一个寻找并行性的解决方案: + +1. 将所有的**URLs**分成组组合到一个数据结构中。 +2. 把这些**URLs**分配給多个任务,这样写任务会爬取每个URL中的包含信息。 +3. 将这些任务分派给多个并行的**workers**来执行。 +4. 前一阶段的结果必须传递到下一阶段,这将改进原始收集的数据,从而保存它们并将它们与原始 **URL** 相关联。 + +正如我们在提议的解决方案的编号步骤中所观察到的,存在以下两种方法的组合: + +* **数据分解**:这发生在我们划分和关联URLs到任务上。 +* **用管道进行任务分解**:这包含三个阶段的管道,这发生在我们**链接接收**、**存储**以及**组织爬取的结果**的任务。 + +下图显示了解决方案: + +![1](../imgs/3-02.png) diff --git a/docs/chapter4/crawling_the_web_using_the_concurrent_futures_module.md b/docs/chapter4/crawling_the_web_using_the_concurrent_futures_module.md new file mode 100755 index 0000000..c41b00e --- /dev/null +++ b/docs/chapter4/crawling_the_web_using_the_concurrent_futures_module.md @@ -0,0 +1,166 @@ +# 使用concurrent.futures模块爬取web信息 + +下一节将通过实现并行 Web 爬虫来使用我们的代码。 在此方案中,我们将使用一个非常有趣的 **Python** 资源,即 `concurrent.futures` 模块中的 `ThreadPoolExecutor`。 在前面的示例中,我们分析了 `parallel_fibonacci.py`,使用了非常原始的线程形式。 此外,在特定时刻,我们不得不手动创建和初始化多个线程。 在较大的程序中,很难管理这种情况。 在这种情况下,有一些机制允许建立一个线程池。 线程池只不过是一个数据结构,它保留了多个先前创建的线程,供某个进程使用。 它旨在**重用线程**,从而避免不必要的线程创建——这是昂贵的。 + +基本上,如前一章所述,我们将有一个算法将分阶段执行一些任务,这些任务相互依赖。 在这里,我们将研究并行网络爬虫的代码。 + +导入一些模块并设置日志文件后,我们使用名为 `re` 的内置模块创建了一个**正则表达式**(有关此模块的完整文档可在 {target="_blank"})。 我们将使用它来过滤从抓取阶段返回的页面中的链接。 代码如下: + +```python +html_link_regex = re.compile('') +``` + +接下来我们创建一个同步队列来模拟输入数据. 然后我们创建一个名为`result_dict`的字典实例. 在此,我们会将 URL 及其各自的链接关联为列表结构。 相关代码如下: + +```python +urls = queue.Queue() +urls.put('http://www.google.com') +urls.put('http://br.bing.com/') +urls.put('https://duckduckgo.com/') +urls.put('https://github.com/') +urls.put('http://br.search.yahoo.com/') +result_dict = {} +``` + +再接下来我们定义一个名为`group_urls_task`的函数,该函数用于从同步队列中抽取出URL并存入`result_dict`的key值中. 另一个应该留意的细节是,我们调用`Queue`的`get`方法是,带了两个参数,第一个参数为`True`表示阻塞其他线程访问这个同步队列,第二个参数是`0.05`表示阻塞的超时时间,这样就防止出现由于同步队列中没有元素而等待太长时间的情况出现. 毕竟,在某些情况下,你不会想化太多的时间来等待新元素的到来. 相关代码如下: + +```python +def group_urls_task(urls): + try: + url = urls.get(True, 0.05) # true表示阻塞其他线程访问这个队列,0.05表示阻塞的超时时间 + result_dict[url] = None + logger.info("[%s] putting url [%s] in dictionary..." % (threading.current_thread().name, url)) + except queue.Empty: + logging.error('Nothing to be done, queue is empty') +``` + +现在,我们有了负责完成每个作为参数发送给 `crawl_task` 函数的 `URL` 的抓取阶段的任务。 基本上,抓取阶段是通过获取接收到的 `URL` 指向的页面内的所有链接来完成的。 爬取返回的元组包含第一个元素作为 `crawl_task` 函数接收的 `URL`。 第二步,提取链接列表。 `requests`模块(关于`request`模块的官方文档可以在{target="_blank"}找到)用于从URL获取网页。 代码如下: + +```python +def crawl_task(url): + links = [] + try: + request_data = requests.get(url) + logger.info("[%s] crawling url [%s] ..." % ( + threading.current_thread().name, url)) + links = html_link_regex.findall(request_data.text) + except: + logger.error(sys.exc_info()[0]) + raise + finally: + return (url, links) +``` + +进一步分析代码,我们将看到创建了一个 `ThreadPoolExecutor` 对象(有关 `ThreadPoolExecutor` 对象的更多信息,请访问 {target="_blank"} ) 在 `concurrent.futures` 模块中有特色。 在这个 `ThreadPoolExecutor` 对象的构造函数中,我们可以定义一个名为 `max_workers` 的参数, 该参数决定了线程池中的线程数。 在从同步队列中删除 `URL` 并将键插入到 `result_dict` 的阶段,选择是使用三个工作线程。 数量将根据问题而有所不同。 在定义 `ThreadPoolExecutor` 并使用 `with` 语句来保证结束线程之后,这些线程将在 `with` 语句范围的输出中执行。 在 `ThreadPoolExecutor` 对象的范围内,我们在同步队列中对其进行迭代,并通过 `submit` 方法分派它为包含 `URL` 的队列线程执行引用。 总而言之,`submit` 方法为线程的执行安排了一个可调用对象,并返回一个包含为执行创建的安排的 `Future` 对象。 `submit` 方法接收一个可调用对象及其参数; 在我们的例子中,可调用项是任务 `group_urls_task`,参数是对同步队列函数的引用。 调用这些参数后,池中定义的工作线程将以并行、异步的方式执行预订。 代码如下: + +```python +with concurrent.futures.ThreadPoolExecutor(max_workers=3) as group_link_threads: + for i in range(urls.qsize()): + group_link_threads.submit(group_urls_task, urls) +``` + +在前面的代码之后,我们又创建了一个`ThreadPoolExecutor`; 但是这一次,我们要使用上一阶段`group_urls_task`生成的`key`来运行爬虫阶段。 这一次我们所使用的代码有些不同: + +```python +future_tasks = { + crawler_link_threads.submit(crawl_task, url): url + for url in result_dict.keys()} +``` + +我们映射了一个名为`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`中生成最后的条目,如下面的截图所示。 + +```shell +$ python temp2.py +2023-03-01 15:53:51,289 - [ThreadPoolExecutor-0_0] putting url [http://www.google.com] in dictionary... +2023-03-01 15:53:51,289 - [ThreadPoolExecutor-0_1] putting url [http://br.bing.com/] in dictionary... +2023-03-01 15:53:51,290 - [ThreadPoolExecutor-0_0] putting url [https://duckduckgo.com/] in dictionary... +2023-03-01 15:53:51,290 - [ThreadPoolExecutor-0_2] putting url [https://github.com/] in dictionary... +2023-03-01 15:53:51,290 - [ThreadPoolExecutor-0_1] putting url [http://br.search.yahoo.com/] in dictionary... +2023-03-01 15:53:51,334 - Starting new HTTP connection (1): 127.0.0.1:7890 +2023-03-01 15:53:51,408 - Starting new HTTPS connection (1): duckduckgo.com:443 +2023-03-01 15:53:51,411 - Starting new HTTP connection (1): 127.0.0.1:7890 +2023-03-01 15:53:51,584 - http://127.0.0.1:7890 "GET http://www.google.com/ HTTP/1.1" 200 6588 +2023-03-01 15:53:51,585 - [ThreadPoolExecutor-1_0] crawling url [http://www.google.com] ... +2023-03-01 15:53:51,621 - Starting new HTTPS connection (1): github.com:443 +2023-03-01 15:53:51,625 - http://127.0.0.1:7890 "GET http://br.bing.com/ HTTP/1.1" 302 0 +2023-03-01 15:53:51,628 - Resetting dropped connection: 127.0.0.1 +2023-03-01 15:53:51,704 - https://duckduckgo.com:443 "GET / HTTP/1.1" 200 None +2023-03-01 15:53:51,706 - [ThreadPoolExecutor-1_2] crawling url [https://duckduckgo.com/] ... +2023-03-01 15:53:51,822 - Starting new HTTP connection (1): 127.0.0.1:7890 +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 +2023-03-01 15:53:51,962 - https://github.com:443 "GET / HTTP/1.1" 200 None +2023-03-01 15:53:51,978 - [ThreadPoolExecutor-1_1] crawling url [http://br.bing.com/] ... +2023-03-01 15:53:52,045 - [ThreadPoolExecutor-1_0] crawling url [https://github.com/] ... +2023-03-01 15:53:52,223 - http://127.0.0.1:7890 "GET http://br.search.yahoo.com/ HTTP/1.1" 301 25 +2023-03-01 15:53:52,225 - Starting new HTTPS connection (1): br.search.yahoo.com:443 +2023-03-01 15:53:52,697 - https://br.search.yahoo.com:443 "GET / HTTP/1.1" 200 17530 +2023-03-01 15:53:52,859 - [ThreadPoolExecutor-1_2] crawling url [http://br.search.yahoo.com/] ... +``` + +又一次,我们可以发现每个线程池中的线程执行是乱序的,但这不重要,重要的是,`resul\_dict`中输出的内容就是最终结果. + +## 完整代码 + +译者注: + +```python +import sys +import re +import logging, threading +import queue +from concurrent.futures import ThreadPoolExecutor + +import requests + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(message)s') + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +ch.setFormatter(formatter) +logger.addHandler(ch) + +html_link_regex = re.compile('') + +urls = queue.Queue() +urls.put('http://www.google.com') +urls.put('http://br.bing.com/') +urls.put('https://duckduckgo.com/') +urls.put('https://github.com/') +urls.put('http://br.search.yahoo.com/') + +result_dict = {} + +def group_urls_task(urls): + try: + url = urls.get(True, 0.05) # true表示阻塞其他线程访问这个队列,0.05表示阻塞的超时时间 + result_dict[url] = None + logger.info("[%s] putting url [%s] in dictionary..." % (threading.current_thread().name, url)) + except queue.Empty: + logging.error('Nothing to be done, queue is empty') + +def crawl_task(url): + links = [] + try: + request_data = requests.get(url) + logger.info("[%s] crawling url [%s] ..." % (threading.current_thread().name, url)) + links = html_link_regex.findall(request_data.text) + except: + logger.error(sys.exc_info()[0]) + raise + finally: + return (url, links) + + +if __name__ == "__main__": + + with ThreadPoolExecutor(max_workers=3) as group_link_threads: + for i in range(urls.qsize()): + group_link_threads.submit(group_urls_task, urls) + + with ThreadPoolExecutor(max_workers=3) as crawler_link_threads: + future_tasks = { + crawler_link_threads.submit(crawl_task, url): url + for url in result_dict.keys()} +``` diff --git a/docs/chapter4/defining_threads.md b/docs/chapter4/defining_threads.md new file mode 100755 index 0000000..306293a --- /dev/null +++ b/docs/chapter4/defining_threads.md @@ -0,0 +1,78 @@ +# 什么是线程 + +**线程是进程中的不同执行线**。 让我们把一个程序想象成一个蜂巢,在这个蜂巢内部有一个收集花粉的过程。 这个采集过程是由几只工蜂同时工作来完成的,以解决花粉不足的问题。 工蜂扮演着线程的角色,在进程内部活动并共享资源来执行它们的任务。 + +线程属于同一个进程,共享同一个内存空间。 因此,开发人员的任务是控制和访问这些内存区域。 + +## 使用线程的优点和缺点 + +在决定使用线程时必须考虑一些**优点**和**缺点**,这取决于用于实现解决方案的语言和操作系统。 + +使用线程的**优势**如下所示: + +- 同一进程内的线程**通信**、**数据定位**、**共享信息**的速度快 +- 线程的创建比进程的创建成本更低,因为不需要复制主进程上下文中包含的所有信息 +- 通过处理器的高速缓存优化内存访问,充分利用**数据局部性**(data locality)的优势。 + +使用线程的**缺点**如下: + +- 数据共享允许快速通信。 但是,它也允许缺乏经验的开发人员引入难以解决的错误。 +- 数据共享限制了解决方案的灵活性。 例如,迁移到分布式架构可能会让人头疼。 通常,它们限制了算法的可扩展性。 + +!!! info "" + + 在 Python 编程语言中,由于 GIL,使用**计算密集型**(CPU-bound)的线程可能会损害应用程序的性能。 + +## 理解不同类型的线程 + +有两种类型的线程,**内核线程**和**用户线程**。 **内核线程是由操作系统创建和管理的线程**, 其上**下文的交换**、**调度**和**结束**都由当前操作系统的内核来进行管理。 对于**用户线程**,这些状态由**包**(package)开发人员控制。 + +我们可以引用每种线程的一些优点: + + + + + + + + + + + + + + + + + + + + + +
线程类型优点缺点
内核线程 + 一个内核线程其实就是一个进程. 因此即使一个内核线程被阻塞了,其他的内核线程仍然可以运行。
+ 内核线程可以在不同的 CPU 上运行。 +
+ 创建线程和线程间同步的消耗太大
+ 实现依赖于平台 +
用户线程 + 用户线程的创建和线程间同步的开销较少
+ 用户线程是平台无关的。 +
+ 同一进程中的所有用户线程都对应一个内核线程. 因此,若该内核线程被阻塞,则所有相应的用户线程都会被阻塞。
+ 不同用户线程无法运行在不同CPU上 +
+ +## 线程的状态 + +线程的生命周期有五种可能的状态。它们如下: + +- **新建**(Creation): 该过程的主要动作就是创建一个新线程, 创建完新线程后,该线程被发送到待执行的线程队列中。 +- **运行**(Execution): 该状态下,线程获取到并消耗CPU资源。 +- **就绪**(Ready): 该状态下,线程在待执行的线程队列中排队,等待被执行 +- **阻塞**(Blocked): 该状态下,线程由于等待某个事件(例如I/O操作)的出现而被阻塞. 这时线程并不使用CPU。 +- **死亡**(Concluded): 该状态下,线程释放执行时使用的资源并结束整个线程的生命周期。 + +## 是使用threading模块还是_thread模块 + +Python提供了两个模块来实现基于系统的线程: **`_thread`模块**(该模块提供了使用线程相关的较低层的API; 它的文档可以在 找到)和**`threading`模块**(该模块提供了使用线程相关的更高级别的API; 它的文档可以在 中找到). **`threading`模块**提供的接口要比**`_thread`模块**的结构更友好一些. 至于具体选择哪个模块取决于开发者, 如果开发人员发现在较低级别使用线程很容易,实现他们自己的线程池并拥抱锁和其他原始功能(features),他/她宁愿使用`_thread`。 否则,`threading`是最明智的选择。 diff --git "a/\347\254\254\345\233\233\347\253\240/ReadMe.md" b/docs/chapter4/index.md similarity index 89% rename from "\347\254\254\345\233\233\347\253\240/ReadMe.md" rename to docs/chapter4/index.md index 2df3bca..2a0c351 100644 --- "a/\347\254\254\345\233\233\347\253\240/ReadMe.md" +++ b/docs/chapter4/index.md @@ -1,8 +1,9 @@ -#使用threading和concurrent.futures模块 +# 使用threading和concurrent.futures模块 在上面的章节里,我们列举了一些能够被并行化解决的潜在问题. 在本章中,我们会分析如何使用Python中的threading模块来解决这些问题. 本章包含如下议题: + - 定义什么是线程 - 选择使用threading库还是_thread库 - 使用threading模块来为多个输入同时计算Fibonacci序列 diff --git a/docs/chapter4/using_threading_to_obtain_the_Fibonacci_series_term_with_multiple_inputs.md b/docs/chapter4/using_threading_to_obtain_the_Fibonacci_series_term_with_multiple_inputs.md new file mode 100644 index 0000000..749cd99 --- /dev/null +++ b/docs/chapter4/using_threading_to_obtain_the_Fibonacci_series_term_with_multiple_inputs.md @@ -0,0 +1,197 @@ +# 使用多线程解决斐波那契序列多输入问题 + +现在是时候实现了。任务是在给定多个输入值时并行执行斐波那契数列的各项。 出于教学目的,我们将固定四个元素中的输入值和四个线程来处理每个元素,模拟 worker 和要执行的任务之间的完美对称。 该算法将按如下方式工作: + +1. 首先使用一个列表存储四个待输入值,这些值将被放入对于线程来说互相锁定的数据结构。 +2. 输入值被放入可被锁定的数据结构之后,负责处理斐波那契序列的线程将被告知可以被执行。这时,我们可以使用python线程的同步机制`Condition`模块(`Condition`模块对共享变量提供线程之间的同步操作),模块详情请参考:{target="_blank"}。 +3. 当每个线程结束斐波那契序列的计算后,分别把结果存入一个字典。 + +接下来我们将列出代码,并且讲述其中有趣的地方: + +代码开始处我们加入了对编码额外的支持,导入`logging`, `threading`和`queue`模块。此外,我们还定义了我们例子中用到主要数据结构。 +一个字典,被命名为`fibo_dict`,将用来存储输入输出数据,输入数据为`key`,计算结果(输出数据)为值。 +我们同样定义了一个队列对象,该对象中存储线程间的共享数据(包括读写)。 +我们把该对象命名为`shared_queue`。最后我们定义一个列表模拟程序的四个输入值。代码如下: + +```python +#coding: utf-8 + +import logging, threading + +from queue import Queue + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(message)s') + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +ch.setFormatter(formatter) +logger.addHandler(ch) + +fibo_dict = {} +shared_queue = Queue() +input_list = [3, 10, 5, 7] +``` + +!!! info "下载示例代码" + + 您可以从{target="_blank"}上的帐户下载您购买的所有 Packt 书籍的示例代码文件。 如果您在其他地方购买了本书,您可以访问 {target="_blank"} 并注册以便将文件直接通过电子邮件发送给您。 + +在下面的代码行中,我们将从名为 `Condition` 的线程模块中定义一个对象。 该对象旨在根据特定条件同步对资源的访问。 + +```python +queue_condition = threading.Condition() +``` + +使用 Condition 对象的用于控制队列的创建和在其中进行条件处理。 + +下一段代码是定义将由多个线程执行的函数。我们将称它为 `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()`方法,目的是告知某个队列的任务已经被提取并执行。代码如下: + +```python + +def fibonacci_task(condition): + with condition: + while shared_queue.empty(): + logger.info("[%s] - waiting for elements in queue.." % threading.current_thread().name) + condition.wait() + else: + value = shared_queue.get() + a, b = 0, 1 + for item in range(value): + a, b = b, a + b + fibo_dict[value] = a + shared_queue.task_done() + logger.debug("[%s] fibonacci of key [%d] with result [%d]" % (threading.current_thread().name, value, fibo_dict[value])) +``` + +我们定义的第二个函数是`queue_task`函数,它将由负责为`shared_queue`填充要处理的元素的线程执行。我们可以注意到获取`condition`作为访问`shared_queue`的一个参数。对于`input_list`中的每一个项目,线程都会将它们插入`shared_queue`中。 + +将所有元素插入 `shared_queue` 后,该函数通知负责计算斐波那契数列的线程队列已准备好使用。 这是通过使用 `condition.notifyAll()` 完成的,如下所示: + +```python +def queue_task(condition): + logging.debug('Starting queue_task...') + with condition: + for item in input_list: + shared_queue.put(item) + logging.debug("Notifying fibonacci_task threadsthat the queue is ready to consume..") + condition.notifyAll() # python3.10 中使用 notify_all() +``` + +在下一段代码中,我们创建了一组包含四个线程的集合,它们将等待来自 `shared_queue` 的准备条件。 然后我们强调了线程类的构造函数,它允许我们定义函数。该线程将使用目标参数执行,该函数在`args`中接收的参数如下: + +```python +threads = [ + threading.Thread(daemon=True, target=fibonacci_task,args=(queue_condition,)) + for i in range(4) +] +``` + +接着我们使用`thread`对象的`start`方法开始线程: + +```python +[thread.start() for thread in threads] +``` + +然后我们创建一个线程处理`shared_queue`,然后执行该线程。代码如下: + +```python +prod = threading.Thread(name='queue_task_thread', daemon=True, target=queue_task, args=(queue_condition,)) +prod.start() +``` + +最后,我们对所有计算斐波那契数列的线程调用了`join()`方法。这个调用的目的是让让主线程等待子线程的调用,直到所有子线程执行完毕之后才结束主线程。请参考下面的代码: + +```python +[thread.join() for thread in threads] +``` + +程序的执行结果如下: + +```shell +$ python temp.py +2023-03-01 12:19:26,873 - [Thread-1 (fibonacci_task)] - waiting for elements in queue.. +2023-03-01 12:19:26,873 - [Thread-2 (fibonacci_task)] - waiting for elements in queue.. +2023-03-01 12:19:26,874 - [Thread-3 (fibonacci_task)] - waiting for elements in queue.. +2023-03-01 12:19:26,874 - [Thread-4 (fibonacci_task)] - waiting for elements in queue.. +2023-03-01 12:19:26,874 - Starting queue_task... +2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. +2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. +2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. +2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume.. +2023-03-01 12:19:26,875 - [Thread-1 (fibonacci_task)] fibonacci of key [3] with result [2] +2023-03-01 12:19:26,875 - [Thread-2 (fibonacci_task)] fibonacci of key [10] with result [55] +2023-03-01 12:19:26,875 - [Thread-4 (fibonacci_task)] fibonacci of key [5] with result [5] +2023-03-01 12:19:26,875 - [Thread-3 (fibonacci_task)] fibonacci of key [7] with result [13] +``` + +请注意,首先创建并初始化 `fibonacci_task` 线程,然后它们进入等待状态。 同时,创建 `queue_task` 并填充 `shared_queue`。 最后,`queue_task` 通知 `fibonacci_task` 线程它们可以执行它们的任务。 + +请注意,`fibonacci_task` 线程的执行不遵循顺序逻辑,每次执行的顺序可能不同。 这就是使用线程的一个特点:**非确定性**(non-determinism)。 + +## 完整例子 + +译者注: + +```python + +#coding: utf-8 + +import logging, threading + +from queue import Queue + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(message)s') + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +ch.setFormatter(formatter) +logger.addHandler(ch) + +fibo_dict = {} +shared_queue = Queue() +input_list = [3, 10, 5, 7] + +queue_condition = threading.Condition() + +def fibonacci_task(condition): + with condition: + while shared_queue.empty(): + logger.info("[%s] - waiting for elements in queue.." % threading.current_thread().name) + condition.wait() + else: + value = shared_queue.get() + a, b = 0, 1 + for item in range(value): + a, b = b, a + b + fibo_dict[value] = a + shared_queue.task_done() + logger.debug("[%s] fibonacci of key [%d] with result [%d]" % (threading.current_thread().name, value, fibo_dict[value])) + +def queue_task(condition): + logging.debug('Starting queue_task...') + with condition: + for item in input_list: + shared_queue.put(item) + logging.debug("Notifying fibonacci_task threadsthat the queue is ready to consume..") + condition.notify_all() + + + + +if __name__ == "__main__": + threads = [ + threading.Thread(daemon=True, target=fibonacci_task,args=(queue_condition,)) + for i in range(4) + ] + + [thread.start() for thread in threads] + + prod = threading.Thread(name='queue_task_thread', daemon=True, target=queue_task, args=(queue_condition,)) + prod.start() + + [thread.join() for thread in threads] +``` diff --git "a/\347\254\254\345\233\233\347\253\240/\346\200\273\347\273\223.md" "b/docs/chapter4/\346\200\273\347\273\223.md" similarity index 63% rename from "\347\254\254\345\233\233\347\253\240/\346\200\273\347\273\223.md" rename to "docs/chapter4/\346\200\273\347\273\223.md" index 5c6ae3b..65e4917 100644 --- "a/\347\254\254\345\233\233\347\253\240/\346\200\273\347\273\223.md" +++ "b/docs/chapter4/\346\200\273\347\273\223.md" @@ -1,5 +1,5 @@ -##总结 -在本章,我们聚焦于使用线程的理论方法. 通过使用threading模块和concurrent.futures模块,我们实现了上一章所展示的案例,并通过这种方式展示这些模块的机制和灵活性 +# 总结 -下一章我们会使用multiprocessing和ProcessPoolExecutor这两个模块来再依次解决这两个问题. +在本章,我们聚焦于使用线程的理论方法. 通过使用threading模块和concurrent.futures模块,我们实现了上一章所展示的案例,并通过这种方式展示这些模块的机制和灵活性 +下一章我们会使用`multiprocessing`和`ProcessPoolExecutor`这两个模块来再依次解决这两个问题. diff --git a/docs/chapter5/crawling_the_web_using_processPoolExecutor.md b/docs/chapter5/crawling_the_web_using_processPoolExecutor.md new file mode 100644 index 0000000..c45ef12 --- /dev/null +++ b/docs/chapter5/crawling_the_web_using_processPoolExecutor.md @@ -0,0 +1,142 @@ +# 使用ProcessPoolExecutor模块设计网络爬虫 + +正如`concurrent.futures`模块提供了`ThreadPoolExecutor`,方便创建和操作多个线程,进程属于`ProcessPoolExecutor`类。 `ProcessPoolExecutor` 类也包含在 `concurrent.futures` 包中,用于实现我们的并行 `Web` 爬虫。 为了实施这个案例研究,我们创建了一个名为 `process_pool_executor_web_crawler.py` 的 `Python` 模块。 + +代码以前面示例中已知的导入启动,例如`requests`、`Manager` 模块等。 关于任务的定义,以及线程的使用,与上一章的示例相比几乎没有变化,只是现在我们发送数据以通过函数参数形式进行; 参考以下说明: + +`group_urls_task`函数定义如下: + +```python +def group_urls_task(urls, result_dict, html_link_regex) +``` + +`crawl_task`函数定义如下: + +```python +def crawl_task(url, html_link_regex) +``` + +现在让我们看一下代码块,其中有一些细微但相关的变化。进入主代码块,我们声明了一个`Manager`类型的对象,现在它将允许**共享队列**,而不仅仅是包含处理结果的字典。为了定义这个包含需要抓取的URL的名为`urls`的队列,我们将使用`Manager.Queue`对象。对于结果字典,我们将使用`Manager.dict`对象,旨在使用一个由代理管理的字典。下面示例代码说明了这些定义。 + +```python +if __name__ == '__main__': + manager = Manager() + urls = manager.Queue() + urls.put("http://br.bing.com/") + urls.put("https://github.com") + result_dict = manager.dict() +``` + +然后,我们定义了爬虫阶段要使用的正则表达式,并获得了运行该程序的机器的处理器数量,如以下代码所示: + +```python +html_link_regex = re.compile('') +number_of_cpus = cpu_count() +``` + +在最后一个代码块中,我们可以注意到`concurrent.futures`模块中的API的一致性。下面这块内容正是我们在上一章中提到的使用`ThreadPoolExecutor`的例子中使用的。然而,通过改变内部行为和解决CPU绑定进程的GIL问题,将该类改为`ProcessPoolExecutor`就足够了,而不会破坏代码。检查以下几个代码块;这两块都创建了`ProcessPoolExecutor`,其工作者的限制等于机器中处理器的数量。第一个executor用于将字典中的`URL`以标准的`None`值分组。第二个executor进行抓取阶段。 + +下面是第一个executor的一代码块。 + +```python +with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as group_link_processes: + for i in range(urls.qsize()): + group_link_processes.submit(group_urls_task, urls, result_dict, html_link_regex) +``` + +第二个executor的代码块如下: + +```python +with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as crawler_link_processes: + future_tasks = {crawler_link_processes.submit(crawl_task, url, html_link_regex): url for url in result_dict.keys()} + for future in concurrent.futures.as_completed(future_tasks): + result_dict[future.result()[0]] = future.result()[1] +``` + +!!! info "" + + 使用 `concurrent.futures` 从多线程模式切换到多进程稍微简单一些。 + +程序运行结果如下图: + +```shell +$ python process_pool_executor_web_crawler.py +[SpawnProcess-4] putting url [http://www.google.com] in dictionary... +[SpawnProcess-4] putting url [http://br.bing.com/] in dictionary... +[SpawnProcess-4] putting url [https://duckduckgo.com/] in dictionary... +[SpawnProcess-4] putting url [http://br.search.yahoo.com/] in dictionary... +[SpawnProcess-2] putting url [https://github.com/] in dictionary... +[SpawnProcess-11] crawling url [https://duckduckgo.com/] ... +[SpawnProcess-10] crawling url [http://www.google.com] ... +[SpawnProcess-8] crawling url [https://github.com/] ... +[SpawnProcess-7] crawling url [http://br.bing.com/] ... +[SpawnProcess-9] crawling url [http://br.search.yahoo.com/] ... +[http://www.google.com] with links: [http://www.google.com.hk/imghp?hl=zh-TW&tab=wi... +[http://br.bing.com/] with links: [javascript:void(0);... +[https://duckduckgo.com/] with links: [/about... +[http://br.search.yahoo.com/] with links: [https://br.yahoo.com/... +[https://github.com/] with links: [#start-of-content... +``` + +## 完整示例 + +译者注: + +```python +import sys +import re +import queue +from concurrent.futures import ProcessPoolExecutor, as_completed +from multiprocessing import Manager, cpu_count, current_process + +import requests + + +result_dict = {} + +def group_urls_task(urls, result_dict, html_link_regex): + try: + url = urls.get(True, 0.05) # true表示阻塞其他线程访问这个队列,0.05表示阻塞的超时时间 + result_dict[url] = None + print("[%s] putting url [%s] in dictionary..." % (current_process().name, url)) + except queue.Empty: + print('Nothing to be done, queue is empty') + +def crawl_task(url, html_link_regex): + links = [] + try: + request_data = requests.get(url) + print("[%s] crawling url [%s] ..." % (current_process().name, url)) + links = html_link_regex.findall(request_data.text) + except: + print(f"error: {sys.exc_info()[0]}") + raise + finally: + return (url, links) + +if __name__ == "__main__": + + manager = Manager() + urls = manager.Queue() + urls.put('http://www.google.com') + urls.put('http://br.bing.com/') + urls.put('https://duckduckgo.com/') + urls.put('https://github.com/') + urls.put('http://br.search.yahoo.com/') + result_dict = manager.dict() + + html_link_regex = re.compile('') + number_of_cpus = cpu_count() + + with ProcessPoolExecutor(max_workers=number_of_cpus) as group_link_processes: + for i in range(urls.qsize()): + group_link_processes.submit(group_urls_task, urls, result_dict, html_link_regex) + + with ProcessPoolExecutor(max_workers=number_of_cpus) as crawler_link_processes: + future_tasks = {crawler_link_processes.submit(crawl_task, url, html_link_regex): url for url in result_dict.keys()} + for future in as_completed(future_tasks): + result_dict[future.result()[0]] = future.result()[1] + + for url, links in result_dict.items(): + print(f"[{url}] with links: [{links[0]}...") +``` diff --git a/docs/chapter5/implementing_multiprocessing_communication.md b/docs/chapter5/implementing_multiprocessing_communication.md new file mode 100644 index 0000000..ce5eac0 --- /dev/null +++ b/docs/chapter5/implementing_multiprocessing_communication.md @@ -0,0 +1,106 @@ +# 实现多进程间通信 + +[multiprocessing](http://docs.python.org/3/library/multiprocessing.html)模块允许进程间以两种方式进行通信,这两种方式都基于消息传递机制。 如前所述,由于缺乏同步机制因此不得不采取消息传递机制,因此是数据副本在进程之间交换。 + +## 使用multiprocessing.Pipe模块 + +**管道**(pipe)由在两个端点(通信中的两个进程)之间建立通信的机制组成。 这是一种创建通道以便在进程之间交换消息的方法。 + +!!! warning "" + + Python 官方文档建议每两个端点使用一个管道,因为不能保证另一个端点同时读取安全。 + +为了举例说明`multiprocessing.Pipe`对象的使用,我们来实现一个创建两个进程A和B的Python程序,进程A发送一个1到10的随机整数值给进程B,进程B会显示它在屏幕上。 现在,让我们一步步介绍这个程序。 + +我们首先导入一些我们程序中需要的包,如下: + +```python +import os, random +from multiprocessing import Process, Pipe +``` + +通过`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`。 + +在程序的下一部分,我们将定义`producer_task`函数,除此之外,该函数将使用`random.randint(1, 10)`调用生成一个随机数。这个函数的关键点被称为`conn.send(value)`,它使用`Pipe`在主程序的流量中生成的连接对象,该连接对象(conn)已被作为参数发送到该函数。观察`producer_task`函数的全部内容,如下所示: + +```python +def producer_task(conn): + value = random.randint(1, 10) + conn.send(value) + print('Value [%d] send by PID [%d]' % (value, os.getpid())) + conn.close() +``` + +!!! warning "" + + 永远不要忘记总是调用`Pipe`连接的`close()`方法,该连接通过发送方法发送数据。当不再使用时,这对于最终释放与通信通道相关的资源是很重要的。 + +消费者进程要执行的任务非常简单,它的唯一目标是在屏幕上打印接收到的值,接收消费者进程的`PID`。 为了从通信通道获取发送的值,我们使用了 [conn.recv()](http://docs.python.org/dev/library/multiprocessing.html#multiprocessing.Connection.recv){target="_blank"} 调用。 `consumer_task` 函数的实现最终如下所示: + +```python +def consumer_task(conn): + print('Value [%d] received by PID [%d]' % (conn.recv(), os.getpid())) +``` + +我们这个小例子的最后部分实现了对`Pipe()`对象的调用,创建了两个连接对象,将被消费者和生产者进程使用。在这个调用之后,生产者和消费者进程被创建,分别发送`consumer_task`和`producer_task`函数作为目标函数,我们可以在下面的完整代码中看到。 + +```python +if __name__ == '__main__': + producer_conn, consumer_conn = Pipe() + consumer = Process(target=consumer_task,args=(consumer_conn,)) + producer = Process(target=producer_task,args=(producer_conn,)) + + consumer.start() + producer.start() + + consumer.join() + producer.join() +``` + +定义完进程后,就该调用`start()`方法来启动执行,并调用`join()`方法,这样主进程就会等待生产者和消费者进程的执行。 + +在下面的截图中,我们可以看到`multiprocessing_pipe.py`程序的输出。 + +```shell +$ python multiprocessing_pipe.py +Value [6] send by PID [95980] +Value [6] received by PID [95979] +``` + +## 理解multiprocessing.Queue模块 + +在上一节中,我们分析了管道的概念,通过创建一个通信通道在进程之间建立通信。现在,我们将分析如何有效地建立这种通信,利用`Queue`对象,它在`multiprocessing`模块中实现。`multiprocessing.Queue`的可用接口与`queue.Queue`相当类似。然而,内部实现使用了不同的机制,比如使用了`thread`的内部线程`feeder` ,它将数据从队列的数据缓冲区传输到与目标进程相关的管道。管道和队列机制都利用了**消息传递机制**,这使用户无需使用**同步机制**,从而节省了使用**同步机制**带来的开销。 + +!!! warning "" + + 虽然使用`multiprocessing.Queue`的用户不需要使用同步机制,例如`Locks`,但在内部,这些机制被用来在缓冲区和管道之间传输数据,以完成通信。 + +## 完整示例 + +译者注: + +```python +import os, random +from multiprocessing import Process, Pipe + +def producer_task(conn): + value = random.randint(1, 10) + conn.send(value) + print('Value [%d] send by PID [%d]' % (value, os.getpid())) + conn.close() + + +def consumer_task(conn): + print('Value [%d] received by PID [%d]' % (conn.recv(), os.getpid())) + +if __name__ == '__main__': + producer_conn, consumer_conn = Pipe() + consumer = Process(target=consumer_task,args=(consumer_conn,)) + producer = Process(target=producer_task,args=(producer_conn,)) + + consumer.start() + producer.start() + + consumer.join() + producer.join() +``` diff --git a/docs/chapter5/index.md b/docs/chapter5/index.md new file mode 100644 index 0000000..3eeba3e --- /dev/null +++ b/docs/chapter5/index.md @@ -0,0 +1,10 @@ +# 使用多进程和进程池 + +上章中,我们学习了如何使用`threading`模块解决两个问题。通过本章的学习,我们将学习如何使用`multiprocessing`模块解决上章的两个问题,我们将使用和上章类似的接口实现。然而,我们会使用多进程机制。 + +本章将覆盖如下一个知识点: + +* 理解进程的概念 +* 理解多进程通信 +* 使用多进程解决斐波那契数列多输入问题 +* 使用`ProcessPoolExecutor`模块设计网络爬虫 diff --git a/docs/chapter5/understanding_the_concept_of_a_process.md b/docs/chapter5/understanding_the_concept_of_a_process.md new file mode 100644 index 0000000..9c8b4ad --- /dev/null +++ b/docs/chapter5/understanding_the_concept_of_a_process.md @@ -0,0 +1,24 @@ +# 理解进程的概念 + +我们必须将操作系统中的**进程**(processes)理解为执行中的程序及其资源的**容器**。 与执行中的程序有关的所有内容都可以通过它所代表的进程进行管理——它的**数据区域**(data area)、它的**子进程**(child processes)、它的**资产**(estates)以及它**与其他进程的通信**(communication with other processes)。 + +## 理解进程模型 + +**进程**(processes)具有相关的信息和资源,可以对其进行操作和控制。 操作系统有一个称为**进程控制块** (PCB) 的结构,它存储有关进程的信息。 例如,PCB 可能存储以下信息: + +- **Process ID**(PID): 这是唯一的整数值(无符号),用于标识操作系统中的进程。 +- **程序计数器**(Program counter): 这包含要执行的下一条程序指令的地址。 +- **I/O信息**(I/O information): 这是与进程相关联的打开文件和设备的列表 +- **内存分配**(Memory allocation): 这存储有关进程使用和保留的内存空间以及分页表的信息。 +- **CPU调度**(CPU scheduling): 这存储有关进程优先级的信息并指向**交错队列**。(staggering queues)。 +- **优先级**(Priority:): 这定义了进程在获取 CPU 时的优先级 +- **当前状态**(Current state): 这表明进程是就绪(ready)、等待(waiting)还是正在运行(running) +- **CPU 注册表**(CPU registry): 这存储堆栈指针和其他信息。 + +### 定义进程状态 + +**进程**(processes)拥有跨越其生命周期的三种状态; 它们如下: + +- **运行状态**(Running): 该进程正在使用 CPU。 +- **就绪状态**(Ready): 在进程队列中等待的进程现在可以使用 CPU。 +- **等待状态**(Waiting): 进程正在等待与它执行的任务相关的一些 I/O 操作。 diff --git a/docs/chapter5/using_multiprocessing_to_compute_fibonacci_series_terms_with_multiple_inputs.md b/docs/chapter5/using_multiprocessing_to_compute_fibonacci_series_terms_with_multiple_inputs.md new file mode 100644 index 0000000..dfd6404 --- /dev/null +++ b/docs/chapter5/using_multiprocessing_to_compute_fibonacci_series_terms_with_multiple_inputs.md @@ -0,0 +1,165 @@ +# 使用多进程解决斐波那契序列多输入问题 + +下面我们将使用多进程解决多输入情况下的斐波那契数列问题,而不是之前我们使用的多线程的方法。 + +`multiprocessing_fibonacci.py` 代码使用了 `multiprocessing` 模块,为了运行,它导入了一些基本模块,我们可以在以下代码中观察到: + +```python +import sys, time, random, re, requests +import concurrent.futures +from multiprocessing import cpu_count, current_process, Manager +``` + +前面的章节中已经提到了一些导入; 尽管如此,以下某些导入确实值得特别注意: + +- **cpu_count**: 这是一个允许获取机器中 CPU 数量的函数。 +- **current_process**: 这是一个允许获取有关当前进程的信息的函数,例如,它的名称。 +- **Manager**: 这是一种允许通过代理在不同进程之间共享 `Python` 对象的类型。(更多信息参考:{target="_blank"}) + +按照代码,我们可以注意到第一个函数的行为有所不同; 它将在 0-14 次迭代期间以 1 到 20 的间隔生成随机值。 这些值将作为键插入到 `fibo_dict` 中,这是一个由 `Manager` 对象生成的字典。 + +!!! warning "" + + 使用消息传递的方法更为常见。 然而,在某些情况下,我们需要在不同进程之间共享一段数据,正如我们在 `fibo_dict` 字典中看到的那样。 + +接下来让我们一起来看`producer_task`方法,如下: + +```python +def producer_task(q, fibo_dict): + for i in range(15): + value = random.randint(1, 20) + fibo_dict[value] = None + + print("Producer [%s] putting value [%d] into queue.." % (current_process().name, value)) + q.put(value) +``` + +下一步是定义函数,该函数将为 `fibo_dict` 中的每个键计算斐波那契数列值。值得注意的是,与上一章中介绍的函数相关的唯一区别是使用 `fibo_dict` 作为参数以允许在不同进程使用它。 + +下面是`consumer_task`方法,如下: + +```python +def consumer_task(q, fibo_dict): + while not q.empty(): + value = q.get(True, 0.05) + a, b = 0, 1 + for item in range(value): + a, b = b, a+b + fibo_dict[value] = a + print("consumer [%s] getting value [%d] from queue..." % (current_process().name, value)) +``` + +为了进一步了解代码,我们看看程序的主代码块。在这个主代码块中,定义了以下一些变量: + +- **data_queue**: 该参数由`multiprocessing.Queueu`来创建,是进程安全的 +- **number_of_cpus**: 该参数由`multiprocessing.cpu_count`方法获得,获得机器cpu的个数 +- **fibo_dict**: 这个字典类型变量从`Manager`实例获得,保存多进程计算结果 + +在代码中,我们创建了一个名为 `producer` 的进程,以使用 `producer_task` 函数使用随机值填充 `data_queue`,如下所示: + +```python +producer = Process(target=producer_task, args=(data_queue, fibo_dict)) +producer.start() +producer.join() +``` + +我们可以注意到`Process`实例的初始化过程和我们之前的`Thread`实例初始化过程类似。初始化函数接收`target`参数作为进程中要执行的函数,和`args`参数作为`target`传入的函数的参数。接下来我们通过`start()`函数开始进程,然后使用`join()`方法,等待`producer`进程执行完毕。 + +在下一个块中,我们定义了一个名为 `consumer_list` 的列表,它将存储已初始化进程的消费者列表。 创建此列表的原因是仅在所有 `worker` 的进程开始后调用 `join()`。 如果为循环中的每个项目调用 `join()` 函数,那么只有第一个 `worker` 会执行该作业,因为下一次迭代将被阻塞,等待当前 `worker` 结束,最后下一个`worker`将没有其他要处理的内容; 以下代码代表了这种情况: + +```python +consumer_list = [] +number_of_cpus = cpu_count() + +for i in range(number_of_cpus): + consumer = Process(target=consumer_task, args=(data_queue, fibo_dict)) + consumer.start() + consumer_list.append(consumer) + +[consumer.join() for consumer in consumer_list] +``` + +最终,我们在 `fibo_dict` 中展示了迭代的结果,如下截图所示: + +```shell +$ python multiprocessing_fibonacci.py +Producer [Process-2] putting value [8] into queue.. +Producer [Process-2] putting value [10] into queue.. +Producer [Process-2] putting value [19] into queue.. +Producer [Process-2] putting value [6] into queue.. +Producer [Process-2] putting value [17] into queue.. +Producer [Process-2] putting value [18] into queue.. +Producer [Process-2] putting value [19] into queue.. +Producer [Process-2] putting value [17] into queue.. +Producer [Process-2] putting value [18] into queue.. +Producer [Process-2] putting value [4] into queue.. +Producer [Process-2] putting value [6] into queue.. +Producer [Process-2] putting value [7] into queue.. +Producer [Process-2] putting value [9] into queue.. +Producer [Process-2] putting value [4] into queue.. +Producer [Process-2] putting value [19] into queue.. +consumer [Process-4] getting value [8] from queue... +consumer [Process-6] getting value [6] from queue... +consumer [Process-9] getting value [19] from queue... +consumer [Process-13] getting value [6] from queue... +consumer [Process-11] getting value [4] from queue... +consumer [Process-4] getting value [9] from queue... +consumer [Process-8] getting value [18] from queue... +consumer [Process-10] getting value [18] from queue... +consumer [Process-3] getting value [19] from queue... +consumer [Process-5] getting value [10] from queue... +consumer [Process-6] getting value [4] from queue... +consumer [Process-7] getting value [17] from queue... +consumer [Process-14] getting value [17] from queue... +consumer [Process-12] getting value [7] from queue... +consumer [Process-9] getting value [19] from queue... +{8: 21, 10: 55, 19: 4181, 6: 8, 17: 1597, 18: 2584, 4: 3, 7: 13, 9: 34} +``` + +## 完整示例 + +译者注: + +```python +#coding: utf-8 +import sys, time, random +import concurrent.futures +from multiprocessing import cpu_count, current_process, Manager, Process, Queue + +def producer_task(q, fibo_dict): + for i in range(15): + value = random.randint(1, 20) + fibo_dict[value] = None + print("Producer [%s] putting value [%d] into queue.." % (current_process().name, value)) + q.put(value) + +def consumer_task(q, fibo_dict): + while not q.empty(): + value = q.get(True, 0.05) + a, b = 0, 1 + for item in range(value): + a, b = b, a+b + fibo_dict[value] = a + time.sleep(random.randint(1, 3)) # 由于现代计算机cpu处理太快,这里随机sleep几秒 + print("consumer [%s] getting value [%d] from queue..." % (current_process().name, value)) + +if __name__ == "__main__": + fibo_dict = Manager().dict() # 如果替换为 {}, 则没有共享对象的功能,打印出来将是空的。 + data_queue = Queue() + + producer = Process(target=producer_task, args=(data_queue, fibo_dict)) + producer.start() + producer.join() + + consumer_list = [] + number_of_cpus = cpu_count() + for i in range(number_of_cpus): + consumer = Process(target=consumer_task, args=(data_queue, fibo_dict)) + consumer.start() + consumer_list.append(consumer) + + [consumer.join() for consumer in consumer_list] + + print(fibo_dict) + +``` diff --git "a/docs/chapter5/\346\200\273\347\273\223.md" "b/docs/chapter5/\346\200\273\347\273\223.md" new file mode 100644 index 0000000..49f7791 --- /dev/null +++ "b/docs/chapter5/\346\200\273\347\273\223.md" @@ -0,0 +1,5 @@ +# 小结 + +本章中我们介绍了多进程的概念,并使用多进程解决了两个小问题,分别是并行计算斐波那契数列值和设计网络爬虫。 + +下一章节我们将使用`parallel Python`模块执行多进程任务,`parallel`模块并不是**python**的内部模块。我们还将学习进程间通信相关的知识,使用`pipes`在进程间通信。 diff --git a/docs/chapter6/discovering_pp.md b/docs/chapter6/discovering_pp.md new file mode 100644 index 0000000..701c242 --- /dev/null +++ b/docs/chapter6/discovering_pp.md @@ -0,0 +1,36 @@ +# 了解PP模块 + +上一节中,我们介绍了直接使用系统调用创建进程间通讯的方法,这是一种很低层的机制. 并且它只在Linux或Unix环境下才有效. 接下来我们会使用一个名为PP的python模块来创建IPC通讯,这种通讯不仅仅可以在本地进程间进行,还能通过计算机网络在物理上彼此分散的进程间进程. + +关于PP模块的文档并不多,可以在 中找到相关文档和FAQ. API提供了大量的使用该工具的说明. 十分的简洁明了 + +使用PP模块的最大优势在于它提供了一个抽象层. PP模块的重要特征如下所示: + +- 自动探测进程的数量并以此改进负载均衡 +- 可以在运行期改变要投入的处理器数量 +- 运行时负载均衡 +- 全网自动发现资源 + +PP模块有两种方式来执行并行代码. 第一种方式基于SMP架构,即在同一台机器上有多个处理器/核心. 第二中方式将网络中的各个机器配置,整合成区块,并将任务分配給这些机器去运行. 无论哪一种方式,进程间消息交换的过程都是抽象的. 这使得我们无需关系其低层的实现方式到底是通过管道还是socket. 我们只需要使用回调函数来通过参数和函数的方式来交换信息就行了. 下面给个例子. + +在PP的API中有一个名为Server的类,使用该类可以实现在本地和远程的进程间封装和分派任务. Server的构造函数(\__init\__)中有几个参数比较重要: + +- **ncpus**: 该参数用于指定执行任务的工作进程数量. 若没有指定该参数,则会自动根据机器上处理器/核心的数量来创建工作进程的总数,以优化资源的使用。 +- **ppservers**: 该参数是一个元组,该元组的元素为**并行Python执行服务器**(Parallel Python Execution Servers - PPES)的名称或IP地址. 一个PPES由连入网络的机器组成. 且该机器通过`ppsever.py`共组运行并等待待执行的任务. 其他参数的说明请参阅 + + `Server`类的实例拥有很多方法,其中`submit`方法允许我们分配任务到各个工作进程. `submit`函数具有如下签名: + + ```python + submit(self, func, args=(), depfuncs=(), modules=(), + callback=None, callbackargs=(), group='default', + globals=None) + ``` + +在`submit`方法中,我们主要关注以下几个参数: + +- **func**: 该函数会被本地进程或远程服务器执行。 +- **args**: 该参数提供了了执行`func`函数时的参数。 +- **modules**: 该参数说明远程代码(remote code)或进程为了调用func函数,需要导入哪些模块. 例如若被分配的函数用到了time模块, 则在元组参数中,传递方式必须为 `modules=('time',)` +- **callback** :这是执行完`func`函数后的回调函数,`func`的执行结果会作为其函数参数. 常用于对`func`的执行结果作进一步加工 + +还有其他的参数将会在下一节分析代码时进行介绍. diff --git a/docs/chapter6/index.md b/docs/chapter6/index.md new file mode 100644 index 0000000..9300ed6 --- /dev/null +++ b/docs/chapter6/index.md @@ -0,0 +1,10 @@ +# 使用并行 Python + +在上一章中,我们学习了如何使用 `multiprocessing` 和 `ProcessPoolExecutor` 模块来解决两个案例问题。 本章将介绍命名管道以及如何使用Parallel Python(PP)通过进程执行并行任务。 + +本章会覆盖下面几个知识点: + +* 理解进程间通信 +* 了解Parallel Python(PP) +* 在SMP架构上使用PP计算斐波那契序列 +* 使用PP创建分布式的网络爬虫 \ No newline at end of file diff --git a/docs/chapter6/understanding_interprocess_communication.md b/docs/chapter6/understanding_interprocess_communication.md new file mode 100644 index 0000000..404c423 --- /dev/null +++ b/docs/chapter6/understanding_interprocess_communication.md @@ -0,0 +1,169 @@ +# 理解进程间通讯 + +进程间通信 (Interprocess communication - IPC) 由允许在进程之间交换信息的机制组成。 + +有多种实现 IPC 的方法,通常,它们取决于为运行时环境选择的体系结构。 在某些情况下,例如,进程在同一台机器上运行,我们可以使用各种类型的通信,例如**共享内存**、**消息队列**和**管道**。 例如,当进程物理分布在集群中时,我们可以使用**套接字**(sockets)和**远程过程调用** (Remote Procedure Call - RPC)。 + +在第 5 章,使用 `Multiprocessing` 和 `ProcessPoolExecutor`,我们验证了常规管道的使用等。 我们还研究了具有共同父进程的进程之间的通信。 但是,有时需要在不相关的进程(具有不同父进程的进程)之间进行通信。 我们可能会问自己,不相关进程之间的通信是否可以通过它们的寻址空间来完成。 尽管如此,**一个进程永远不会从另一个进程访问寻址空间**。 因此,我们必须使用称为**命名管道**的机制。 + +## 探索命名管道 + +在 **POSIX** 系统中,例如 **Linux**,我们应该记住一切,绝对一切,都可以总结为文件。对于我们执行的每个任务,在某处都有一个文件,我们还可以找到一个附加到它的文件描述符,它允许我们操作这些文件。 + +!!! info "文件描述符" + + 文件描述符是允许用户程序访问文件以进行读/写操作的机制。 通常,文件由唯一的文件描述符引用。 有关文件描述符的更多信息,请访问{target="_blank"}(原文地址为: ,但找不到了,找到了一个替代描述文件描述符的。) + +**命名管道**(Named pipes)不过是允许通过使用与特殊文件相关联的文件描述符进行 IPC 通信的机制,例如,用于写入和读取数据的**先进先出** (FIFO) 方案。 命名管道与常规管道的不同之处在于它们管理信息的方法。 **命名管道**(Named pipes)使用文件系统中的文件描述符和特殊文件,而常规管道是在内存中创建的。 + +## 在python中使用命名管道 + +**命名管道**在 Python 中的使用非常简单,我们将通过实现两个执行单向通信的程序来说明这一点。 第一个程序名为`write_to_named_pipe.py`,其功能是在管道中写入一条22字节的消息,通知一个字符串和生成它的进程的PID。 第二个程序称为 `read_from_named_pipe.py`,它将读取消息并显示消息内容,添加其 PID。 + +在执行结束时,`read_from_named_pipe.py` 进程将显示一条形如"I pid [\] received a message => Hello from pid [the PID of writer process"的消息。 + +为了说明在命名管道中写入和读取进程之间的相互依赖性,我们将在两个不同的控制台中执行读取和写入。 但在此之前,让我们分析一下这两个程序的代码。 + +### 往命名管道写入数据 + +在 Python 中,命名管道是通过系统调用实现的。 在下面的代码中,我们将逐行解释 `write_to_named_pipe.py` 程序中代码的功能。 + +我们从 `os` 模块的输入开始,它将提供对系统调用的访问,我们将使用以下代码行: + +```python +import os +``` + +接下来我们会解释__main__代码块,在该代码块中创建了命名管道以及一个用于存储消息的FIFO的特殊文件. __main__代码块中的第一行代码定义了命名管道的标签. + +```python +named_pipe = "my_pipe" +``` + +接下来我们检查该命名管道是否已经存在,若不存在,则调用`mkfifo`系统调用来创建这个命名管道. + +```python +if not os.path.exists(named_pipe): + os.mkfifo(named_pipe) +``` + +`mkfifo`调用会创建一个特殊的文件,该文件对通过命名管道读写的消息实现了**FIFO**机制. + +我们再以一个命名管道和一个行如"Hello from pid [%d]"的消息来作为参数调用函数`write_message`. 该函数会将消息写入到(作为参数传递给它的)命名管道所代表的文件中. `write_message`函数定义如下: + +```python +def write_message(input_pipe, message): + fd = os.open(input_pipe, os.O_WRONLY) + os.write(fd, (message % str(os.getpid()))) + os.close(fd) +``` + +我们可以观察到,在函数的第一行,我们调用一个系统调用:`open`. 该系统调用若成功的话会返回一个文件描述符, 通过该文件描述符我们就能够读写那个`FIFO`文件中的数据. 请注意,我们可以通过`flags`参数控制打开**FIFO**文件的模式. 由于`write_message`函数紧紧需要写数据,因此我们使用如下代码: + +```python +fd = os.open(input_pipe, os.O_WRONLY) +``` + +在成功打开命名管道后,我们使用下面代码写入消息: + +```python +os.write(fd, (message % os.getpid())) +``` + +最后,请一定记着使用`close`关闭通讯渠道,这样才能释放被占用的计算机资源. + +```python +os.close(fd) +``` + +### 从命名管道读取数据 + +我们实现`read_from_pipe.py`来读取命名管道. 当然,改程序也需要借助`os`模块才能操作命名管道. 改程序的主要代码很简单: 首先,我们定义了所使用命名管道的标签,该标签需要与写进程所用的命名管道同名. + +```python +named_pipe = "my_pipe" +``` + +然后,我们调用`read_message`函数,该函数会读取`write_to_named_pipe.py`写入的内容. `read_message`函数的定义如下: + +```python +# 此处原文应该有错 + +def read_message(input_pipe): + fd = os.open(input_pipe, os.O_RDONLY) + message = "I pid [%d] received a message => %s" % (os.getpid(), os.read(fd, 22)) + os.close(fd) + return message +``` + +`open`调用不需要再介绍。 这里的新事物是我们的读取调用,它以字节为单位执行数量的读取。 在我们的例子中,如果给出了文件描述符,它就是 `22` 个字节。 消息被读取后,由函数返回。 最后,必须执行`close`调用以关闭通信通道。 + +!!! info "" + + 要验证已打开文件描述符的有效性。需要由用户处理在使用文件描述符和命名管道时产生的相关异常。 + +最终,下面的截屏显示了`write_to_named_pip`和`read_from_named_pipe`程序的执行结果. + +```shell +>$ python write_to_named_pipe.py + +``` + +```shell +>$ python read_from_pipe.py +I pid [61032] received a message => Hello 61017 +``` + +## 完整示例 + +译者注: + +```python +# write_to_named_pip.py + +import os +import sys + + +def write_message(input_pipe, message): + fd = os.open(input_pipe, os.O_WRONLY) + os.write(fd, (message % str(os.getpid())).encode()) # 管道通信为字节,这里需要转码 + os.close(fd) + + +if __name__ == "__main__": + named_pipe = "my_pipe" + + if not os.path.exists(named_pipe): + os.mkfifo(named_pipe) + + write_message(named_pipe, "Hello %s") + +``` + +```python +# read_from_named_pipe.py + +import os +import sys + + +def read_message(input_pipe): + fd = os.open(input_pipe, os.O_RDONLY) + message = "I pid [%d] received a message => %s" % ( + os.getpid(), + os.read(fd, 22).decode(), # 管道通信为字节,这里需要转码 + ) + os.close(fd) + return message + + +if __name__ == "__main__": + named_pipe = "my_pipe" + + if not os.path.exists(named_pipe): + os.mkfifo(named_pipe) + + print(read_message(named_pipe)) + +``` diff --git a/docs/chapter6/using_pp_to_calculate_the_fibonacci_series_term_on_smp_architecture.md b/docs/chapter6/using_pp_to_calculate_the_fibonacci_series_term_on_smp_architecture.md new file mode 100644 index 0000000..1ec2ba0 --- /dev/null +++ b/docs/chapter6/using_pp_to_calculate_the_fibonacci_series_term_on_smp_architecture.md @@ -0,0 +1,121 @@ +# 在SMP架构上使用pp模块计算斐波那契序列 + +是时候开始行动了! 让我们解决涉及在 SMP 架构中使用 PP 的多个输入的斐波那契数列的案例研究。 我正在使用配备双核处理器和四个线程的笔记本电脑。 + +我们将为这个实现只导入两个模块,`os` 和 `pp`。`os` 模块将仅用于获取正在执行的进程的 `PID`。 我们将有一个名为 `input_list` 的列表,其中包含要计算的值和一个用于对结果进行分组的字典,我们将其称为 `result_dict`。 然后,我们转到代码块如下: + +```python +import os, pp + +input_list = [4, 3, 8, 6, 10] +result_dict = {} +``` + +然后,我们定义一个名为 `fibo_task` 的函数,它将由**并行进程**执行。 它将是我们通过 `Server` 类的提交方法传递的 `func` 参数。 该函数与前几章相比没有重大变化,除了现在通过使用元组封装参数中接收的值以及包含 `PID` 和计算的 `Fibonacci` 项的消息来完成返回。 看看下面的完整函数: + +```python +def fibo_task(value): + a, b = 0, 1 + for item in range(value): + a, b = b, a + b + message = "the fibonacci calculated by pid %d was %d" % (os.getpid(), a) + + return (value, message) +``` + +下一步是定义我们的回调函数,我们将其称为 `aggregate_results`。 一旦 `fibo_task` 函数返回其执行结果,就会调用回调函数。 它的实现非常简单,只显示一条状态消息,随后在 `result_dict` 中生成一个输入,其中包含传递给 `fibo_dict` 函数的值作为键,结果是计算斐波那契项的过程返回的消息。 以下代码是`aggregate_results`函数的完整实现: + +```python +def aggregate_results(result): + print "Computing results with PID [%d]" % os.getpid() + + result_dict[result[0]] = result[1] +``` + +现在,我们有两个函数要定义。 我们必须创建一个 `Server` 类的实例来分派任务。 以下代码行创建一个服务器实例: + +```python +job_server = pp.Server() +``` + +在前面的示例中,我们使用标准值作为参数。 在下一节中,我们将使用一些可用的参数。 + +现在我们有了 `Server` 类的一个实例,让我们迭代 `input_list` 的每个值,通过提交调用分派 `fibo_task` 函数,将需要导入的模块作为参数传递给 `args` 元组中的输入值,以便函数 正确执行并且回调注册 `aggregate_results`。 参考以下代码块: + +```python +for item in input_list: + job_server.submit(fibo_task, (item,), modules=('os',), + callback=aggregate_results) +``` + +最后,我们必须等到所有派发的任务结束。 因此,我们可以使用`Server`类的`wait`方法,如下: + +```python +job_server.wait() +``` + +!!! info "" + + 除了使用回调函数之外,还有另一种方法可以获得已执行函数的返回值。 `submit` 方法返回一个对象类型,`pp._Task`,当执行结束时,它包含了执行的结果。 + +最后,我们将通过字典迭代打印条目的结果,如下所示: + +```python +print "Main process PID [%d]" % os.getpid() +for key, value in result_dict.items(): + print "For input %d, %s" % (key, value) +``` + +以下屏幕截图说明了程序的输出: + +```shell +$ python feibonacci_pp_smp.py +Computing results with PID [21058] +Computing results with PID [21058] +Computing results with PID [21058] +Computing results with PID [21058] +Computing results with PID [21058] +Main process PID [21058] +For input 4, the fibonacci calculated by pid 21059 was 3 +For input 3, the fibonacci calculated by pid 21060 was 2 +For input 6, the fibonacci calculated by pid 21062 was 8 +For input 8, the fibonacci calculated by pid 21061 was 21 +For input 10, the fibonacci calculated by pid 21065 was 55 +``` + +## 完整示例 + +译者注: `feibonacci_pp_smp.py` + +```python +import os, pp + +input_list = [4, 3, 8, 6, 10] +result_dict = {} + +def fibo_task(value): + a, b = 0, 1 + for item in range(value): + a, b = b, a + b + message = "the fibonacci calculated by pid %d was %d" % (os.getpid(), a) + + return (value, message) + +def aggregate_results(result): + print("Computing results with PID [%d]" % os.getpid()) + + result_dict[result[0]] = result[1] + +job_server = pp.Server() + +for item in input_list: + job_server.submit(fibo_task, (item,), modules=('os',), # 这里增加新的模块需要添加进来。 + callback=aggregate_results) + +job_server.wait() + +print("Main process PID [%d]" % os.getpid()) + +for key, value in result_dict.items(): + print("For input %d, %s" % (key, value)) +``` diff --git a/docs/chapter6/using_pp_to_make_a_distributed_web_crawler.md b/docs/chapter6/using_pp_to_make_a_distributed_web_crawler.md new file mode 100644 index 0000000..bf8415e --- /dev/null +++ b/docs/chapter6/using_pp_to_make_a_distributed_web_crawler.md @@ -0,0 +1,147 @@ +# 使用PP创建分布式网络爬虫 + +现在我们已经使用 `PP` 并行执行代码来调度本地进程,现在是验证代码是否以分布式方式执行的时候了。 为此,我们将使用以下三台不同的机器: + +- Iceman-Thinkpad-X220: Ubuntu 13.10 +- Iceman-Q47OC-500P4C: Ubuntu 12.04 LTS +- Asgard-desktop: Elementary OS + +我们将在如上列举的三台机器上测试pp组件在分布式环境下的使用。对此,我们实现了分布式网络爬虫。`web_crawler_pp_cluster.py`方法中,将`input_list`列举的URL分发到本地以及远端进程执行,`web_crawler_pp_cluster.py`中的回调函数将组织这些URL以及以及通过它们找到的前三个连接(URL)并进行分组。 + +让我们逐步分析代码以了解如何找到解决此问题的方法。 首先,我们将`import`必要的模块并定义要使用的数据结构。 与上一节一样,我们将创建一个 `input_list` 和一个包含最终处理结果的字典`result_dict`。 参考以下代码: + +```python +import os, re, requests, pp + +ppurl_list = ['http://www.google.com/','http://gizmodo.uol.com.br/', + 'https://github.com/', 'http://br.search.yahoo.com/', + 'http://www.python.org/','http://www.python.org/psf/'] + +result_dict = {} +``` + +现在,我们的 `aggregate_results` 函数将再次成为我们的回调函数,与斐波那契项的示例相比变化不大。 我们只是更改了要插入字典中的消息的格式,以及这个回调的返回将是一个包含执行它的进程的 `PID`、执行它的主机名和找到的前三个链接组成的元组。 参考`aggregate_results`函数如下: + +```python +def aggregate_results(result): + print("Computing results in main process PID [%d]" % os.getpid()) + message = "PID %d in hostname [%s] the following links were found: %s" % (result[2], result[3], result[1]) + result_dict[result[0]] = message +``` + +下一步是定义 `crawl_task` 函数,它将由 `Server` 类的实例调度。 该功能与前面章节中介绍的功能类似,旨在收集作为参数接收的 `URL` 显示的页面中的现有链接。 唯一的区别是返回是一个元组。 参考以下代码: + +```python +def crawl_task(url): + html_link_regex = re.compile('') + request_data = requests.get(url) + + links = html_link_regex.findall(request_data.text)[:3] + return (url, links, os.getpid(), os.uname()[1]) +``` + +在`main`方法和`callback`方法定义之后,我们需要初始化`Server`类实例,以至于能够在分布式环境下执行网络爬虫任务。我们注意到`pp.Server`类有三个参数,第一个参数是执行`Server`类方法的`IP`或`hostname`,我们的例子中,除了本机之外,还需要定义另外两台机器的`IP`和`hostname`,定义如下所示: + +```python +ppservers = ("192.168.25.21", "192.168.25.9") +``` + +!!! info "" + + 如果您不想通知并希望自动发现可用于接收任务的机器,请在 `ppservers` 元组中使用 `*` 字符串。 + +定义标识服务器的元组。 我们将创建一个 `Server` 实例,如下所示: + +```python +job_dispatcher = pp.Server(ncpus=1, ppservers=ppservers, socket_timeout=60000) +``` + +值得注意的是,与前面的示例相比有一些变化。 首先,我们将值 `1` 传递给了 `ncpus` 参数。 这将导致 `PP` 创建单个本地进程,并在必要时将其他任务分派给远程机器。 定义的第二个参数是我们在上一步中创建的服务器的元组。 最后,我们为通信中使用的套接字定义了一个超时值,仅用于测试目的。 目标是避免网络超时而关闭通道。 + +创建 `Server` 类的实例后,就可以分派我们的函数来执行了。 让我们遍历每个 `URL` 并将它们传递给 `Server` 实例的`submit`方法,如下所示: + +```python +for url in ppurl_list: + job_dispatcher.submit(crawl_task, (url,), + modules=('os', 're', 'requests',), + callback=aggregate_results) +``` + +与之前计算斐波那契数列的示例相比,重要的变化是发送执行所需的模块。 + +!!! info "" + + 你一定在想为什么元组模块中没有传`PP`模块。 很简单; PP 执行环境已经为我们做了这个导入。 毕竟,它需要在远程节点中执行此操作。 + +为了完成我们的并行和分布式网络爬虫,我们必须等到执行结束才能显示它们的输出。 请注意,最后,`Server` 类的 `print_stats` 方法中有一个新元素,它显示了一些有趣的执行统计信息,如下所示: + +```python +job_dispatcher.wait() + +print "\nMain process PID [%d]\n" % os.getpid() +for key, value in result_dict.items(): + print "** For url %s, %s\n" % (key, value) + job_dispatcher.print_stats() +``` + +在执行程序之前,我们需要在远程机器上初始化 `ppserver.py` 实用程序; `ppserver.py –a –d` 是这里使用的命令,其中 `–a` 是自动发现的选项,允许未指定 `IP` 地址的客户端找到服务器。 另一个参数是 `-d`,它通过日志显示有关服务器活动如何执行的信息。 + +让我们按以下顺序可视化输出: + +1. 首先,以下屏幕截图显示了主节点中创建和分发任务的阶段: + ![1](../imgs/6-02.png) +2. 然后,初始化 `ppservers.py` 服务器并在以下屏幕截图中看到处理任务(从 iceman-Q47OC500P4C 的 `ppserver.py` 输出和从 `asgard-desktop` 的 `ppserver.py` 输出)。 +3. 在前面的屏幕截图中,值得注意的是统计信息带来了有趣的信息,例如分配到不同目的地的任务数量、每个任务的时间以及每个目的地的总数。 前面屏幕截图中的另一个相关点是回调函数仅在主进程中执行,即调度任务中的那些。 因此,切记不要让回调任务过重,因为它们可能会消耗主节点的过多资源; 这显然取决于每个案例的具体情况。 +4. 下面的截图是的`debug`模式下`ppserver.py`脚本在`iceman-Q470C-500P4C`机器上的的执行日志。 + + ![1](../imgs/6-03.png) + +5. 下面截图是`debug`模式下`ppserver.py`脚本在`asgard-desktop`机器上的执行日志。 + ![1](../imgs/6-04.png) + +## 完整示例 + +译者注: 由于机器性能原因或网络原因,未能还原作者执行场景。 + +`fibonacci_pp_cluster.py` + +```python +import os, re, requests, pp + + +ppurl_list = ['http://www.google.com/','http://gizmodo.uol.com.br/', + 'https://github.com/', 'http://br.search.yahoo.com/', + 'http://www.python.org/','http://www.python.org/psf/'] + +result_dict = {} + +def aggregate_results(result): + print("Computing results in main process PID [%d]" % os.getpid()) + message = "PID %d in hostname [%s] the following links were found: %s" % (result[2], result[3], result[1]) + result_dict[result[0]] = message + +def crawl_task(url): + html_link_regex = re.compile('') + request_data = requests.get(url) + + links = html_link_regex.findall(request_data.text)[:3] + + return (url, links, os.getpid(), os.uname()[1]) + + +ppservers = ("192.168.25.21", "192.168.25.9") + +job_dispatcher = pp.Server(ncpus=1, ppservers=ppservers, socket_timeout=60000) + +for url in ppurl_list: + job_dispatcher.submit(crawl_task, (url,), + modules=('os', 're', 'requests', ), + callback=aggregate_results) + +job_dispatcher.wait() + +print("\nMain process PID [%d]\n" % os.getpid()) +for key, value in result_dict.items(): + print("** For url %s, %s\n" % (key, value)) + job_dispatcher.print_stats() # 显示了一些有趣的执行统计信息 +``` diff --git "a/docs/chapter6/\346\200\273\347\273\223.md" "b/docs/chapter6/\346\200\273\347\273\223.md" new file mode 100644 index 0000000..36c338c --- /dev/null +++ "b/docs/chapter6/\346\200\273\347\273\223.md" @@ -0,0 +1,6 @@ + +# 小结 + +我们研究了使用更低级资源在进程之间建立没有直接关系的进程间通信。 此外,我们还了解了使用 `PP` 模块,这有助于我们抽象本地进程(包括分布式进程)之间的通信。 `PP` 是构建简单、小型、并行和分布式 Python 应用程序的便捷工具。 + +在下一章中,我们将学习如何使用名为 `Celery` 的模块以并行和分布式的方式执行任务。 \ No newline at end of file diff --git a/docs/chapter7/defining_queues_by_task_types.md b/docs/chapter7/defining_queues_by_task_types.md new file mode 100644 index 0000000..2d1e56f --- /dev/null +++ b/docs/chapter7/defining_queues_by_task_types.md @@ -0,0 +1,46 @@ +# 根据任务类型定义队列 + +负责计算 `Fibonacci` 的任务已实现并正在运行。 我们可以看到所有任务都被发送到 `Celery` 的默认队列中。 但是,有多种方法可以将任务路由到不同的队列; 让我们在服务器端重构我们的架构,并从客户端实现所谓的路由任务。 我们将为每种类型的任务指定队列。 + +在服务器端启动`Celery`服务器的那一刻,我们会建立三个不同的队列。 这些现在将被worker看到和消费。 `Fibonacci` 任务的队列是 `fibo_queue`,平方根任务的 `sqrt_queue`,Web 爬虫任务的 `webcrawler_queue`。 但是,将任务分开有什么好处呢? 让我们列举一些: + +- 它将相同类型的任务分组,使它们的监控更容易 +- 它定义了专用于消费特定队列的`worker`,从而提高了性能 +- 它在具有性能更好的机器上建立任务更重的队列 + +!!! info "" + + 前面几点本书不会解释,但我们可以通过初始化 Celery 服务器甚至在网络中分配具有专用队列的代理来实现负载平衡。 我建议您使用 Celery 尝试这种集群风格。 + +要在服务器中设置队列,我们只需要使用以下命令启动 `Celery`: + +```shell +$celery –A tasks –Q sqrt_queue,fibo_queue,webcrawler_queue worker --loglevel=info +``` + +下图是在服务端截图: + +```log +-------------- [queues] + .> fibo_queue exchange=fibo_queue(direct) key=fibo_queue + .> sqrt_queue exchange=sqrt_queue(direct) key=sqrt_queue + .> webcrawler_queue exchange=webcrawler_queue(direct) key=webcrawler_queue +``` + +在转到下一个示例之前,让我们将现有任务的发送路由到它们的队列。 在服务器端,在 `task_dispatcher.py` 模块中,我们将更改 `send_task` 调用,以便下次分派任务时,它们将被定向到不同的队列。 我们现在将按如下方式更改 `sqrt_task` 调用: + +```python +app.send_task('tasks.sqrt_task', args=(value,), queue='sqrt_queue', routing_key='sqrt_queue') +``` + +然后,修改`fibo_task`调用,如下: + +```python +app.send_task('tasks.fibo_task', args=(x,), queue='fibo_queue', routing_key='fibo_queue') +``` + +!!! info "" + + 如果有兴趣监控队列、统计任务数量或者其他,请参考`Celery`文档 {target="_blank"}。 + + 在任何情况用`Redis`,`redis-cli`都可以作为一个工具。队列、任务、workders都可以被监控,详见 {target="_blank"}. diff --git a/docs/chapter7/dispatching_a_simple_task.md b/docs/chapter7/dispatching_a_simple_task.md new file mode 100644 index 0000000..ab82f03 --- /dev/null +++ b/docs/chapter7/dispatching_a_simple_task.md @@ -0,0 +1,175 @@ +# 分发简单任务 + +在之前,我们已经建立好环境。下面测试一下环境,发送一个计算平方根的任务。 + +定义任务模块`tasks.py`。在开始,导入必须的模块。 + +```python +from math import sqrt +from celery import Celery +``` + +然后,创建`Celery`实例,代表客户端应用: + +```python +app = Celery('tasks', broker='redis://192.168.25.21:6379/0') +``` + +在初始化时我们传入了模块的名称和`broker`的地址。 + +然后,启动`result backend`,如下: + +```python +app.config.CELERY_RESULT_BACKEND = 'redis://192.168.25.21:6379/0' + +# 较新的版本(v5.2.7)直接填充在celery app的初始化参数中. +app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') +``` + +用`@app.tack`装饰器定义任务: + +```python +@app.task +def sqrt_task(value): + return sqrt(value) +``` + +到此,我们完成了`tasks.py`模块的定义,我们需要初始化服务端的`workers`。我们创建了一个单独的目录叫做`8397_07_broker`。拷贝`tasks.py`模块到这个目录,运行如下命令: + +```shell +$celery –A tasks worker –-loglevel=INFO +``` + +上述命令初始化了**Clery Server**,`—A`代表`Celery`应用。下图是初始化的部分截图 + +```shell +$# celery -A tasks worker --loglevel=INFO +/opt/celery_env/lib/python3.9/site-packages/celery/platforms.py:840: SecurityWarning: You're running the worker with superuser privileges: this is +absolutely not recommended! + +Please specify a different user using the --uid option. + +User information: uid=0 euid=0 gid=0 egid=0 + + warnings.warn(SecurityWarning(ROOT_DISCOURAGED.format( + + -------------- celery@ch1.nauu.com v5.2.7 (dawn-chorus) +--- ***** ----- +-- ******* ---- Linux-3.10.0-957.el7.x86_64-x86_64-with-glibc2.17 2023-03-06 16:12:10 +- *** --- * --- +- ** ---------- [config] +- ** ---------- .> app: tasks:0x7fe5cbea9b80 +- ** ---------- .> transport: redis://localhost:6379/0 +- ** ---------- .> results: redis://localhost/0 +- *** --- * --- .> concurrency: 2 (prefork) +-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker) +--- ***** ----- + -------------- [queues] + .> celery exchange=celery(direct) key=celery + + +[tasks] + . tasks.square_root + +[2023-03-06 16:12:10,866: INFO/MainProcess] Connected to redis://localhost:6379/0 +[2023-03-06 16:12:10,871: INFO/MainProcess] mingle: searching for neighbors +[2023-03-06 16:12:11,897: INFO/MainProcess] mingle: all alone +[2023-03-06 16:12:11,929: INFO/MainProcess] celery@ch1.nauu.com ready. +``` + +现在,**Celery Server**等待接收任务并且发送给`workers`。 + +下一步就是在客户端创建应用调用`tasks`。 + +!!! info "" + + 上述步骤不能忽略,因为下面会用在之前创建的东西。 + +在客户端机器,我们有**celery_env**虚拟环境,现在创建一个`task_dispatcher.py`模块很简单,如下步骤; + +1. 导入logging模块来显示程序执行信息,导入Celery模块: + + ```python + import logging + from celery import Celery + ``` + +2. 下一步是创建Celery实例,和服务端一样: + + ```python + #logger configuration... + app = Celery('tasks', broker='redis://192.168.25.21:6379/0') + app.conf.CELERY_RESULT_BACKEND = 'redis://192.168.25.21:6397/0' + ``` + +由于我们在接下的内容中要复用这个模块来实现任务的调用,下面我们创建一个方法来封装`sqrt_task(value)`的发送,我们将创建`manage_sqrt_task(value)`方法: + +```python +def manage_sqrt_task(value): + result = app.send_task('tasks.sqrt_task', args=(value,)) + logging.info(result.get()) +``` + +从上述代码我们发现客户端应用不需要知道服务端的实现。通过**Celery**类中的`send_task`方法,我们传入`module.task`格式的字符串和以元组的方式传入参数就可以调用一个任务。最后,我们看一看`log`中的结果。 +在`__main__`中,我们调用了`manage_sqrt_task(value)`方法: + +```python +if __name__ == '__main__': + manage_sqrt_task(4) +``` + +下面的截图是执行`task_dispatcher.py`文件的结果: + +```shell +[2023-03-06 16:18:45,481: INFO/MainProcess] Task tasks.sqrt_task[3ecab729-f1cb-4f29-bb47-b713b2e563ed] received +[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 +``` + +在客户端,通过`get()`方法得到结果,这是通过`send_task()`返回的`AsyncResult`实例中的重要特征。结果如下图: + +```shell +$# python task_dispatcher.py +2023-03-06 16:26:05,841 - 2.0 +``` + +## 完整案例 + +`tasks.py` + +```python +from math import sqrt +from celery import Celery + +app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') + + +@app.task +def sqrt_task(value): + return sqrt(value) +``` + +`task_dispatcher.py` + +```python +import logging +from celery import Celery + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(message)s') + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +ch.setFormatter(formatter) +logger.addHandler(ch) + +app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') + +def manage_sqrt_task(value): + result = app.send_task('tasks.sqrt_task', args=(value,)) + logger.info(result.get()) + + +if __name__ == '__main__': + print(manage_sqrt_task(4)) +``` diff --git a/docs/chapter7/index.md b/docs/chapter7/index.md new file mode 100644 index 0000000..61ca8c2 --- /dev/null +++ b/docs/chapter7/index.md @@ -0,0 +1,12 @@ +# 使用Celery分发任务 + +在上一章中,我们了解并使用了**并行 Python**(parallel Python)。 我们看到了案例研究的实施,包括斐波那契数列项和使用并行 Python 模块的 Web 爬虫。 我们学习了如何使用管道在进程之间建立通信,以及如何在网络中的不同机器之间分配进程。 在本章中,我们将研究如何使用 Celery 框架在网络中的不同机器之间分发任务。 + +在本章中,我们将讨论以下主题: + +- 理解 Celery +- 理解 Celery 的架构 +- 搭建环境 +- 分派一个简单的任务 +- 使用 Celery 获取斐波那契数列项 +- 使用 Celery 制作分布式网络爬虫 diff --git a/docs/chapter7/setting_up_the_environment.md b/docs/chapter7/setting_up_the_environment.md new file mode 100644 index 0000000..4f397e0 --- /dev/null +++ b/docs/chapter7/setting_up_the_environment.md @@ -0,0 +1,108 @@ +# 建立环境 + +在本节中,我们将在 `Linux` 中设置两台机器。 第一个,主机名 `foshan`,将执行客户端角色,应用程序 `Celery` 将在其中调度要执行的任务。 另一台主机名为 `Phoenix` 的机器将执行**代理**(broker)、**结果后端**(result backend)和worker使用的队列的角色。 + +## 配置客户端机器 + +让我们开始设置客户端机器。 在这台机器上,我们将使用 `pyvenv` 工具设置一个 `Python 3.3` 的虚拟环境。 `pyvenv` 的目标是不使用额外的模块污染操作系统中存在的 `Python`,而是将每个项目所需的开发环境分开。 我们将执行以下命令来创建我们的虚拟环境: + + ```shell + $pyvenv celery_env + ``` + +上述命令在当前路径创建一个名为`celery_env`的文件夹,里面包含所有Python开发环境必须的结构。下图是该目录所包含的内容: + +```shell +# 这里使用的最新的python venv模块 +$# ./Python-3.9.14/python -m venv celery_env +$# ls celery_env/ +bin include lib lib64 pyvenv.cfg +``` + +在创建了虚拟环境之后,我们就可以开始工作并安装需要使用的包。然而,首先我们得激活这个环境,执行以下命名: + +```shell +$# source celery_env/bin/activate +``` + +当命令行提示符改变了,例如在左边出现`celery_env`,就说明激活完成。所有你安装的包都只在这个目录下有效,而不是在整个系统中有效。 + +```shell +(celery_env) $# ls celery_env/ +bin include lib lib64 pyvenv.cfg +``` + +!!! info "" + + 用`--system-site-packages`标识可以创建能够访问系统`site-packages`的虚拟环境,但是不推荐使用。 + +现在,我们有一个虚拟环境,假设已经安装好了`setuptools`或者`pip`。下面为客户端安装必须的包,如下命令: + +```shell +$pip install celery +``` + +下图是已经安装好的framework v3.1.9,将在本书中使用该版本。 + +```shell +# 由于当前(2023)python2已不再支持,顾这里安装的最新版本v5.2.7 +(celery_env) $# python +Python 3.9.14 (main, Sep 19 2022, 12:04:09) +[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux +Type "help", "copyright", "credits" or "license" for more information. +>>> import celery +>>> celery.VERSION +version_info_t(major=5, minor=2, micro=7, releaselevel='', serial='') +>>> +``` + +现在我们要在**Celery**中安装支持的**Redis**,这样客户端就可以通过`broker`传输消息了。用如下命令: + +```shell +$pip install celery[redis] +``` + +现在我们的客户端环境配置好了,在开始编码之前,我们必须配置好服务器端的环境。 + +## 配置服务器 + +为了配置服务器,我们首先安装**Redis**,**Redis**将作为`broker`和`result backend`。使用如下命令: + +```shell +$sudo apt-get install redis-server +``` + +启动Redis: + +```shell +$redis-server +``` + +如果成功,会出现类似下图中的输出 + +```log +2905:C 06 Mar 15:53:46.571 * supervised by systemd, will signal readiness + _._ + _.-``__ ''-._ + _.-`` `. `_. ''-._ Redis 3.2.12 (00000000/0) 64 bit + .-`` .-```. ```\/ _.,_ ''-._ + ( ' , .-` | `, ) Running in standalone mode + |`-._`-...-` __...-.``-._|'` _.-'| Port: 6379 + | `-._ `._ / _.-' | PID: 2905 + `-._ `-._ `-./ _.-' _.-' + |`-._`-._ `-.__.-' _.-'_.-'| + | `-._`-._ _.-'_.-' | http://redis.io + `-._ `-._`-.__.-'_.-' _.-' + |`-._`-._ `-.__.-' _.-'_.-'| + | `-._`-._ _.-'_.-' | + `-._ `-._`-.__.-'_.-' _.-' + `-._ `-.__.-' _.-' + `-._ _.-' + `-.__.-' + +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. +2905:M 06 Mar 15:53:46.574 # Server started, Redis version 3.2.12 +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. +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. +2905:M 06 Mar 15:53:46.574 * The server is now ready to accept connections on port 6379 +``` diff --git a/docs/chapter7/understanding_celery.md b/docs/chapter7/understanding_celery.md new file mode 100644 index 0000000..fa7c92d --- /dev/null +++ b/docs/chapter7/understanding_celery.md @@ -0,0 +1,18 @@ +# 理解Celery + +**Celery** 是一个框架,该框架提供机制来简化构建分布式系统的过程。 Celery 框架通过在作为网络互连的机器或本地网络之间交换消息来使用**工作单元**(tasks)分布的概念。 任务是 **Celery** 中的关键概念; 我们必须分发的任何类型的工作都必须事先封装在任务中。 + +## 为什么使用Celery + +它以透明的方式在分布在 Internet 上的工作人员或本地工作人员之间分配任务 + +Celery有如下优点: + +- 它以透明的方式在网络上分布的worker或本地网络之间分配任务 +- 它通过设置(进程、线程、Gevent、Eventlet)以一种简单的方式改变了worker的并发性 +- 支持同步、异步、周期、定时任务 +- 它会在出现错误时重新执行任务 + +!!! info "" + + 很多开发者都认为**同步任务**(synchronous tasks)和**实时任务**(real-time tasks)是一样的,实际上它们是完全不同的。对于**实时任务**,它有一个时间窗口,任务执行必须在`Deadline`之前完成。如果经过分析,任务在时间窗口内完成不了,那么它将被终止或者暂停直到下次能够完成,而**同步任务**是当任务执行完后才返回结果。 diff --git a/docs/chapter7/understanding_celery_architecture.md b/docs/chapter7/understanding_celery_architecture.md new file mode 100644 index 0000000..d4ee3b7 --- /dev/null +++ b/docs/chapter7/understanding_celery_architecture.md @@ -0,0 +1,60 @@ +# 理解Celery架构 + +Celery架构基于**可插拔组件**(pluggable components)和根据选择的**消息传输**(代理)(message transport(broker))协议实现的消息交换机制。下图说明了这一点: + +![1](../imgs/7-01.png) + +现在,让我们详细的介绍Celery的每个组件。 + +## 处理任务 + +在上图中的*client*组件,有创建和分派任务到brokers的方法。 + +分析如下示例代码来演示通过使用`@app.task`装饰器来定义一个任务,它可以被一个**Celery**应用的实例访问,下面代码展示了一个简单的`Hello World app`: + +```python + @app.task + def hello_world(): + return "Hello I'm a celery task" +``` + +!!! info "" + + 任何可执行的方法或对象都可以成为任务 (Any callable can be a task.) + +正如我们前面提到的,有几种类型的任务:`同步`、`异步`、`定期`和`计划`。 当我们执行任务调用时,它会返回一个 `AsyncResult` 类型的实例。 `AsyncResult` 对象是一个对象,它允许检查任务状态、它的结束,并且很明显,它在存在时返回。 但是,要使用此机制,另一个组件(结果后端)必须处于活动状态。 这将在本章中进一步解释。 要分派任务,我们应该使用任务的以下一些方法: + +- `delay(arg, kwarg=value)` : 这是调用 `apply_async` 方法的快捷方式。 +- `apply_async((arg,), {'kwarg': value})` : 这允许为任务的执行设置一系列有趣的参数。 其中一些如下: + - `countdown` : 默认任务是立即执行,该参数设置经过`countdown`秒之后执行。 + - `expires` : 代表经过多长时间终止。 + - `retry` : 此参数决定在连接或发送任务失败的情况下,是否必须重新发送。 + - `queue` : 该任务所处的任务队列。 + - `serializer` : 这表示磁盘中任务序列化的数据格式,一些示例包括 json、yaml 等。 + - `link` : 如果发送的任务成功执行,这将链接一个或多个要执行的任务。 + - `link_error` : 这将在任务执行失败的情况下链接一个或多个要执行的任务。 +- `apply((arg,), {'kwarg': value})` : 这会以同步方式在本地进程中执行任务,从而阻塞直到结果准备就绪为止。 + +!!! info "" + + Celery 还提供了伴随任务状态的机制,这对于跟踪和映射处理的真实状态非常有用。 有关内置任务状态的更多信息,请访问{target="_blank"} + +## 理解消息转发(broker) + +`broker`绝对是 **Celery** 中的关键组成部分。 通过它,我们可以发送和接收消息并与`worker`沟通。 **Celery** 支持大量的代理。 然而,对于其中一些,并不是所有的 `Celery` 机制都得到了实现。 就功能而言最完整的是 `RabbitMQ` 和 `Redis`。 在本书中,我们将使用 `Redis` 作为`broker`和结果后端。 `broker`的功能是在发送任务的客户端应用程序和执行任务的工作线程之间提供一种通信方式。 这是通过使用任务队列完成的。 我们可以有几台带有代理的网络机器等待接收消息以供`workers`使用。 + +## 理解workers + +`Workers`负责执行接收到的任务。**Celery**提供了一系列的机制,我们可以选择最合适的方式来控制`workers`的行为。这些机制如下: + +- **并发模式**(Concurrency mode):例如**进程**、**线程**、**协程**(Eventlet)和**Gevent**。 +- **远程控制**(Remote control):使用这种机制,可以通过高优先级队列发送消息到某个特定的`worker`来改变行为,包括在**运行时**(runtime)。 +- **撤销任务**(Revoking tasks):使用这种机制,我们可以指示一个或多个`worker`忽略一个或多个任务的执行。 + +如果需要,可以在运行时设置甚至更改更多功能。 比如`worker`在一段时间内执行的任务数,`worker`从哪个`queue`中消耗的时间最多等等。 有关`worker`的更多信息,请访问{target="_blank"} + +## 理解result backends + +**结果后端**(result backend)组件的作用是存储返回给客户端应用程序的任务的状态和结果。 从 `Celery` 支持的结果后端,比较出彩的有 `RabbitMQ`、`Redis`、`MongoDB`、`Memcached` 等。 前面列出的每个**结果后端**(result backend)都有优点和缺点。 有关详细信息,请参阅 {target="_blank"}。 + +现在,我们对 `Celery` 架构及其组件有了一个大致的了解。 因此,让我们建立一个开发环境来实现一些例子。 diff --git a/docs/chapter7/using_celery_to_make_a_distributed_web_crawler.md b/docs/chapter7/using_celery_to_make_a_distributed_web_crawler.md new file mode 100644 index 0000000..a615b62 --- /dev/null +++ b/docs/chapter7/using_celery_to_make_a_distributed_web_crawler.md @@ -0,0 +1,178 @@ +# 用Celery来构建一个分布式网络爬虫系统 + +现在我们将用`Celery`构建网络爬虫。我们已经有了`webcrawler_queue`,负责`hcrawler`任务。然而,在服务器端,我们将在`tasks.py`模块创建`crawl_task`任务。 + +首先,导入`re`(正则表达式)和`requests`(HTTP lib)模块,代码如下: + +```python +import re +import requests +``` + +然后,定义正则表达式,和之前的章节一样; + +```python +hTML_link_regex = re.compile('') +``` + +然后,替换`crawl_task`方法,添加`@app.task`装饰器,修改返回信息,如下: + +```python +@app.task +def crawl_task(url): + request_data = requests.get(url) + links = html_link_regex.findall(request_data.text) + message = "The task %s found the following links %s.." %(url, links) + return message +``` + +`links`列表不一定要和下图匹配: + +![1](../imgs/7-12.png) + +然后让我们再次向上滚动 `Celery` 并查看。 此时,随着我们的新任务加载,是时候在客户端的 `task_dispatcher.py` 模块中实现名为 `crawl_task` 的任务了。 + +首先,我们需要列出数据的输入`url_list`。代码如下: + +```python +url_list = ['http://www.baidu.com', + 'http://cn.bing.com', + 'http://www.qq.com', + 'http://www.github.com', + ] +``` + +创建`manage_crawl_task`方法。 + +```python +def manage_crawl_task(url_list): + async_result_dict = {url: app.send_task('tasks.crawl_task', + args=(url,), queue='webcrawler_queue', + routing_key='webcrawler_queue') for url in url_list} + for key, value in async_result_dict.items(): + if value.ready(): + logger.info("%s -> %s" % (key, value.get())) + else: + logger.info("The task [%s] is not ready" % value.task_id) +``` + +和之前创建的`manage_fibo_task`方法一样,`async_result_dict`字典包含当前URL和`AsyncResult`结果。然后我们检查任务的状态获取任务结果。 + +现在我们在`__main__`中调用该方法: + +```python + +if __name__ == '__main__': + #manage_sqrt_task(4) + #manage_fibo_task(input_list) + manage_crawl_task(url_list) +``` + +运行`task_dispatcher.py`代码,在服务器端有如下输出: + +```shell + +``` + +![1](../imgs/7-13.png) + +最后,客户端的输出如下: + +![1](../imgs/7-14.png) + +**Celery**是一个强大的工具,在本章我们只是用到了基本的东西。更多的内容建议自己在真实的项目中动手去尝试。 + +## 完整案例 + +`tasks.py` + +```python +from math import sqrt +from celery import Celery +import re +import requests + +app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') +# app.config.CELERY_RESULT_BACKEND = 'redis://192.168.99.89:6379/0' + + +@app.task +def sqrt_task(value): + return sqrt(value) + +@app.task +def fibo_task(value): + a, b = 0,1 + for item in range(value): + a, b = b, a + b + message = "The Fibonacci calculated with task id %s was %d" % (fibo_task.request.id, a) + return (value, message) + +html_link_regex = re.compile('') + +@app.task +def crawl_task(url): + request_data = requests.get(url) + links = html_link_regex.findall(request_data.text) + message = "The task %s found the following links %s.." %(url, links) + + return message +``` + +`tasks_dispatcher.py` + +```python +import logging +from celery import Celery +from celery.result import AsyncResult +from typing import Dict + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(message)s') + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +ch.setFormatter(formatter) +logger.addHandler(ch) + +app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') + +def manage_sqrt_task(value): + result = app.send_task('tasks.sqrt_task', args=(value,), queue='sqrt_queue', routing_key='sqrt_queue') + logger.info(result.get()) + + + +def manage_fibo_task(value_list): + 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} + + for key, value in async_result_dict.items(): + if value.ready(): + logger.info("Value [%d] -> %s" % (key, value.get()[1])) + else: + logger.info("Task [%s] is not ready" % value.task_id) + + +def manage_crawl_task(url_list): + 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} + for key, value in async_result_dict.items(): + if value.ready(): + logger.info("%s -> %s" % (key, value.get())) + else: + logger.info("The task [%s] is not ready" % value.task_id) + +url_list = [ + 'http://www.baidu.com', + 'http://cn.bing.com', + 'http://www.qq.com', + 'http://www.github.com', + 'http://br.search.yahoo.com' +] + +if __name__ == '__main__': + input_list = [4, 3, 8, 6, 10] + # print(manage_sqrt_task(4)) + # print(manage_fibo_task(input_list)) + print(manage_crawl_task(url_list)) +``` diff --git a/docs/chapter7/using_celery_to_obtain_a_fibonacci_series_term.md b/docs/chapter7/using_celery_to_obtain_a_fibonacci_series_term.md new file mode 100644 index 0000000..f666ee2 --- /dev/null +++ b/docs/chapter7/using_celery_to_obtain_a_fibonacci_series_term.md @@ -0,0 +1,190 @@ +# 使用 Celery 获取斐波那契数列项 + +让我们再次去分配我们的多个输入,以计算第 `n` 个斐波那契项,每个项都以分布式方式计算。 计算 `Fibonacci` 的函数相对于前面的章节会有一些变化。 变化很小; 现在我们有了 `@app.task` 装饰器和返回消息中的一个小改动。 + +在代理所在的服务器计算机中的 `tasks.py` 模块(之前创建)中,我们将停止执行 `Celery(Ctrl + C`)并添加 `fibo_task` 任务。 这是通过使用以下代码完成的: + +```python +@app.task +def fibo_task(value): + a, b = 0,1 + for item in range(value): + a, b = b, a + b + message = "The Fibonacci calculated with task id %s was %d" % (fibo_task.request.id, a) + return (value, message) +``` + +通过`ask.reaquest.id`得到任务的ID,请求对象是`task`的对象,`task`对象提供了`task`执行的上下文。通过上下文可以得到`task`的ID等信息。 + +在`tasks.py`模块加入了新的任务之后,再一次初始化**Celery**,结果如下图: + +```log +- *** --- * --- .> concurrency: 2 (prefork) +-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker) +--- ***** ----- + -------------- [queues] + .> celery exchange=celery(direct) key=celery + + +[tasks] + . tasks.fibo_task + . tasks.sqrt_task + +[2023-03-06 17:05:34,402: INFO/MainProcess] Connected to redis://localhost:6379/0 +``` + +现在我们把`fibo_task`任务装载到**Celery server**,我们将在客户端实现对该任务的调用。 + +在`task_dispatcher.py`模块,我们会申明`input_list`,如下: + +```python +input_list = [4, 3, 8, 6, 10] +``` + +和前面的做法一样,定义`manage_fibo_task`方法: + +正如我们在上一节创建的 `sqrt_task` 任务中所做的那样,我们将创建一个方法来组织我们的调用而不污染 `__main__` 块。 我们将此函数命名为 `manage_fibo_task`。 如以下实现: + +```python +def manage_fibo_task(value_list): + async_result_dict = {x: app.send_task('tasks.fibo_task', + args=(x,)) for x in value_list} + + for key, value in async_result_dict.items(): + logger.info("Value [%d] -> %s" % (key, value.get()[1])) +``` + +在 `manage_fibo_task` 函数中,我们创建了一个名为 `async_result_dict` 的字典,填充相同的键值对。 `key` 是作为参数传递的项,用于获取 `Fibonacci` 的无数项,`value` 是从调用 `send_task` 方法返回的 `AsyncResult` 的实例。 通过这种方法,我们可以监控任务的状态和结果。 + +最后,遍历字典得到输入值和输出结果并封装成字典。`AsyncResult`类的`get()`函数可以让我们获取处理结果。 + +`get()`方法会阻塞进程。一个好的方法是调用`ready()`方法来检查结果是否返回了。 + +可能会注意到 `get()` 函数可能不会立即返回结果,因为处理仍在进行。 在客户端调用 `get()` 方法可以阻止调用之后的处理。 将调用结合到 `ready()` 方法是个好主意,这样可以检查是否准备好获取结果。 + +因此,结果展示循环可以修改为如下代码: + +```python +for key, value in async_result_dict.items(): + if value.ready(): + logger.info("Value [%d] -> %s" % (key, value.get()[1])) + else: + logger.info("Task [%s] is not ready" % value.task_id) +``` + +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. + +根据要执行的任务类型,结果可能会有相当长的延迟。 因此,通过调用 `get()` 而不考虑返回状态,我们可以阻止代码运行在 `get()` 函数被调用的地方。 为了解决这个问题,我们应该在 `get(timeout=x)` 方法中定义一个名为 `timeout` 的参数。 因此,通过最小化这种阻塞,我们可以防止任务在返回结果时出现问题,这会无限期地影响执行的运行。 + +最后,我们添加了对 `manage_fibo_task` 函数的调用,作为参数传递给我们的 `input_list`。 代码如下: + +```python +if __name__ == '__main__': + #manage_sqrt_task(4) + manage_fibo_task(input_list) +``` + +当我们执行`task_dispatcher.py`中的代码时,可以在旁边看到如下输出服务器: + +```shell +$# python task_dispatcher.py +2023-03-06 17:20:38,902 - Value [4] -> The Fibonacci calculated with task id 03328f4d-8226-4a15-853d-8b8ab5833b72 was 3 +2023-03-06 17:20:38,904 - Value [3] -> The Fibonacci calculated with task id 4ea527de-0a96-4c6c-ac25-8e4a01a0e919 was 2 +2023-03-06 17:20:38,906 - Task [448d8127-763c-4025-84a1-e05e9979841a] is not ready +2023-03-06 17:20:38,909 - Task [f639a24f-cbf5-403d-b243-4c5c54f5b77a] is not ready +2023-03-06 17:20:38,909 - Task [4e2999a7-bae8-454c-9f70-8b513dd0844e] is not ready +``` + +在客户端有如下输出: + +```shell +-- ******* ---- .> task events: OFF (enable -E to monitor tasks in this worker) +--- ***** ----- + -------------- [queues] + .> celery exchange=celery(direct) key=celery + + +[tasks] + . tasks.fibo_task + . tasks.sqrt_task + +[2023-03-06 17:20:35,572: INFO/MainProcess] Connected to redis://localhost:6379/0 +[2023-03-06 17:20:35,578: INFO/MainProcess] mingle: searching for neighbors +[2023-03-06 17:20:36,599: INFO/MainProcess] mingle: all alone +[2023-03-06 17:20:36,616: INFO/MainProcess] celery@ch1.nauu.com ready. +[2023-03-06 17:20:38,859: INFO/MainProcess] Task tasks.fibo_task[03328f4d-8226-4a15-853d-8b8ab5833b72] received +[2023-03-06 17:20:38,877: INFO/MainProcess] Task tasks.fibo_task[4ea527de-0a96-4c6c-ac25-8e4a01a0e919] received +[2023-03-06 17:20:38,884: INFO/MainProcess] Task tasks.fibo_task[448d8127-763c-4025-84a1-e05e9979841a] received +[2023-03-06 17:20:38,890: INFO/MainProcess] Task tasks.fibo_task[f639a24f-cbf5-403d-b243-4c5c54f5b77a] received +[2023-03-06 17:20:38,896: INFO/MainProcess] Task tasks.fibo_task[4e2999a7-bae8-454c-9f70-8b513dd0844e] received +[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') +[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') +[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') +[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') +[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') +``` + +## 完整示例 + +`tasks.py` + +```python +from math import sqrt +from celery import Celery + +app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') +# app.config.CELERY_RESULT_BACKEND = 'redis://192.168.99.89:6379/0' + + +@app.task +def sqrt_task(value): + return sqrt(value) + +@app.task +def fibo_task(value): + a, b = 0,1 + for item in range(value): + a, b = b, a + b + message = "The Fibonacci calculated with task id %s was %d" % (fibo_task.request.id, a) + return (value, message) +``` + +`tasks_dispatcher.py` + +```python +import logging +from celery import Celery +from celery.result import AsyncResult +from typing import Dict + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +formatter = logging.Formatter('%(asctime)s - %(message)s') + +ch = logging.StreamHandler() +ch.setLevel(logging.DEBUG) +ch.setFormatter(formatter) +logger.addHandler(ch) + +app = Celery('tasks', broker='redis://localhost/0', backend='redis://localhost/0') + +def manage_sqrt_task(value): + result = app.send_task('tasks.sqrt_task', args=(value,)) + logger.info(result.get()) + + + +def manage_fibo_task(value_list): + async_result_dict: Dict[int, AsyncResult] = {x: app.send_task('tasks.fibo_task',args=(x,)) for x in value_list} + + for key, value in async_result_dict.items(): + if value.ready(): + logger.info("Value [%d] -> %s" % (key, value.get()[1])) + else: + logger.info("Task [%s] is not ready" % value.task_id) + +if __name__ == '__main__': + input_list = [4, 3, 8, 6, 10] + # print(manage_sqrt_task(4)) + print(manage_fibo_task(input_list)) +``` diff --git "a/docs/chapter7/\346\200\273\347\273\223.md" "b/docs/chapter7/\346\200\273\347\273\223.md" new file mode 100644 index 0000000..f1e37a3 --- /dev/null +++ "b/docs/chapter7/\346\200\273\347\273\223.md" @@ -0,0 +1,5 @@ +# 小结 + +在本章中,我们讨论了 `Celery` 分布式任务队列。 我们还通过示意图透视了它的架构,分析了它的关键组件,并了解到如何设置环境以使用 `Celery` 构建基本应用程序。 如果单独讲`Celery`可以写一本书,通过本章希望你对`Celery`有了一个基本的认识。 + +在下一章中,我们将学习 `asyncio` 模块以及如何以异步方式执行程序。 我们还将简要介绍`协程`(coroutines),并学习如何将它们与 `asyncio` 一起使用。 diff --git a/docs/chapter8/index.md b/docs/chapter8/index.md new file mode 100644 index 0000000..c901f7f --- /dev/null +++ b/docs/chapter8/index.md @@ -0,0 +1,9 @@ +# 异步编程 + +在上一章中,我们学习了如何使用 Celery 框架分发任务以及在通过网络连接的不同机器上并行计算。 现在,我们将探索异步编程、事件循环和协程,它们是 Python 3.4 版中可用的 `asyncio` 模块中的特色资源。 我们还将学习将它们与执行器结合使用。 + +在本章中,我们将介绍: + +- 阻塞、非阻塞和异步操作 +- 了解事件循环 +- 使用`asyncio`(异步)框架 diff --git "a/docs/chapter8/\344\275\277\347\224\250asyncio.md" "b/docs/chapter8/\344\275\277\347\224\250asyncio.md" new file mode 100644 index 0000000..b9e02a8 --- /dev/null +++ "b/docs/chapter8/\344\275\277\347\224\250asyncio.md" @@ -0,0 +1,203 @@ +# 使用asyncio + +我们可以将 `asyncio` 定义为一个模块,用于重启 Python 中的异步编程。 `asyncio` 模块允许使用以下元素的组合来实现异步编程: + +- **Event loop**: 这已在上一节中定义。 `asyncio` 模块允许每个进程有一个事件循环。 +- **Coroutines(协程)**: 正如`asyncio`官方文档中所说,“**协程是一种遵循一定约定的生成器**”。 它最有趣的特性是它可以**在执行期间挂起**以等待外部处理(I/O 中的某些例程)完成, 并在外部处理完成后又可以从原来的位置恢复执行。 +- **Futures**: `asyncio` 模块定义了自己的对象 `Future`。 `Futures` 代表一个尚未完成的处理过程。 +- **Tasks**: 这是 `asyncio.Future` 的子类,用于**封装**和**管理**协程。 + +除了这些机制之外,`asyncio` 还为应用程序的开发提供了一系列其他功能,例如传输和协议,它们允许使用 `TCP`、`SSL`、`UDP` 和`管道`等通过通道进行通信。 有关 `asyncio` 的更多信息,请访问 {target="_blank"}。 + +## 理解coroutines和futures + +为了能够在`asyncio`中定义一个`coroutine`,我们使用`@asyncio.coroutine`装饰器,并且我们必须利用`yield from`语法来暂停`coroutine`,以便执行一个I/O操作或者其他可能阻塞**事件循环**的计算。但是这种暂停和恢复的机制是如何工作的呢?`Coroutine`与`asyncio.Future`对象一起工作。我们可以把这个操作总结如下: + +- 初始化协程,并在内部实例化一个 `asyncio.Future` 对象或将其作为参数传递给协程。 +- 在到达使用 `yield from` 的协程点时,协程将暂停以等待在 `yield from` 中引发的计算。 `yield from instance` 等待 `yield from` 的构造。 +- 当 `yield from` 中引发的计算结束后,协程执行协程相关的 `asyncio.Future` 对象的 `set_result()` 方法,告诉事件循环可以恢复协程。 + +!!! info "" + + 当我们使用 `asyncio.Task` 对象封装协程时,我们不需要显式使用 `asyncio.Future` 对象,因为 `asyncio.Task` 对象已经是 `asyncio.Future` 的子类。 + +## 使用coroutine和asyncio.Future + +下面是使用`coroutine`和`asyncio.Future`对象的一些例子: + +```python +import asyncio + +@asyncio.coroutine +def sleep_coroutine(f): + yield from asyncio.sleep(2) + f.set_result("Done!") +``` + +在上述代码中,我们定义了名为 `sleep_coroutine` 的协程,它接收一个 `asyncio.Future`对象 作为参数。 在`sleep_coroutine`中,我们的协程将运行 `asyncio.sleep(2)` 导致暂停执行并休眠 2 秒; 我们必须观察到 `asyncio.sleep` 函数已经与 `asyncio` 兼容。 因此,它作为未来返回; 然而,由于教学原因,我们包含了作为参数传递的 `asyncio.Future` 对象,以说明如何通过 `asyncio.Future.set_result()` 在协程中显式完成恢复。 + +最终,我们有了我们的主函数,我们在其中创建了我们的 `asyncio.Future` 对象,并在 `loop = asyncio.get_event_loop()` 行中,我们从 `asyncio` 创建了一个事件循环实例来执行我们的协程,如下代码所示: + +```python +if __name__ == '__main__': + future = asyncio.Future() + loop = asyncio.get_event_loop() + loop.run_until_complete(sleep_coroutine(future)) +``` + +!!! info "" + + 任务和协程仅在**事件循环**(event loop)执行时执行。 + +在最后一行,`loop.run_until_complete(sleep_coroutine(future))`,我们要求我们的事件循环一直运行直到我们的协程完成它的执行。 这是通过 `BaseEventLoop` 类中提供的 `BaseEventLoop.run_until_complete` 方法完成的。 + +!!! info "" + + 在 `asyncio` 中恢复协程的魔法在于 `asyncio.Future` 对象的 `set_result` 方法。 所有要恢复的协程都需要等待`asyncio.Future`执行`set_result`方法。 所以,`asyncio` 的事件循环会知道计算已经结束,它可以恢复协程。 + +## 使用asyncio.Task + +如前所述,`asyncio.Task` 类是 `asyncio.Future` 的子类,旨在管理协程。 让我们检查一个名为 `asyncio_task_sample.py` 的示例代码,其中将创建多个 `asyncio.Task` 对象并在 `asyncio` 的事件循环中分派以执行: + +```python +import asyncio + +@asyncio.coroutine +def sleep_coro(name, seconds=1): + print("[%s] coroutine will sleep for %d second(s)…" % (name, seconds)) + yield from asyncio.sleep(seconds) + print("[%s] done!" % name) +``` + +我们的协程称为 `sleep_coro`,将接收两个参数:`name`,它将用作我们协程的标识符,以及标准值为 1 的`seconds`,它将指示协程将暂停多少秒。 + +在主函数中,我们定义了一个列表,其中包含三个类型为 `asyncio.Task` 的对象,名为 `Task-A`,它将休眠 `10` 秒,以及 `Task-B` 和 `Task-C`,它们将分别休眠 `1` 秒。 请参见以下代码: + +```python +if __name__ == '__main__': + tasks = [asyncio.Task(sleep_coro('Task-A', 10)), + asyncio.Task(sleep_coro('Task-B')), + asyncio.Task(sleep_coro('Task-C')) + ] + loop = asyncio.get_event_loop() + loop.run_until_complete(asyncio.gather(*tasks)) +``` + +!!! info "由于学习时,python 3已更新到3.10.2 固如下写法也可以" + + ```python + # python3.10.2 版如下, 使用async、await 关键字 + import asyncio + + async def sleep_coro(name, seconds=1): + print("[%s] coroutine will sleep for %d second(s)…" % (name, seconds)) + await asyncio.sleep(seconds) + print("[%s] done!" % name) + + + if __name__ == "__main__": + tasks = [ + asyncio.Task(sleep_coro("Task-A", 10)), + asyncio.Task(sleep_coro("Task-B")), + asyncio.Task(sleep_coro("Task-C")), + ] + loop = asyncio.get_event_loop() + loop.run_until_complete(asyncio.gather(*tasks)) + ``` + +仍然在主函数中,我们使用 `BaseEventLoop` 定义事件循环。 `run_until_complete` 函数; 然而,这个函数接收的参数不超过一个协程,而是一个对 `asyncio.gather` 的调用,它是作为未来返回的函数,附加接收到的协程或未来列表的结果作为参数。 `asyncio_task_sample.py` 程序的输出如以下屏幕截图所示: + +![1](../imgs/7-15.png) + +值得注意的是,程序的输出按照声明的顺序显示正在执行的任务; 但是,它们都不能阻止事件循环。 这是因为 `Task-B` 和 `Task-C` 睡眠较少并且在 `Task-A` 睡眠 `10` 倍并首先被调度之前结束。 `Task-A` 阻塞事件循环的场景是灾难性的。 + +## 使用与asyncio不兼容的库 + +`asyncio` 模块在 `Python` 社区中仍然是最新的。 一些库仍然不完全兼容。 让我们重构上一节示例 `asyncio_task_sample.py` 并将函数从 `asyncio.sleep` 更改为 `time.sleep`。 在不作为未来返回的时间模块中休眠并检查其行为。 我们将 `yield from asyncio.sleep(seconds)` 行更改为 `yield from time.sleep(seconds)`。我们显然需要导入时间模块来使用新的睡眠。 运行该示例,请注意以下屏幕截图中显示的输出中的新行为: + +![1](../imgs/7-16.png) + +我们可以注意到协程正常初始化,但是由于 `yield from` 语法等待协程或 `asyncio.Future` 而发生错误,并且 `time.sleep` 在其结束时没有生成任何东西。 那么,在这些情况下我们应该如何处理呢? 答案很简单: 我们需要一个 `asyncio.Future` 对象,然后重构我们的示例。 + +首先,让我们创建一个函数,该函数将创建一个 `asyncio.Future` 对象以将其返回到 `sleep_coro` 协程中的 `yield from present`。 `sleep_func`函数如下: + +```python +def sleep_func(seconds): + f = asyncio.Future() + time.sleep(seconds) + f.set_result("Future done!") + return f +``` + +请注意,`sleep_func` 函数在结束时会执行 `f.set_result("Future done!")` 在 `future cause` 中放置一个虚拟结果,因为此计算不会生成具体结果; 它只是一个睡眠功能。 然后,返回一个 `asyncio.Future` 对象,`yield from` 期望它恢复 `sleep_coro` 协程。 以下屏幕截图说明了修改后的 `asyncio_task_sample.py` 程序的输出: + +![1](../imgs/7-17.png) + +现在所有已分派的任务都执行无误。 可是等等! 上一个屏幕截图中显示的输出仍然有问题。 请注意,执行顺序内部有些奇怪,因为 **Task-A** 休眠了 10 秒,并在随后两个仅休眠 1 秒的任务开始之前结束。 也就是说,我们的事件循环被任务阻塞了。 这是使用不与 asyncio 异步工作的库或模块的结果。 + +解决此问题的一种方法是将阻塞任务委托给 `ThreadPoolExecutor`(请记住,如果处理受 **I/O** 限制,则此方法效果很好;如果受 **CPU** 限制,请使用 `ProcessPoolExecutor`。为了我们的舒适,`asyncio` 以一种非常简单的方式支持此机制. 让我们再次重构我们的 `asyncio_task_sample.py` 代码,以便在不阻塞**事件循环**的情况下执行任务。 + +首先,我们必须删除 `sleep_func` 函数,因为它不再是必需的。 对 `time.sleep` 的调用将由 `BaseEventLoop.run_in_executor` 方法完成。然后让我们按照以下方式重构我们的 `sleep_coro` 协程: + +```python +@asyncio.coroutine +def sleep_coro(name, loop, seconds=1): + future = loop.run_in_executor(None, time.sleep, seconds) + + print("[%s] coroutine will sleep for %d second(s)..." % (name, seconds)) + yield from future + print("[%s] done!" % name) +``` + +!!! info "由于学习时,python 3已更新到3.10.2 固如下写法也可以" + + ```python + # python3.10.2 版如下, 使用async、await 关键字 + import asyncio + + async def sleep_coro(name, loop, seconds=1): + future = loop.run_in_executor(None, time.sleep, seconds) + print("[%s] coroutine will sleep for %d second(s)…" % (name, seconds)) + await future + print("[%s] done!" % name) + + + if __name__ == "__main__": + loop = asyncio.get_event_loop() + tasks = [ + asyncio.Task(sleep_coro2("Task-A", loop, 10)), + asyncio.Task(sleep_coro2("Task-B", loop)), + asyncio.Task(sleep_coro2("Task-C", loop)), + ] + + loop.run_until_complete(asyncio.gather(*tasks)) + ``` + +值得注意的是,协程接收到一个新参数,该参数将是我们在主函数中创建的事件循环,以便使用 `ThreadPoolExecutor` 来响应相同的执行结果。 + +在这之后,我们有下一行: + +```python +future = loop.run_in_executor(None, time.sleep, seconds) +``` + +在上一行中,调用了 `BaseEventLoop.run_in_executor` 函数,它的第一个参数是一个[执行器](https://docs.python.org/3.4/library/concurrent.futures.html#concurrent.futures.Executor)。 如果它通过 `None`,它将使用 `ThreadPoolExecutor` 作为默认值。 第二个参数是一个回调函数,在本例中是 `time.sleep` 函数,代表我们要完成的计算,最后我们可以传递回调参数。 + +请注意,`BaseEventLoop.run_in_executor` 方法返回一个 `asyncio.Future` 对象。 然而,通过返回的 `future` 调用 `yield from` 就足够了,并且我们的协程已经准备好了。 + +记住,我们需要改变程序的主函数,将**事件循环传**(event loop)递给`sleep_coro`。 + +```python +if __name__ == '__main__': + loop = asyncio.get_event_loop() + tasks = [asyncio.Task(sleep_coro('Task-A', loop, 10)), + asyncio.Task(sleep_coro('Task-B', loop)), + asyncio.Task(sleep_coro('Task-C', loop))] + loop.run_until_complete(asyncio.gather(*tasks)) loop.close() +``` + +让我们看看下面截图中显示的重构后的代码执行情况。 + +![1](../imgs/7-18.png) + +我们得到了它!结果是一致的,事件循环没有被`time.sleep`函数的执行阻塞。 diff --git "a/docs/chapter8/\346\200\273\347\273\223.md" "b/docs/chapter8/\346\200\273\347\273\223.md" new file mode 100644 index 0000000..115d886 --- /dev/null +++ "b/docs/chapter8/\346\200\273\347\273\223.md" @@ -0,0 +1,11 @@ +# 小结 + +在本章节,我们学习了异步、阻塞、非阻塞编程。为了了解这些行为,我们使用`asyncio`模块的基本机制写了一些例子。 + +`asyncio`模块是对`python`异步编程进行革命的一个尝试。Guido Van Rossum 在探索性选择和提取基本机制为这些选择提供清晰的API方面非常成功。`yield from`语法产生是为了增强一些使用协程的程序的表现力,使程序员免去写回调函数的负担。除此之外,`asyncio`模块拥有与其它应用程序集成的能力。 + +快到本书的结束了,写这本书还是很有挑战性的,希望它对你有所帮助。本书中有很多东西没有介绍,比如 `IPython`, `mpi4py`, `Greenlets`, `Eventlets`, 等等。 + +基于本书提供的内容,你可以自己做实验比较不同的工具。几乎在本书的每一个章节都用了两个相关例子来介绍,这也说明了Python可以在不改变核心代码的基础上灵活的替换不同的工具。 + +我们已经学习了一些全局解释器锁 (**GIL**) 和一些绕过 **GIL** 副作用的变通方法。 相信主要的 Python 实现(**CPython**)不会解决与 **GIL** 相关的问题; 只有未来才能揭示这一点。 **GIL** 是 **Python** 社区中一个困难且反复出现的话题。 另一方面,我们有 **PyPy** 实现,它带来了 **JIT** 和其他性能改进。 如今,**PyPy** 团队正在尝试将软件事务内存 (**STM**) 用于 **PyPy**,旨在移除 **GIL**。 diff --git "a/docs/chapter8/\347\220\206\350\247\243\344\272\213\344\273\266\345\276\252\347\216\257.md" "b/docs/chapter8/\347\220\206\350\247\243\344\272\213\344\273\266\345\276\252\347\216\257.md" new file mode 100644 index 0000000..1a231ff --- /dev/null +++ "b/docs/chapter8/\347\220\206\350\247\243\344\272\213\344\273\266\345\276\252\347\216\257.md" @@ -0,0 +1,57 @@ +# 理解事件循环 + +为了理解事件循环的概念,我们需要理解构成其内部结构的元素。 + +我们将使用术语**资源描述符**来代指**套接字描述符**和**文件描述符**。 + +## 轮询函数 + +轮询技术由不同的操作系统实现,旨在监视一个或多个**资源描述符**的状态,且轮询技术由系统功能负责实现。 **轮询函数**构成了事件循环的基础,我们经常发现这些模型被称为**准备就绪通知方案(RN - Readiness Notification)**,因为轮询功能通知对事件感兴趣的程序,同时**资源描述符**也已准备好进行交互; 然而,感兴趣的程序可能会或不会完成所需的操作。 + +例如,在 Linux 方面,我们有以下轮询函数: + +- `select()`: *POSIX* 实现存在一些缺点,如下所示: + + - 要监视的**资源描述符**数量有限制。 + - 复杂度为 `O(n)`,其中 `n` 表示连接的客户端数量,这使得服务器无法同时处理多个客户端。 + +- `poll()`: `select()`的增强版,有以下特点: + + - 允许监视更大范围的资源描述符 + - 和`select()`一样的`O(n)`时间复杂度 + - 允许更多类型的监控事件 + - 和`select()`相比比,可以复用**entry**数据 + +- `epoll()`: 这是一个强大的 Linux 实现,具有恒定时间复杂度 `O(1)` 的吸引人的特性。 `epoll()` 函数提供了两种行为来通过 [epoll_wait()][epoll_wait]{target="_blank"} 调用来监视事件。 为了定义这两种行为,让我们想象这样一种场景:**生产者**在套接字(具有关联的**套接字描述符**)中写入数据,而**消费者**等待完成数据读取: + - **水平触发**(Level-triggered):当消费者完成对 `epoll_wait()` 的调用时,它将立即获得该资源描述符的状态并返回给请求的事件,指示执行读取操作的可能性(在我们的例子中)。 因此,水平触发的行为与事件的状态直接相关,而不是事件本身。 + - **边缘触发**(Edge-triggered):只有当套接字中的写入事件结束并且数据可用时,对 `epoll_wait()` 的调用才会返回。 因此,在边缘触发的行为中,重点是事件本身已经发生,而不是执行任何事件的可能性。 + +!!! info "" + + 在其他平台上,也有可用的轮询功能,例如用于 BSD 和 Mac OS X 的 `kqueue`。 + + 轮询函数对于创建具有可以并发方式管理多个操作的单个线程的应用程序很有用。 例如,[Tornado Web 服务器](http://www.tornadoweb.org/en/stable/overview.html){target="_blank"}是使用非阻塞 I/O 编写的,作为轮询功能,它分别支持 `epoll` 和 `kqueue for Linux` 和 `BSD/Mac OS X`。 + +轮询函数工作步骤如下: + +1. 一个`poller`对象被创建. +2. 我们可以在`poller`中注册或不注册一个或多个资源描述符。 +3. 轮询函数在创建的`poller`对象中执行。 + +!!! info "" + + `Poller`是一个提供使用轮询方法的抽象接口 + +## 使用事件循环 + +我们可以将事件循环定义为简化版的使用轮询函数监视事件的抽象。 在内部,事件循环使用`poller`对象,消除了程序员控制添加、删除和控制事件的任务的责任。 + +事件循环,一般来说,利用回调函数来处理事件的发生; 例如,给定一个资源描述符A,当A中发生写事件时,会有一个回调函数。 下面列举一些用Python实现事件循环的应用示例: + +- Tornado web server ( {target="_blank"} ) +- Twisted ( {target="_blank"} ) +- asyncio ( {target="_blank"} ) +- Gevent ( {target="_blank"} ) +- Eventlet ( {target="_blank"} ) + +[epoll_wait]: http://refspecs.linux-foundation.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/libc-epoll-wait-1.html diff --git "a/docs/chapter8/\347\220\206\350\247\243\351\230\273\345\241\236\351\235\236\351\230\273\345\241\236\345\222\214\345\274\202\346\255\245\346\223\215\344\275\234.md" "b/docs/chapter8/\347\220\206\350\247\243\351\230\273\345\241\236\351\235\236\351\230\273\345\241\236\345\222\214\345\274\202\346\255\245\346\223\215\344\275\234.md" new file mode 100644 index 0000000..422a893 --- /dev/null +++ "b/docs/chapter8/\347\220\206\350\247\243\351\230\273\345\241\236\351\235\236\351\230\273\345\241\236\345\222\214\345\274\202\346\255\245\346\223\215\344\275\234.md" @@ -0,0 +1,29 @@ +# 理解阻塞非阻塞和异步操作 + +了解任务执行的不同方法对于建模和构思可扩展的解决方案极为重要。 了解何时使用**异步**、**阻塞**和**非阻塞**操作可以对系统的响应时间产生巨大的影响。 + +## 理解阻塞操作 + +在阻塞操作的情况下,我们用在银行柜台接待客户的情境来举例子。 当呼叫特定客户的号码时,收银员的所有注意力都集中在这个特定的客户身上。 在满足当前特定客户的需求之前,收银员不能同时接待另一个客户。 现在,考虑到这一点,想象一家只有两名收银员的银行机构,当每小时有 100 名顾客涌入时; 然后我们就会面临一个处理流程的问题。 这个例子说明了处理的阻塞操作,当一个任务需要等待另一个任务结束时,阻塞了其对资源的访问。 + +!!! info "" + + 在处理阻塞操作时,请求者阻塞结果直到它的请求被完成。 + +## 理解非阻塞操作 + +一般情况下很容易将非阻塞操作与异步操作混淆; 但是,它们是不同的概念,但他们却可以很好地协同工作,并且经常以这种方式使用。 让我们再用一个现实世界的情境来说明这种情况。 回到银行情境中,想象一下,在等待服务的客户中,有一个客户X需要提取一笔收益,但是暂时没有收益。 同时收银员不会拒绝对其他客户的服务,直到可以提取收益为止,而只是向客户 X 发出信号,让他在另一个时间或另一个日期再来。 + +!!! info "" + + 非阻塞操作是这样一种操作,它在出现**最小阻塞信号**时返回一个**控制代码**或**异常**,告诉请求者稍后重试。 + +## 理解异步操作 + +回到银行机构的例子,设想每个出职员有10个助手来执行需要较长时间的任务;现在考虑我们的机构有两个职员,每个人有10个助手。随着客户的到来,如果客户X有一个可能无限期阻塞队列的请求,这个请求就会被派给一个助理,这个助理会在后台做这个工作,当客户X的答案准备好了,就会直接找他,这样职员就可以腾出手来处理后面客户的请求,而不必阻塞前面的客户的需求了。 + +!!! info "" + + 异步操作通过**回调**、**协程**等机制通知请求结束。 + + **回调函数**是在特定条件发生时调用的函数。它通常用于处理异步执行的结果。 diff --git a/Parallel Programming with Python.pdf b/docs/files/Parallel Programming with Python.pdf similarity index 100% rename from Parallel Programming with Python.pdf rename to docs/files/Parallel Programming with Python.pdf diff --git a/docs/imgs/1-01.png b/docs/imgs/1-01.png new file mode 100644 index 0000000..28b9e52 Binary files /dev/null and b/docs/imgs/1-01.png differ diff --git a/docs/imgs/1-02.png b/docs/imgs/1-02.png new file mode 100644 index 0000000..0a4caa2 Binary files /dev/null and b/docs/imgs/1-02.png differ diff --git a/docs/imgs/1-03.png b/docs/imgs/1-03.png new file mode 100644 index 0000000..6c213c0 Binary files /dev/null and b/docs/imgs/1-03.png differ diff --git a/docs/imgs/2-01.png b/docs/imgs/2-01.png new file mode 100644 index 0000000..b304ca1 Binary files /dev/null and b/docs/imgs/2-01.png differ diff --git a/docs/imgs/2-02.png b/docs/imgs/2-02.png new file mode 100644 index 0000000..46bec92 Binary files /dev/null and b/docs/imgs/2-02.png differ diff --git a/docs/imgs/2-03.png b/docs/imgs/2-03.png new file mode 100644 index 0000000..6c922ca Binary files /dev/null and b/docs/imgs/2-03.png differ diff --git a/docs/imgs/3-01.png b/docs/imgs/3-01.png new file mode 100644 index 0000000..59f3862 Binary files /dev/null and b/docs/imgs/3-01.png differ diff --git a/docs/imgs/3-02.png b/docs/imgs/3-02.png new file mode 100644 index 0000000..223a275 Binary files /dev/null and b/docs/imgs/3-02.png differ diff --git a/docs/imgs/6-02.png b/docs/imgs/6-02.png new file mode 100644 index 0000000..c9895af Binary files /dev/null and b/docs/imgs/6-02.png differ diff --git a/docs/imgs/6-03.png b/docs/imgs/6-03.png new file mode 100644 index 0000000..f22897b Binary files /dev/null and b/docs/imgs/6-03.png differ diff --git a/docs/imgs/6-04.png b/docs/imgs/6-04.png new file mode 100644 index 0000000..5693d1e Binary files /dev/null and b/docs/imgs/6-04.png differ diff --git a/docs/imgs/7-01.png b/docs/imgs/7-01.png new file mode 100644 index 0000000..036d48a Binary files /dev/null and b/docs/imgs/7-01.png differ diff --git a/docs/imgs/7-11.png b/docs/imgs/7-11.png new file mode 100644 index 0000000..0882101 Binary files /dev/null and b/docs/imgs/7-11.png differ diff --git a/docs/imgs/7-12.png b/docs/imgs/7-12.png new file mode 100644 index 0000000..4b96c1c Binary files /dev/null and b/docs/imgs/7-12.png differ diff --git a/docs/imgs/7-13.png b/docs/imgs/7-13.png new file mode 100644 index 0000000..287b3f0 Binary files /dev/null and b/docs/imgs/7-13.png differ diff --git a/docs/imgs/7-14.png b/docs/imgs/7-14.png new file mode 100644 index 0000000..7efea30 Binary files /dev/null and b/docs/imgs/7-14.png differ diff --git a/docs/imgs/7-15.png b/docs/imgs/7-15.png new file mode 100644 index 0000000..7234557 Binary files /dev/null and b/docs/imgs/7-15.png differ diff --git a/docs/imgs/7-16.png b/docs/imgs/7-16.png new file mode 100644 index 0000000..f8fd549 Binary files /dev/null and b/docs/imgs/7-16.png differ diff --git a/docs/imgs/7-17.png b/docs/imgs/7-17.png new file mode 100644 index 0000000..b49f54d Binary files /dev/null and b/docs/imgs/7-17.png differ diff --git a/docs/imgs/7-18.png b/docs/imgs/7-18.png new file mode 100644 index 0000000..9c5a734 Binary files /dev/null and b/docs/imgs/7-18.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..75b911e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,116 @@ +# parallel programming with python + +使用 python 进行并行编程 + +使用强大的 python 环境开发高效的并行系统 + +-- Jan Palach + +## 关于作者 + +Jan Palach 从事软件开发已有 13 年,曾与科学界合作使用 C++、Java 和 Python 技术的私人公司的可视化和后端。 Jan 拥有巴西里约热内卢 Estácio de Sá 大学的信息系统学位,以及巴拉那州立联邦理工大学的软件开发研究生学位。 +目前,他在一家实施 C++ 系统的电信行业的私营公司担任高级系统分析师; 但是,他喜欢尝试使用 Python 和 Erlang 来获得乐趣——这是他的两大技术爱好。 +他天生好奇,喜欢挑战和学习新技术、结识新朋友以及了解不同的文化。 + +## 致谢 + +我不知道在如此紧迫的期限内写一本书有多难,我生活中发生了很多其他事情。 我不得不把写作融入我的日常生活中, +照顾我的家人、空手道课程、工作、暗黑破坏神 III 等等。 这项任务并不容易; 然而,考虑到根据我的经验,我已经专注于最重要的事情,我希望我已经产生了高质量的内容来取悦大多数读者。 + +我要致谢的人名单很长,我只需要一本书就可以了。 所以,我要感谢一些我经常联系的人,他们以直接或间接的方式在整个探索过程中帮助了我。 + +我的妻子 Anicieli Valeska de Miranda Pertile,我选择与她分享我的爱并收集牙刷直到生命的尽头,是她让我有时间创作这本书,并没有让我在我想我的时候放弃 做不到。 在我作为一个人的成长过程中,我的家人一直对我很重要,并教会了我善良的道路。 + +我要感谢 Fanthiane Ketrin Wentz,她不仅是我最好的朋友,还指导我学习武术,教会我一生都将秉持的价值观——她是我的榜样。 Lis Marie Martini,亲爱的朋友,为本书提供了封面,她是一位了不起的摄影师和动物爱好者。 + +非常感谢我以前的英语老师、审校者和校对者玛丽娜·梅洛 (Marina Melo),她在本书的写作过程中提供了帮助。 感谢审稿人和我的好友 Vitor Mazzi 和 Bruno Torres,他们对我的职业发展做出了巨大贡献,现在仍然如此。 + +特别感谢 Rodrigo Cacilhas、Bruno Bemfica、Rodrigo Delduca、Luiz Shigunov、Bruno Almeida Santos、Paulo Tesch (corujito)、Luciano Palma、Felipe Cruz 以及我经常与之谈论技术的其他人。 特别感谢 Turma B. + +非常感谢 Guido Van Rossum 创建了 Python,它将编程变成了令人愉快的事情; 我们需要更多这样的东西,而不是设置/获取。 + +## 关于审稿人 + +**Cyrus Dasadia** 在 AOL 和 InMobi 等组织担任 Linux 系统管理员已有十多年。 他目前正在开发 CitoEngine,这是一种完全用 Python 编写的开源警报管理服务。 + +**Wei Di** 是 eBay 研究实验室的研究科学家,专注于大型电子商务应用的高级计算机视觉、数据挖掘和信息检索技术。 她的兴趣包括大规模数据挖掘、商品销售中的机器学习、电子商务的数据质量、搜索相关性以及排名和推荐系统。 她在模式识别和图像处理方面也有多年的研究经验。 她于 2011 年在普渡大学获得博士学位,重点研究数据挖掘和图像分类。 + +**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 绑定,以及使用现代图形卡加速曲线拟合的快速模型拟合工具套件。 + +**Ludovic Gasc** 是欧洲知名的开源 VoIP 和统一通信公司 Eyepea 的高级软件集成工程师。 在过去的五年中,Ludovic 基于 Python(Twisted 和现在的 AsyncIO)和 RabbitMQ 为电信开发了冗余分布式系统。 + +他还是多个 Python 库的贡献者。 有关这方面的更多信息和详细信息,请参阅 。 + +**Kamran Husain** 在计算机行业工作了大约 25 年,为电信和石油行业编程、设计和开发软件。 他喜欢在空闲时间涉足漫画。 + +**Bruno Torres** 已经工作了十多年,解决了多个领域的各种计算问题,涉及客户端和服务器端应用程序的组合。 Bruno 拥有巴西里约热内卢 Universidade Federal Fluminense 的计算机科学学位。 + +在数据处理、电信系统以及应用程序开发和媒体流方面工作后,他从 Java 和 C++ 数据处理系统开始开发了许多不同的技能,通过解决电信行业的可扩展性问题和使用 Lua 简化大型应用程序定制,到 为移动设备和支持系统开发应用程序。 + +目前,他在一家大型媒体公司工作,开发了许多通过 Internet 为桌面浏览器和移动设备传送视频的解决方案。 + +他热衷于学习不同的技术和语言、结识新朋友,并热爱解决计算问题的挑战。 + +## 前言 + +几个月前,也就是 2013 年,Packt Publishing 的专业人士联系我,让我写一本关于使用 Python 语言进行并行编程的书。 我以前从未想过要写一本书,也不知道即将进行的工作; 构思这项工作会有多复杂,以及在我目前的工作中将它融入我的工作时间表会有什么感觉。 虽然我考虑了几天这个想法,但我最终还是接受了这个任务,并对自己说这将是一次大量的个人学习,也是一个向全世界的观众传播我的 Python 知识的绝好机会,因此 ,希望在我这一生的旅程中留下宝贵的遗产。 + +这项工作的第一部分是概述其主题。 取悦所有人并不容易; 但是,我相信我已经在这本迷你书提出的主题上取得了很好的平衡,我打算在其中介绍结合理论和实践的 Python 并行编程。 我在这项工作中冒了风险。 我使用了一种新的格式来展示如何解决问题,其中示例在第一章中定义,然后使用本书中提供的工具来解决。 我认为这是一种有趣的格式,因为它允许读者分析和质疑 Python 提供的不同模块。 + +所有章节都结合了一些理论,从而构建了上下文,为您提供一些基本知识来理解文本的实际部分。 我真诚地希望这本书对那些探索 Python 并行编程世界的人有用,因为我一直努力专注于高质量的写作。 + +## 本书涵盖的内容 + +第 1 章,上下文化并行、并发和分布式编程,涵盖了并行编程模型的概念、优点、缺点和含义。 此外,本章还公开了一些 Python 库来实现并行解决方案。 + +第 2 章,设计并行算法,介绍了有关设计并行算法的一些技术的讨论。 + +第 3 章,识别可并行化问题,介绍了一些问题示例,并分析了这些问题是否可以拆分为并行部分。 + +第 4 章,使用 threading 和 concurrent.futures 模块,解释了如何使用 threading 和 concurrent.futures 模块实现第 3 章中提出的每个问题,识别可并行化的问题。 + +第 5 章,使用 Multiprocessing 和 ProcessPoolExecutor,介绍如何使用 multiprocessing 和 ProcessPoolExecutor 实现第 3 章中提出的每个问题,识别可并行化的问题。 + +第 6 章,使用并行 Python,介绍如何使用并行 Python 模块实现第 3 章中提出的每个问题,识别可并行化的问题。 + +第 7 章,使用 Celery 分配任务,解释了如何使用 Celery 分布式任务队列实现第 3 章中提出的每个问题,识别可并行化的问题。 + +第 8 章,异步执行,解释了如何使用 asyncio 模块和有关异步编程的概念。 + +## 这本书需要你掌握什么 + +Python 编程的先前知识是必要的,因为 Python 教程不会包含在本书中。 欢迎了解并发和并行编程,因为本书是为刚开始从事此类软件开发的开发人员设计的。 关于软件,有必要获得以下内容: + +- 第 8 章“异步处理”需要 Python 3.3 和 Python 3.4(仍在开发中) +- 需要读者选择的任何代码编辑器 +- 应安装并行 Python 模块 1.6.4 +- 第 5 章使用 Multiprocessing 和 ProcessPoolExecutor 需要 Celery 框架 3.1 +- 需要读者选择的任何操作系统 + +## 这本书是给谁的 + +本书是关于使用 Python 进行并行编程的紧凑讨论。 它为初级和中级 Python 开发人员提供工具。 本书适合那些愿意全面了解使用 Python 开发并行/并发软件并学习不同 Python 替代方案的人。 到本书结束时,您将使用各章中提供的信息扩大您的工具箱。 + +## 惯例 + +在本书中,您会发现许多区分不同类型信息的文本风格。 以下是这些样式的一些示例,以及对它们含义的解释。 + +文中代码如下:“为了举例说明multiprocessing.Pipe对象的使用,我们将实现一个创建两个进程A和B的Python程序。” + +这段代码如下: + +```python +def producer_task(conn): + value = random.randint(1, 10) + conn.send(value) + print('Value [%d] sent by PID [%d]' % (value, os.getpid())) + conn.close() +``` + +任何命令行输入或输出都写成如下: + +**`$celery –A tasks –Q sqrt_queue,fibo_queue,webcrawler_queue worker --loglevel=info`** + +## 英文原文 + +参考: [Parallel Programming with Python](./files/Parallel%20Programming%20with%20Python.pdf){target="_blank"} diff --git a/mkdocs.yaml b/mkdocs.yaml new file mode 100644 index 0000000..601ba1b --- /dev/null +++ b/mkdocs.yaml @@ -0,0 +1,125 @@ +site_name: python并发编程-中文版 +repo_url: https://github.com/hellowac/parallel-programming-with-python-zh/tree/docs +repo_name: hellowac/parallel-programming-with-python-zh + +# 配置主题,在mkdocs.yml文件下 +theme: + name: material + language: zh + palette: # 文档颜色, https://squidfunk.github.io/mkdocs-material/setup/changing-the-colors/ + scheme: default # 配色方案 + primary: default # 主色 + accent: red # 强调色 + features: + # - navigation.tabs # 顶部导航 + # - navigation.sections # 全部展开(非折叠状态) + # - navigation.expand # 子目录展开 + - navigation.indexes # 带章节索引页 + - navigation.top # 返回顶部按钮 + - toc.follow + +# 插件 +plugins: + - glightbox + - search: + separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' + - git-revision-date-localized: # 支持文档创建时间显示, https://github.com/timvink/mkdocs-git-revision-date-localized-plugin + locale: zh + enable_creation_date: true + type: date + +# markdown解析扩展 +markdown_extensions: + - tables + - admonition + - attr_list + - md_in_html # https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown/#footnotes + # pymdownx 扩展,参考:https://facelessuser.github.io/pymdown-extensions/ + - pymdownx.inlinehilite # 单行高亮, 参考: https://squidfunk.github.io/mkdocs-material/reference/code-blocks/#highlighting-specific-lines + - pymdownx.critic # 支持部分字段格式化,参考:https://squidfunk.github.io/mkdocs-material/reference/formatting/ + - pymdownx.highlight # 支持代码块高亮显示 + - pymdownx.snippets + - pymdownx.details + - pymdownx.superfences: # 注释 : + # preserve_tabs: true + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.caret # 下划线, 上标 : https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/?h=caret#caret-mark-tilde + # https://facelessuser.github.io/pymdown-extensions/extensions/caret/ + - pymdownx.mark # 标记 :https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/?h=caret#caret-mark-tilde + # https://facelessuser.github.io/pymdown-extensions/extensions/mark/ + - pymdownx.tilde # 删除线, 下标 参考: https://squidfunk.github.io/mkdocs-material/setup/extensions/python-markdown-extensions/?h=caret#caret-mark-tilde + # https://facelessuser.github.io/pymdown-extensions/extensions/tilde/ + - pymdownx.arithmatex: + generic: true + +# 扩展支持, 支持数学符号 +extra_javascript: + - javascripts/mathjax.js + - https://polyfill.io/v3/polyfill.min.js?features=es6 + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js + - https://unpkg.com/mermaid@9.4.0/dist/mermaid.min.js + # - https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs + +# 页面导航 +nav: + - python并发编程: index.md + - 第一章 并行、并发以及分布式编程的对比分析: + - chapter1/index.md + - 为什么使用并行编程: chapter1/why_use_parallel_programming.md + - 探索并行化的几种方式: chapter1/exploring_common_forms_of_parallelization.md + - 并行编程间的通信: chapter1/communicating_in_parallel_programming.md + - 识别并行编程的问题: chapter1/identifying_parallel_programming_problems.md + - 发现Python并行编程的工具: chapter1/discovering_Pythons_parallel_programming_tools.md + - 小心PythonGIL: chapter1/taking_care_of_GIL.md + - 小结: chapter1/summary.md + - 第二章 设计并行算法: + - chapter2/index.md + - 分治技术: chapter2/分治技术.md + - 使用数据分解: chapter2/使用数据分解.md + - 用管道分解任务: chapter2/用管道分解任务.md + - 处理和映射: chapter2/处理和映射.md + - 小结: chapter2/总结.md + - 第三章 设计并行算法: + - chapter3/index.md + - 从多个输入中得到斐波那契最大的值: chapter3/从多个输入中得到斐波那契最大的值.md + - 爬取网页: chapter3/爬取网页.md + - 小结: chapter3/总结.md + - 第四章 使用threading和concurrent.futures模块: + - chapter4/index.md + - 什么是线程: chapter4/defining_threads.md + - 使用threading模块来为多个输入同时计算Fibonacci序列: chapter4/using_threading_to_obtain_the_Fibonacci_series_term_with_multiple_inputs.md + - 使用concurrent.futures模块爬取web信息: chapter4/crawling_the_web_using_the_concurrent_futures_module.md + - 小结: chapter4/总结.md + - 第五章 使用多进程和进程池: + - chapter5/index.md + - 理解进程的概念: chapter5/understanding_the_concept_of_a_process.md + - 理解多进程通信: chapter5/implementing_multiprocessing_communication.md + - 使用多进程解决斐波那契数列多输入问题: chapter5/using_multiprocessing_to_compute_fibonacci_series_terms_with_multiple_inputs.md + - 使用ProcessPoolExecutor模块设计网络爬虫: chapter5/crawling_the_web_using_processPoolExecutor.md + - 小结: chapter5/总结.md + - 第六章 使用并行 Python: + - chapter6/index.md + - 理解进程间通信: chapter6/understanding_interprocess_communication.md + - 了解Parallel Python(PP): chapter6/discovering_pp.md + - 在SMP架构上使用PP计算斐波那契序列: chapter6/using_pp_to_calculate_the_fibonacci_series_term_on_smp_architecture.md + - 使用PP创建分布式的网络爬虫: chapter6/using_pp_to_make_a_distributed_web_crawler.md + - 小结: chapter6/总结.md + - 第七章 使用Celery分发任务: + - chapter7/index.md + - 理解 Celery: chapter7/understanding_celery.md + - 理解 Celery 的架构: chapter7/understanding_celery_architecture.md + - 搭建环境: chapter7/setting_up_the_environment.md + - 分派一个简单的任务: chapter7/dispatching_a_simple_task.md + - 使用 Celery 获取斐波那契数列项: chapter7/using_celery_to_obtain_a_fibonacci_series_term.md + - 根据任务类型定义队列: chapter7/defining_queues_by_task_types.md + - 使用 Celery 制作分布式网络爬虫: chapter7/using_celery_to_make_a_distributed_web_crawler.md + - 小结: chapter7/总结.md + - 第八章 异步编程: + - chapter8/index.md + - 理解阻塞非阻塞和异步操作: chapter8/理解阻塞非阻塞和异步操作.md + - 理解事件循环: chapter8/理解事件循环.md + - 使用asyncio: chapter8/使用asyncio.md + - 小结: chapter8/总结.md \ No newline at end of file diff --git "a/\347\254\254\344\270\200\347\253\240/ReadMe.md" "b/\347\254\254\344\270\200\347\253\240/ReadMe.md" deleted file mode 100644 index 06ad90a..0000000 --- "a/\347\254\254\344\270\200\347\253\240/ReadMe.md" +++ /dev/null @@ -1,20 +0,0 @@ -##Contextualizing Parallel, Concurrent, and Distributed Programming - -- 并行、并发分布式编程对比分析 -- 为什么使用并行编程 -- 探索并行化的几种方式 -- 并行编程间的通信 - * 理解状态共享 - * 理解消息传递 -- 识别并行编程的问题 - * 死锁 - * 饥饿 - * 竞态条件 -- 发现Python并行编程的工具 - - Python线程模块 - - Python多进程模块 - - Python并行模块 - - Celery-一个分布式任务队列 -- 小心PythonGIL -- 总结 - diff --git "a/\347\254\254\344\270\200\347\253\240/\344\270\272\344\273\200\344\271\210\344\275\277\347\224\250\345\271\266\350\241\214\347\274\226\347\250\213.md" "b/\347\254\254\344\270\200\347\253\240/\344\270\272\344\273\200\344\271\210\344\275\277\347\224\250\345\271\266\350\241\214\347\274\226\347\250\213.md" deleted file mode 100644 index ed4c079..0000000 --- "a/\347\254\254\344\270\200\347\253\240/\344\270\272\344\273\200\344\271\210\344\275\277\347\224\250\345\271\266\350\241\214\347\274\226\347\250\213.md" +++ /dev/null @@ -1,7 +0,0 @@ -##为什么使用并行编程 - -自从计算系统进化以来,它们已经开始提供一个能使我们以并行的方式运行指定软件的独立的某部分的机制,从而提升响应速度以及一般的性能。此外,我们可以很容易的验证配备有多个处理器以及多核的机器。那么,为什么不利用这个架构呢? - -在系统开发的所有情况下,从智能手机、平板电脑到研究中心的重型计算,并行编程都是现实的。并行编程的一个坚实的基础将允许开发人员优化一个应用的性能。这会增强用户体验以及计算资源的消费,从而减少完成复杂任务的处理时间。 - -举一个并行性的例子,让我们想象一个场景,在这个场景中,有一些任务,其中一个任务是从数据库中检索一些信息,而这个数据库规模又很大。再假如,这个应用还需要顺序执行,在这个应用中,这些任务必须以一定的逻辑顺序,一个接一个的执行。当用户请求数据时,在返回的数据没有结束之前,其它系统将一直被阻塞。然而,利用并行编程,我们将会创造一个新的worker来在数据库中查询信息,而不会阻塞这个应用的其它功能,从而提高它的使用。 diff --git "a/\347\254\254\344\270\200\347\253\240/\345\217\221\347\216\260Python\345\271\266\350\241\214\347\274\226\347\250\213\347\232\204\345\267\245\345\205\267.md" "b/\347\254\254\344\270\200\347\253\240/\345\217\221\347\216\260Python\345\271\266\350\241\214\347\274\226\347\250\213\347\232\204\345\267\245\345\205\267.md" deleted file mode 100644 index 1d1e12a..0000000 --- "a/\347\254\254\344\270\200\347\253\240/\345\217\221\347\216\260Python\345\271\266\350\241\214\347\274\226\347\250\213\347\232\204\345\267\245\345\205\267.md" +++ /dev/null @@ -1,31 +0,0 @@ -##发现Python并行编程的工具 - -由Guido Van Rossum创造的语言,是一种多泛型的,多用途的语言。由于它非常简单且易于维护,被世界各处广泛接受。它也被称为含有电池的语言。它有广泛的模块使其用起来更流畅。在并行编程中,Python有简化实现的内置和外部模块。本书基于Python3.X的。 - -###Python的threading模块 - -Python的threading模块提供了一个抽象层次的模块_thread,这是一个低层次的模块。当开发一个基于线程的并行系统的艰巨任务时,它为程序员提供了一些函数来帮助程序员的开发。线程模块的官方文档可以在找到。 - -###Python的mutliprocess模块 - -multiprocessing模块旨在为基于进程的并行的使用提供一个简单的API。这个模块与线程模块类似,它简化了基于进程的并行系统的开发,这一点与线程模块没有什么不同。在Python社区中,基于进程的方法很流行,因为它是在解决出现在Python中CPU-Bound threads和GIL的使用的问题时的一个解决方案。多进程模块的官方文档可以在找到。 - -###Python的parallel模块 - -Python的parallel模块是外部模块,它提供了丰富的API,这些API利用进程的方法创建并行和分布式系统。这个模块是轻量级并且易安装的,它与其他的Python程序一起集成的。parallel模块可以在找到。在那么多特性中,我们着重强调以下几点: - -* 最优配置的自动检测 -* 运行时可以改变多个工作进程 -* 动态的负载均衡 -* 容错性 -* 自发现计算资源 - -###Celery-分布式任务队列 - -Celery是一个用于创建分布式系统的极其优秀的模块,并且拥有很好的文档。它在并发形式上使用了至少三种不同类型的方法来执行任务:multiprocessing, Eventlet,和 Gevent。这项工作将会集中精力在多进程的方法的使用上。而且,只需要通过配置就能实现进程间的互联,这被留下来作为一个研究,以便读者能够建立一个与他/她的实验的一个比较。 - -Celery模块可以在官方的项目页面得到。 - - diff --git "a/\347\254\254\344\270\200\347\253\240/\345\234\250\345\271\266\350\241\214\347\274\226\347\250\213\351\200\232\344\277\241.md" "b/\347\254\254\344\270\200\347\253\240/\345\234\250\345\271\266\350\241\214\347\274\226\347\250\213\351\200\232\344\277\241.md" deleted file mode 100644 index 3de2ae0..0000000 --- "a/\347\254\254\344\270\200\347\253\240/\345\234\250\345\271\266\350\241\214\347\274\226\347\250\213\351\200\232\344\277\241.md" +++ /dev/null @@ -1,23 +0,0 @@ -##在并行编程通信 - -在并行编程中,workers被送来执行任务,而执行任务常常需要建立通信,以便可以合作解决一个问题。 -在大多数情况下,通信以一种可以在workers之间进行数据交换的方式被建立。当说到并行编程,有两种通信方式广为人知:共享状态和消息传递。在下面的章节中,将对这两种方式进行简要描述。 - -###理解共享状态 - -在workers中最有名的一种通信方式就是共享状态。分享状态似乎是一个简单的使用,但是这会有许多的陷阱,因为若其中某个进程对共享的资源执行了一项无效的操作会影响到所有其它的进程,从而导致一个不好的结果。这也是使在多台计算机之间进行分布式的程序成为不可能的显而易见的原因。 - -为了说明这一点,我们将使用一个真实的案例。假设你是一个具体的银行的一个客户,而这个银行只用一个收银员。当你去银行,你必须要排队等到轮到你的时候。当你在队列中时,你注意到收银员一次只能为一个顾客服务,而收银员不可能同时为两个顾客提供服务而不出错。电脑运算拥有多种手段来以可控的方式访问数据,如mutex(互斥?)。 - -Mutex可以理解为一种特殊的过程变量,表示了访问数据的可靠性等级。也就是说,在真实世界的栗子中,顾客有一个编号,在某一特定的时刻,这个编号将会被激活,然后收银员仅对于这个顾客提供服务。在进程结束时,该名顾客将会释放收银员让其为下一个顾客服务,以此类推。 - -> 在某些情况下,当程序正在运行时,在一个变量中数据会有一个常数值,数据仅仅以只读的目的被分享。所以访问控制不是必须的,因为永远不会出现完整性问题。 - -###理解信息传递 - -运用消息传递是为了避免来自共享状态带来的数据访问控制以及同步的问题。消息传递包含一种在运行的进程中进行消息交换的机制。每当我们用分布式架构开发程序的时候,就能见到消息传递的使用,在网络中,消息交换被放在一个重要的位置。Erlang等语言,在它的并行体系结构中,就是使用这个模型来实现通信。由于每次数据交换都复制一份数据的拷贝,因此不会出现并发访问的问题。尽管内存使用看起来比共享内存状态要高,但是这个模型还是有一些优势的。优势如下: - -* 缺乏数据的一致性访问 -* 数据即可在本地交换(不同的进程)也能在分布式环境中交换 -* 不太可能出现扩展性问题,并且允许不同系统相互写作。 -* 一般来说,据程序员来说易于维护。 diff --git "a/\347\254\254\344\270\200\347\253\240/\345\260\217\345\277\203Python GIL.md" "b/\347\254\254\344\270\200\347\253\240/\345\260\217\345\277\203Python GIL.md" deleted file mode 100644 index 43c388f..0000000 --- "a/\347\254\254\344\270\200\347\253\240/\345\260\217\345\277\203Python GIL.md" +++ /dev/null @@ -1,6 +0,0 @@ -##小心Python GIL - -GIL是一种用于实现标准Python(也被称为CPython)的一种机制,是为了避免不同的线程同时执行字节码。在这门语言的使用者中,GIL存在的原因被激烈的讨论着。GIL被用于保护被CPython解释器使用的内存储器,因为CPython解析器并未实现为线程的并发访问的同步机制。在任何情况下,当我们决定使用线程时,GIL将会导致一个问题,这些往往是CPU受限。比如说,I/O Threads超出了GIL的范围。也许GIL机在Python的演变过程中带来的好处要多于坏处。显然,我们不能仅仅将效率作为评判一个事情是好是坏的唯一的标准。 - -很多情况下,使用多进程配合消息传递能更好的平衡可维护性、可扩展性以及性能之间的关系。然而,在某些情况下即使由于GIL的存在会降低效率也还是会需要线程。这时,所能做的就是写一些代码片段作为C语言的扩展,并且把它们嵌入到Python程序中。因此也是有替代品的,这应由开发人员分析真正的需求。那么问题来了,GIL一般来说是一个恶棍吗?重要的是要记住,PyPy团队正致力于将GIL从Python中移除的STM的实现。想了解有关此项目的更多细节,请访问. - diff --git "a/\347\254\254\344\270\200\347\253\240/\345\271\266\350\241\214\343\200\201\345\271\266\345\217\221\344\270\216\345\210\206\345\270\203\345\274\217\347\274\226\347\250\213\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" "b/\347\254\254\344\270\200\347\253\240/\345\271\266\350\241\214\343\200\201\345\271\266\345\217\221\344\270\216\345\210\206\345\270\203\345\274\217\347\274\226\347\250\213\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" deleted file mode 100644 index 478662d..0000000 --- "a/\347\254\254\344\270\200\347\253\240/\345\271\266\350\241\214\343\200\201\345\271\266\345\217\221\344\270\216\345\210\206\345\270\203\345\274\217\347\274\226\347\250\213\347\232\204\345\257\271\346\257\224\345\210\206\346\236\220.md" +++ /dev/null @@ -1,24 +0,0 @@ -##并行、并发以及分布式编程的对比分析 - -并行编程可以被定义为一种模型,这个模型旨在创造一种能与被准备用于同时执行代码指令的环境相兼容的程序。并行技术被用于开发软件还不是很长。几年前,处理器在其它组件中只有一个在一段时间空间中一次只能执行一条指令的算术逻辑单元(ALU)。多年来,只有一个以Hz为衡量单位的时钟,被用来决定在给定的时间间隔里一个处理器能执行的指令数量。时钟的数量越多,那么执行的更多指令数量的潜力更大,比如说KHz(每秒数以千计的操作)、MHz(每秒数以百万计的操作)、以及现在的GHz(每秒数以千万计的操作)。 - -总结,每个周期处理器执行的指令越多,那么它执行的越快。在80年代,一个革命性的处理器步入人们的生活,Intel 80386,允许任务以预先执行的方式来执行,也就是说,它会阶段性的中断当前的程序来为另一个程序提供处理器时间;这意味着一种基于时间分片的伪并行处理的诞生。 - -在80年代末期,出现了Intel80486,它实现了一种管道系统,在实践中,这个系统将执行阶段划分为不同的子阶段。实际上,在一个处理器周期中,我们可能有不同的指令在一个子阶段同时被执行。 - -所有的在前一节所提到的进步带来一些性能上的提升,但是这些仍然不够,我们所面对的微妙的问题,而这,被称为摩尔定律()。 - -探寻满足时钟高能耗的过程最终会被物理规则的限制所阻;处理器将消耗更高的能量,从而产生更多的热。此外,还有一个重大的问题,便携式电脑的市场在90年代正在加速扩张。所以,有一种能够使这些设备的电源能够持续长时间的远离插座的处理器是极其重要的。几个来自不同的制造商的技术和家庭的处理器诞生了。在服务器和大型主机领域,值得一提的是英特尔的Core(R)系列产品,该系列产品允许即使只有一个物理芯片,也能让操作系统模拟出存在不止一个的处理器。 - -在Core(R)系列产品中,处理器有重大的内部变化和特色组件被称为内核,它有自己的ALU和L2、L3缓存,以及其它的用于执行指令的元素。这些内核也被称为逻辑处理器,让我们可以同时并行的执行一个程序的不同的部分,甚至同时执行不同的程序。这个时代的内核是低能耗的使用与功率处理都比前一代处理器更优。作为并行工作的内核,模拟独立的处理器,我们可以有多核芯片和一个比较次的时钟,从而根据任务来获得与单核芯片却有更高的时钟相比的更高的性能。 - -当然,如此多的进化改变了我们软件设计的方法。现如今,我们必须考虑并行的设计系统来使我们更合理的使用资源,而不是浪费它们,从而为用户提供更好地体验,并且不仅仅在个人电脑上节省资源,而且要在处理中心节省资源。尤其是现在并行编程已融入开发人员的日常生活,并且很明显不会消失。 - -本章节包含以下几个主题: - -* 为什么使用并行编程? -* 介绍并行化的常见形式 -* 在并行编程中通信 -* 识别并行编程的问题 -* 发现Python的并行编程工具 -* 小心Python的Global Interpreter Lock(GIL) diff --git "a/\347\254\254\344\270\200\347\253\240/\346\216\242\347\264\242\345\271\266\350\241\214\345\214\226\347\232\204\345\207\240\347\247\215\346\250\241\345\274\217.md" "b/\347\254\254\344\270\200\347\253\240/\346\216\242\347\264\242\345\271\266\350\241\214\345\214\226\347\232\204\345\207\240\347\247\215\346\250\241\345\274\217.md" deleted file mode 100644 index d85dd93..0000000 --- "a/\347\254\254\344\270\200\347\253\240/\346\216\242\347\264\242\345\271\266\350\241\214\345\214\226\347\232\204\345\207\240\347\247\215\346\250\241\345\274\217.md" +++ /dev/null @@ -1,33 +0,0 @@ -##探索并行化的几种模式 - -当我们试图定义并行系统的主要模式时,有困惑很正常。常常会有人提到并发系统和并行系统,这两个术语看起来像是讲的同一件事。然而实际上有着轻微的差异。 - -在并发程序中,我们有这么一个场景,在这个场景中,一个程序分派几个workers,这些workers争着使用CPU来执行任务。在纷争发生的阶段将被CPU调度器所控制,CPU调度器的功能是决定在一个特定的时刻,哪个worker更适合使用资源。在大多数情况下,CPU调度器执行清理过程的任务太快以至于我们会有伪并行的印象。因此,并发编程是来自并行编程的一种抽象。 - -> 并发系统允许多个任务争夺同一个CPU。 - -下图显示了一个并发程序方案: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Concurrent%20programming%20scheme.png?raw=true) - -并行编程可以被定义为一种方法,在那个方法中,程序数据创造workers在多核环境中同时执行指定的任务,在这些任务中它们不需要并发的接触CPU。 - -> 并行系统同时的运行任务。 - -下面的图显示了并行系统的概念: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Parallel%20programming%20scheme.png?raw=true) - -分布式编程旨在在物理分离的计算机器(节点)之间通过消息交换数据来分享进程 - -分布式编程变得越来越受欢迎的原因有很多,下面是被探讨的受欢迎的原因: - -* **容错性**:由于系统是去中心化的,我们可以分发执行到同一个网络的不同机器,从而执行指定机器的个人维护而不影响整个系统的功能。 -* **横向扩展**:通常我们可以在分布式系统中增加处理的性能。我们可以在不需要终止正在执行的应用的情况下连接新的设备。 -* **云计算**:随着硬件成本的降低,我们需要这种业务类型的增长,在这种增长中,我们可以获得巨大的机器集群,这些集群对用户来说以一种合作的方式运行并且以一种透明的方式运行程序。 - -> 分布式系统在物理隔离的节点上运行任务。 - -下图显示了一个分布式系统的方案: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Distributed%20programming%20scheme.png?raw=true) diff --git "a/\347\254\254\344\270\200\347\253\240/\350\257\206\345\210\253\345\271\266\350\241\214\347\274\226\347\250\213\347\232\204\351\227\256\351\242\230.md" "b/\347\254\254\344\270\200\347\253\240/\350\257\206\345\210\253\345\271\266\350\241\214\347\274\226\347\250\213\347\232\204\351\227\256\351\242\230.md" deleted file mode 100644 index 47a5e89..0000000 --- "a/\347\254\254\344\270\200\347\253\240/\350\257\206\345\210\253\345\271\266\350\241\214\347\274\226\347\250\213\347\232\204\351\227\256\351\242\230.md" +++ /dev/null @@ -1,33 +0,0 @@ -##识别并行编程的问题 - -当在并行魔鬼居住的大地上战斗之时,勇敢的键盘战士将会遇到一些经典的问题。当没有经验的程序员使用workers和共享状态相结合时,这些问题时有发生。这些问题将会在下面的小节中进行描述。 - -###死锁 - -死锁是这么一种情形,有两个或两个以上的workers继续为了资源的释放而无限期的等待,而资源由于某些原因被这一组的一个worker所占用。为了更好的理解,我们将使用另一个真实的案例。想象银行的入口有一个旋转门。顾客A走向了允许他进入银行的一侧,而顾客B试图从旋转门入口的一侧离开银行,这样的话门将被卡在那里,两个顾客哪里都去不了。这种情形在现实中会很搞笑,但是在编程中将会是一个悲剧。 - -> 死锁是这么一个现象,进程都在等待一个释放它们任务出现的情况,而这个情况永远不会出现。 - -###饥饿 - -这个问题是由于一个或者多个进程不公平的竞争所引起的副作用,这会花费更多的时间来执行任务。想象有一组进程,A进程正在执行繁重的任务,而且这个任务还有数据处理优先级。现在,想象一下,高优先级的进程A持续不断的占用CPU,而低优先级的进程B将永远没有机会。因此可以说进程B在CPU周期中是饥饿的。 - -> 饥饿是由于进程排名中差劲的调整策略引起的。 - -###竞态条件 - -当一个进程的结果取决于执行结果的顺序,而这个顺序由于缺乏同步机制而被破坏,这个时候我们将面临竞态条件。在大的系统中,它们造成的问题将会难以过滤。举个栗子,有一对夫妇有一个联名的账户,在他们操作之前初始的余额是$100.下表显示了常规的有保护机制、预期的事实顺序以及结果情况: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Presents%20baking%20operations%20without%20the%20chance%20of%20race%20conditions%20occurrence.png?raw=true) - -在下表中,有问题的场景出现了。假设账户没有同步机制,并且操作的顺序也和预期不一样。 - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Analogy%20to%20balance%20the%20problem%20in%20a%20joint%20account%20and%20race%20conditions.png?raw=true) - -由于在操作的顺序中意外的没有同步,最终的结果将会明显不一致。并行编程的一个特点就是不确定性。当两个workers都正在运行或者它们中的一个第一个运行的时候,结果将会是不可预见的。因此同步机制是必不可少的。 - -> 不确定性,如果缺乏同步机制,将会导致竞态条件的问题。 - - - - diff --git "a/\347\254\254\344\270\203\347\253\240/ReadMe.md" "b/\347\254\254\344\270\203\347\253\240/ReadMe.md" deleted file mode 100644 index f5c1f67..0000000 --- "a/\347\254\254\344\270\203\347\253\240/ReadMe.md" +++ /dev/null @@ -1 +0,0 @@ -##Distributing Tasks with Celery \ No newline at end of file diff --git "a/\347\254\254\344\270\203\347\253\240/\345\210\206\345\217\221\347\256\200\345\215\225\344\273\273\345\212\241.md" "b/\347\254\254\344\270\203\347\253\240/\345\210\206\345\217\221\347\256\200\345\215\225\344\273\273\345\212\241.md" deleted file mode 100644 index 43f3d0a..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\345\210\206\345\217\221\347\256\200\345\215\225\344\273\273\345\212\241.md" +++ /dev/null @@ -1,70 +0,0 @@ -在之前,我们已经建立好环境。下面测试一下环境,发送一个计算平方根的任务。 -定义任务模块tasks.py。在开始,导入必须的模块。 - - from math import sqrt - from celery import Celery - -然后,创建Celery实例,代表客户端应用: - - app = Celery('tasks', broker='redis://192.168.25.21:6379/0') - -在初始化时我们传入了模块的名称和broker的地址。 -然后,启动result backend,如下: - - app.config.CELERY_RESULT_BACKEND = 'redis://192.168.25.21:6379/0' - -用@app.tack装饰器定义任务: - - @app.task - def square_root(value): - return sqrt(value) - -到此,我们完成了tasks.py模块的定义,我们需要初始化服务端的workers。我们创建了一个单独的目录叫做8397_07_broker。拷贝tasks.py模块到这个目录,运行如下命令: - - $celery –A tasks worker –-loglevel=INFO - -上述命令初始化了Clery Server,—A代表Celery应用。下图是初始化的部分截图 - -![](图片链接地址) - -现在,Celery Server等待接收任务并且发送给workers。 -下一步就是在客户端创建应用调用tasks。 - - > 上述步骤不能忽略,因为下面会用在之前创建的东西。 - -在客户端机器,我们有celery_env虚拟环境,现在创建一个task_dispatcher.py模块很简单,如下步骤; -1. 导入logging模块来显示程序执行信息,导入Celery模块: - - import logging - from celery import Celery - -2. 下一步是创建Celery实例,和服务端一样: - - #logger configuration... - app = Celery('tasks', - broker='redis://192.168.25.21:6379/0') - app.conf.CELERY_RESULT_BACKEND = - 'redis://192.168.25.21:6397/0' - - -由于我们在接下的内容中要复用这个模块来实现任务的调用,下面我们创建一个方法来封装sqrt_task(value)的发送,我们将创建manage_sqrt_task(value)方法: - - def manage_sqrt_task(value): - result = app.send_task('tasks.sqrt_task', args=(value,)) - logging.info(result.get()) - -从上述代码我们发现客户端应用不需要知道服务端的实现。通过Celery类中的send_task方法,我们传入module.task格式的字符串和以元组的方式传入参数就可以调用一个任务。最后,我们看一看log中的结果。 -在__main__中,我们调用了manage_sqrt_task(value)方法: - - if __name__ == '__main__': - manage_sqrt_task(4) - -下面的截图是执行task_dispatcher.py文件的结果: - -![](图片链接地址) - -在客户端,通过get()方法得到结果,这是通过send_task()返回的AsyncResult实例中的重要特征。结果如下图: - -![](图片链接地址) - - diff --git "a/\347\254\254\344\270\203\347\253\240/\345\273\272\347\253\213\347\216\257\345\242\203.md" "b/\347\254\254\344\270\203\347\253\240/\345\273\272\347\253\213\347\216\257\345\242\203.md" deleted file mode 100644 index adbcd27..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\345\273\272\347\253\213\347\216\257\345\242\203.md" +++ /dev/null @@ -1,51 +0,0 @@ -### 建立环境 - -在本章,我们将使用两台linux机器。第一台主机名foshan,作为客户端,执行app Celery分发的任务。另一台机器主机名Phoenix,作为broker、result backend和任务队列。 - -### 配置客户端机器 - -首先配置客户端机器。在这台机器上,用pyvenv建立Python3.3的虚拟环境。用pyvenv的目的是隔离每个项目的开发环境。执行以下命令能够创建虚拟环境: - - $pyvenv celery_env - -上述命令在当前路径创建一个名为celery_env的文件夹,里面包含所有Python开发环境必须的结构。下图是该目录所包含的内容: - -![](图片链接地址) - -在创建了虚拟环境之后,我们就可以开始工作并安装需要使用的包。然而,首先我们得激活这个环境,执行以下命名: - - $source bin/activate - -当命令行提示符改变了,例如在左边出现celery_env,就说明激活完成。所有你安装的包都只在这个目录下有效,而不是在整个系统中有效。 - -> 用--system-site-packages标识可以创建能够访问系统site-packages的虚拟环境,但是不推荐使用。 - -现在,我们有一个虚拟环境,假设已经安装好了setuptools或者pip。下面为客户端安装必须的包,如下命令: - - $pip install celery - -下图是已经安装好的framework v3.1.9,将在本书中使用该版本。 - -![](图片链接地址) - -现在我们要在Celery中安装支持的Redis,这样客户端就可以通过broker传输消息了。用如下命令: - - $pip install celery[redis] - -现在我们的客户端环境配置好了,在开始编码之前,我们必须配置好服务器端的环境。 - - -### 配置服务器 - -为了配置服务器,我们首先安装Redis,Redis将作为broker和result backend。使用如下命令: - - $sudo apt-get install redis-server - -启动Redis: - - $redis-server - -如果成功,会出现类似下图中的输出 - -![](图片链接地址) - diff --git "a/\347\254\254\344\270\203\347\253\240/\346\200\273\347\273\223.md" "b/\347\254\254\344\270\203\347\253\240/\346\200\273\347\273\223.md" deleted file mode 100644 index e36d284..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\346\200\273\347\273\223.md" +++ /dev/null @@ -1,2 +0,0 @@ -在本章,我们论述了Celery的分布式任务队列,并且对Celery的架构、组件、基本环境搭建、运行简单应用有了了解。如果单独讲Celery可以写一本书,通过本章需要你对Celery有基本的认识。 -在下一章,我们将学习asyncio模块和怎样以异步的方式执行程序。下一章也会涉及到协程(coroutines)和asyncio中协程的应用。 \ No newline at end of file diff --git "a/\347\254\254\344\270\203\347\253\240/\346\240\271\346\215\256\344\273\273\345\212\241\347\261\273\345\236\213\345\256\232\344\271\211\351\230\237\345\210\227.md" "b/\347\254\254\344\270\203\347\253\240/\346\240\271\346\215\256\344\273\273\345\212\241\347\261\273\345\236\213\345\256\232\344\271\211\351\230\237\345\210\227.md" deleted file mode 100644 index 199c2ff..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\346\240\271\346\215\256\344\273\273\345\212\241\347\261\273\345\236\213\345\256\232\344\271\211\351\230\237\345\210\227.md" +++ /dev/null @@ -1,28 +0,0 @@ -计算斐波那契数列的任务已经实现并且运行。所有任务都被发送到默认的Celery队列。然而,有方法路由任务到不同的队列。让我们重构服务器端的架构来实现路由任务。我们将根据任务类型定义队列。 -在服务器端启动Celery server时,我们将建立三种不同的队列,它们将被workers消费。斐波那契数列任务有fibo_queue,平方根任务有sqrt_queue,网络爬虫任务有 webcrawler_queue。然而,划分队列有什么好处呢。以下列举一些: - -- 相同类型的任务划分为一组,使得监控更加简单 -- 定义worker从一个队列取任务,性能更佳 -- 可在性能更好的机器上建立任务更重的队列 - -为了再server上建立队列,我们只需用如下命令初始化Celery: - - $celery –A tasks –Q sqrt_queue,fibo_queue,webcrawler_queue worker --loglevel=info - -下图是在服务端截图: - -![](图片链接地址) - -在进行下一个例子之前,我们路由现有的任务到队列中。在服务端,在task_dispatcher.py模块中,我们将修改send_task调用来将任务发送到不同的队列。修改如下: - - app.send_task('tasks.sqrt_task', args=(value,), - queue='sqrt_queue', routing_key='sqrt_queue') - -然后,修改fibo_task调用,如下: - - app.send_task('tasks.fibo_task', args=(x,), queue='fibo_queue', - routing_key='fibo_queue') - -> 如果有兴趣监控队列、统计任务数量或者其他,请参考Celery文档 http://celery.readthedocs.org/en/latest/userguide/monitoring.html。 -> 在任何情况用Redis,redis-cli都可以作为一个工具。队列、任务、workders都可以被监控,详见 http://celery.readthedocs.org/en/latest/userguide/monitoring.html#workers. - diff --git "a/\347\254\254\344\270\203\347\253\240/\347\220\206\350\247\243Celery.md" "b/\347\254\254\344\270\203\347\253\240/\347\220\206\350\247\243Celery.md" deleted file mode 100644 index 2bf5fd6..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\347\220\206\350\247\243Celery.md" +++ /dev/null @@ -1,11 +0,0 @@ -Celery是一个框架,该框架提供机制来简化构建分布式系统的过程。Celery框架通过在同一个网络内的机器之间交换信息来分发任务。任务(task)在Celery中是一个关键的概念,任何形式的job在分发之前都必须封装成任务。 - -## 为什么使用Celery - -Celery有如下优点: -- 分发任务是透明的 -- 在并发worker启动时,对其改变很小 -- 支持同步、异步、周期和计划任务 -- 如果出现了错误,会重新执行任务 - -> 很多开发者都认为同步任务和实时任务是一样的,实际上它们是完全不同的。对于实时任务,它有一个时间窗口,任务执行必须在Deadline之前完成。如果经过分析,任务在时间窗口内完成不了,那么它将被终止或者暂停直到下次能够完成,而同步任务是当任务执行完后才返回结果。 diff --git "a/\347\254\254\344\270\203\347\253\240/\347\220\206\350\247\243Celery\346\236\266\346\236\204.md" "b/\347\254\254\344\270\203\347\253\240/\347\220\206\350\247\243Celery\346\236\266\346\236\204.md" deleted file mode 100644 index dbdbe02..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\347\220\206\350\247\243Celery\346\236\266\346\236\204.md" +++ /dev/null @@ -1,50 +0,0 @@ -Celery架构基于可插拔组件和用*message transport(broker)*实现的消息交换机制。具体的如下图所示: -![](图片链接地址) - -现在,让我们详细的介绍Celery的每个组件。 - -### 处理任务 - -在上图中的*client*组件,有创建和分派任务到brokers的方法。 -分析如下示例代码来演示通过使用@app.task装饰器来定义一个任务,它可以被一个Celery应用的实例访问,下面代码展示了一个简单的`Hello World app`: - - @app.task - def hello_world(): - return "Hello I'm a celery task" - - -> 任何可以被调用的方法或对象都可以成为任务 - -正如我们之前所说,有各种类型的任务:同步、异步、周期和计划任务。当我们调用任务,它返回类型AsyncResult的实例。AsyncResult对象中可以查看任务状态,当任务结束后可以查看返回结果。然而,为了利用这个机制,另一个叫做*result backend*的组件必须启动,这将在本章的后面讲解。为了分派任务,我们可以用下面这些方法: -- delay(arg, kwarg=value) : 它会调用apply_aync方法。 -- apply_async((arg,), {'kwarg': value}) : 该方法可以为任务设置很多参数,一些参数如下。 - - countdown : 默认任务是立即执行,该参数设置经过countdown秒之后执行。 - - expires : 代表经过多长时间终止。 - - retry : 如果连接或者发送任务失败,该参数可是重试。 - - queue : 任务队列。 - - serializer : 数据格式,其他还有json、yaml等等 - - link : 连接一个或多个即将执行的任务。 - - link_error : 连接一个或多个执行失败的任务。 -- apply((arg,), {'kwarg': value}) : 在本地进程以同步的方式执行任务,因而阻塞直到结果就绪。 - -> Celery 提供了查看任务状态的机制,这在跟踪进程的真实状态非常有用。更多的关于内建任务状态的资料请查看http://celery.readthedocs.org/en/latest/reference/celery.states.html - -### 发现消息传输(broker) - -broker是Celery中的核心组件,通过broker可以发送和接受消息,来完成和workers的通信。Celery支持大量的brokers。然而,对于某些broker,不是所有的Celery机制都实现了。实现功能最全的是**RabbitMQ**和**Redis**。在本书中,我们将采用Redis作为broker。broker提供在不同客户端应用之间通信的方法,客户端应用发送任务,workers执行任务。可以有多台带有broker的机器等待接收消息,然后发送消息给workers。 - - -### 理解workers - -Workers负责执行接收到的任务。Celery提供了一系列的机制,我们可以选择最合适的方式来控制workers的行为。这些机制如下: -- 并发模式:例如进程、线程、协程(Eventlet)和Gevent -- 远程控制:利用该机制,可以通过高优先级队列发送消息到某个特定的worker来改变行为,包括runtime。 -- 撤销任务:利用该机制,可以指挥一个或多个workers来忽略一个或多个任务。 - -更多的特性可以在运行时设定或者改变。比如,worker在某一段时间执行的任务数,worker在哪个队列消费等等。软玉worker更多的信息可以参考 http://docs.celeryproject.org/en/latest/userguide/workers.html#remote-control - -### 理解result backends - -Result backend组件存储任务的状态和任务返回给客户端应用的结果。Celery支持的result backend之中,比较出彩的有 RabbitMQ, Redis, MongoDB, Memcached。每个result backend都有各自的优缺点,详见 http://docs.celeryproject.org/en/latest/userguide/tasks.html#task-result-backends - -现在,我们对Celery架构有了一个大概的认识。下面我们建立一个开发环境来实现一些例子。 \ No newline at end of file diff --git "a/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\345\210\206\345\217\221\344\273\273\345\212\241.md" "b/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\345\210\206\345\217\221\344\273\273\345\212\241.md" deleted file mode 100644 index 7f6a092..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\345\210\206\345\217\221\344\273\273\345\212\241.md" +++ /dev/null @@ -1,8 +0,0 @@ -在之前的章节,我们学习了Python并行编程。我们用Python并行模块实现了一些实例,包括斐波那契数列和网络爬虫。我们知道了怎样用管道进行进程间通信和如何在同一个网络的不同机器上分发进程。在本章节,我们将学习如何利用Celery框架在同一个网络的不同机器之间分发任务。 -在本章,我们将覆盖一下几个主题: -- 理解Celery -- 理解Celery架构 -- 建立环境 -- 分发简单任务 -- 用Celery来获得斐波那契数列的项 -- 用Celery来构建一个分布式网络爬虫系统 \ No newline at end of file diff --git "a/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\346\236\204\345\273\272\344\270\200\344\270\252\345\210\206\345\270\203\345\274\217\347\275\221\347\273\234\347\210\254\350\231\253\347\263\273\347\273\237.md" "b/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\346\236\204\345\273\272\344\270\200\344\270\252\345\210\206\345\270\203\345\274\217\347\275\221\347\273\234\347\210\254\350\231\253\347\263\273\347\273\237.md" deleted file mode 100644 index ac71967..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\346\236\204\345\273\272\344\270\200\344\270\252\345\210\206\345\270\203\345\274\217\347\275\221\347\273\234\347\210\254\350\231\253\347\263\273\347\273\237.md" +++ /dev/null @@ -1,65 +0,0 @@ -现在我们将用Celery构建网络爬虫。我们已经有了webcrawler_queue,负责hcrawler任务。然而,在服务器端,我们将在tasks.py模块创建crawl_task任务。 -首先,导入re(正则表达式)和requests(HTTP lib)模块,代码如下: - - import re - import requests - -然后,定义正则表达式,和之前的章节一样; - - hTML_link_regex = re.compile( - '') - -然后,替换crawl_task方法,添加@app.task装饰器,修改返回信息,如下: - - @app.task - def crawl_task(url): - request_data = requests.get(url) - links = html_link_regex.findall(request_data.text) - message = "The task %s found the following links %s.."\ - Return message - -links列表不一定要和下图匹配: - -![](图片链接地址) - -在客户端task_dipatcher.py模块实现crawl_task调用。 -首先,我们需要列出数据的输入url_list。代码如下: - - - url_list = ['http://www.google.com', - 'http://br.bing.com', - 'http://duckduckgo.com', - 'http://github.com', - 'http://br.search.yahoo.com'] - -创建manage_crawl_task方法。 - - def manage_crawl_task(url_list): - async_result_dict = {url: app.send_task('tasks.crawl_task', - args=(url,), queue='webcrawler_queue', - routing_key='webcrawler_queue') for url in url_list} - for key, value in async_result_dict.items(): - if value.ready(): - logger.info("%s -> %s" % (key, value.get())) - else: - logger.info("The task [%s] is not ready" % - value.task_id) - -和之前创建的manage_fibo_task方法一样,async_result_dict字典包含当前URL和AsyncResult结果。然后我们检查任务的状态获取任务结果。 - -现在我们在__main__中调用该方法: - - if __main__ == '__main__': - #manage_sqrt_task(4) - #manage_fibo_task(input_list) - manage_crawl_task(url_list) - -运行task_dispatcher.py代码,在服务器端有如下输出: - -![](图片链接地址) - -最后,客户端的输出如下: - -![](图片链接地址) - -Celery是一个强大的工具,在本章我们只是用到了基本的东西。更多的内容建议自己在真实的项目中动手去尝试。 diff --git "a/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\350\216\267\345\276\227\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\232\204\351\241\271.md" "b/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\350\216\267\345\276\227\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\232\204\351\241\271.md" deleted file mode 100644 index 42ab1dd..0000000 --- "a/\347\254\254\344\270\203\347\253\240/\347\224\250Celery\346\235\245\350\216\267\345\276\227\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260\345\210\227\347\232\204\351\241\271.md" +++ /dev/null @@ -1,58 +0,0 @@ -让我们再一次计算多个输入的斐波那契数列的项,每个都用分布式的方法。现在的方法和之前的方法改变很小。 -我们将终止Celery的执行(Ctrl+C)并且在tasks.py模块(之前创建过)加入fibo_task任务。 - - @app.task - def fibo_task(value): - a, b = 0,1 - for item in range(value): - a, b = b, a + b - message = "The Fibonacci calculated with task id %s" \ - " was %d" % (fibo_task.request.id, a) - Return (value, message) - -通过\得到任务的ID,请求对象是task的对象,task对象提供了task执行的上下文。通过上下文可以得到task的ID等信息。 - -在tasks.py模块加入了新的任务之后,再一次初始化Celery,结果如下图: - -![](图片链接地址) - -现在我们把fibo_task任务装载到Celery server,我们将在客户端实现对该任务的调用。 - -在task_dispatcher.py模块,我们会申明input_list,如下: - - input_list = [4, 3, 8, 6, 10] - -和前面的做法一样,定义manage_fibo_task方法: - - def manage_fibo_task(value_list): - async_result_dict = {x: app.send_task('tasks.fibo_task', - args=(x,)) for x in value_list} - for key, value in async_result_dict.items(): - logger.info("Value [%d] -> %s" % (key, value.get()[1])) - -在manage_fibo_task方法中,创建了一个叫做async_result_dict的字典,key是传入的要计算的值,value是send_task方法返回的AyncResult对象。通过这个方法,我们可以查看任务的结果和状态。 -最后,遍历字典得到输入值和输出结果。AsyncResult的get方法能够得到结果。 -get()方法会阻塞进程。一个好的方法是调用ready()方法来检查结果是否返回了。 -上述循环可以修改为如下; - - for key, value in async_result_dict.items(): - if value.ready(): - logger.info("Value [%d] -> %s" % (key, value.get()[1])) - else: - logger.info("Task [%s] is not ready" % value.task_id) - -不同的任务会有不同的延迟时间,为了防止无限等待,可以用get(timeout=x)方法设置超时。 -最后,添加manage_fibo_task的调用,传入input_list。代码如下: - - if __name__ == '__main__': - #manage_sqrt_task(4) - manage_fibo_task(input_list) - -当我们执行task_dispatcher.py后,输入如下: - -![](图片链接地址) - -在客户端有如下输出: - -![](图片链接地址) - diff --git "a/\347\254\254\344\270\211\347\253\240/ReadMe.md" "b/\347\254\254\344\270\211\347\253\240/ReadMe.md" deleted file mode 100644 index db6ae98..0000000 --- "a/\347\254\254\344\270\211\347\253\240/ReadMe.md" +++ /dev/null @@ -1,6 +0,0 @@ -##Identifying a Parallelizable Problem - -* 识别一个并行化的问题 -* 从多个输入中得到斐波那契最大的值 -* 爬取网页 -* 小结 \ No newline at end of file diff --git "a/\347\254\254\344\270\211\347\253\240/\344\273\216\345\244\232\344\270\252\350\276\223\345\205\245\344\270\255\345\276\227\345\210\260\346\226\220\346\263\242\351\202\243\345\245\221\346\234\200\345\244\247\347\232\204\345\200\274.md" "b/\347\254\254\344\270\211\347\253\240/\344\273\216\345\244\232\344\270\252\350\276\223\345\205\245\344\270\255\345\276\227\345\210\260\346\226\220\346\263\242\351\202\243\345\245\221\346\234\200\345\244\247\347\232\204\345\200\274.md" deleted file mode 100644 index 709b561..0000000 --- "a/\347\254\254\344\270\211\347\253\240/\344\273\216\345\244\232\344\270\252\350\276\223\345\205\245\344\270\255\345\276\227\345\210\260\346\226\220\346\263\242\351\202\243\345\245\221\346\234\200\345\244\247\347\232\204\345\200\274.md" +++ /dev/null @@ -1,25 +0,0 @@ -##从多个输入中得到斐波那契最大的值 - -众所周知,斐波那契数列被定义如下: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%203/fibonacci.png?raw=true) - -实际上,按照从0到10计算斐波那契的值,结果将会是0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 和55. - -用迭代法计算斐波那契最高的值的Python代码如下: -```python - def fibonacci(input): - a, b = 0, 1 - for item in range(input): - a, b = b, a + b - return a -``` -斐波那契函数为一个特定的输入的数据计算一个最大的斐波那契值。让我们想象一个场景,在这个场景中,需要计算斐波那契的值,而本网站将会从一个用户那里接收到几个输入。假设用户提供一个数组的值作为输入,因此使这些计算按顺序将会很有趣。但是假使是100万个用户同时发出请求那么将会怎么样?在这种情况下,一些用户不得不等待很长一段时间才能得到他们的答案。 - -让我们只考虑先前文中的Python实现的斐波那契函数代码。我们怎么在有一个数组数据输入的情况下使用并行性来实现它?在前一章已经展示过多种实现的方式,这里我们使用其中一种方式-数据分解。我们可以将数组分为几个单元,每个单元关联一个任务然后被一个worker执行。下图描述了一个建议的解决方案: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%203/Parallel%20Fibonacci%20for%20multiples%20inputs.png?raw=true) - - -> 对读者的建议,实现使用缓存机制存储计算的值来避免浪费CPU时间来完成练习。我们建议使用像memcached的缓存。 - diff --git "a/\347\254\254\344\270\211\347\253\240/\346\200\273\347\273\223.md" "b/\347\254\254\344\270\211\347\253\240/\346\200\273\347\273\223.md" deleted file mode 100644 index 7b1f3c5..0000000 --- "a/\347\254\254\344\270\211\347\253\240/\346\200\273\347\273\223.md" +++ /dev/null @@ -1,5 +0,0 @@ -##总结 - -在这一章节,我们学习了并行中出现的常见问题以及可能的解决方案。下面将会题展示任何使用不同的Python库来解决上面展示的问题。 - -在下一章节,当使用threading模块,我们专注于的线程的解决方案,在用到mutliprocess模块时,解决方案也会涉及到使用多进程,以此类推。 diff --git "a/\347\254\254\344\270\211\347\253\240/\347\210\254\345\217\226\347\275\221\351\241\265.md" "b/\347\254\254\344\270\211\347\253\240/\347\210\254\345\217\226\347\275\221\351\241\265.md" deleted file mode 100644 index 7f21e61..0000000 --- "a/\347\254\254\344\270\211\347\253\240/\347\210\254\345\217\226\347\275\221\351\241\265.md" +++ /dev/null @@ -1,17 +0,0 @@ -##爬取网页 - -贯穿本书的另一个正在研究的问题是是实现一个并行的网络爬虫。一个网络爬虫由一个个浏览网页并从页面获取信息的电脑程序组成。被分析的场景是一个问题,在这个问题中,一个顺序执行的网络爬虫会被提供一组可变数量的统一资源定位器(URLs)为参数,它需要检索每个URL所提供的所有链接。假设输入的URLs的数量相当的大,我们可以在以下方法中寻找并行性的解决方案: - -1. 将所有的URLs分成组组合到一个数据结构中。 -2. 把这些URLs分配給多个任务,这样写任务会爬取每个URL中的包含信息。 -3. 将这些任务分派给多个并行的workers来执行。 -4. 由于前一阶段的结果必须传给下一个阶段,这将会改进未加工的存储的数据,因此保存它们关联它们到原始的URLs。 - -正如我们在上面编号的步骤中观察到的为了一个提出的解决方案,可以与以下两个方法结合: - -* 数据分解:这发生在我们划分和关联URLs到任务上。 -* 用管道进行任务分解:这包含三个阶段的管道,这发生在我们链接接收、存储以及组织爬取的结果的任务。 - -下图显示了解决方案: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%203/Parallel%20Web%20crawler.png?raw=true) diff --git "a/\347\254\254\344\272\214\347\253\240/ReadMe.md" "b/\347\254\254\344\272\214\347\253\240/ReadMe.md" deleted file mode 100644 index fd0b339..0000000 --- "a/\347\254\254\344\272\214\347\253\240/ReadMe.md" +++ /dev/null @@ -1,11 +0,0 @@ -##Designing Parallel Algorithms - -* 设计并行算法 -* 分治技术 -* 使用数据分解 -* 用管道分解任务 -* 处理和映射 - * 识别独立的任务 - * 识别需要数据交换的任务 - * 负载均衡 -* 小结 \ No newline at end of file diff --git "a/\347\254\254\344\272\214\347\253\240/\344\275\277\347\224\250\346\225\260\346\215\256\345\210\206\350\247\243.md" "b/\347\254\254\344\272\214\347\253\240/\344\275\277\347\224\250\346\225\260\346\215\256\345\210\206\350\247\243.md" deleted file mode 100644 index 0853f01..0000000 --- "a/\347\254\254\344\272\214\347\253\240/\344\275\277\347\224\250\346\225\260\346\215\256\345\210\206\350\247\243.md" +++ /dev/null @@ -1,10 +0,0 @@ -##使用数据分解 - -并行化问题的方法之一是通过数据分解。想象一下有这么一个场景,在这个场景中我们要以标量4乘以一个2x2矩阵(这个矩阵被称为矩阵A).在一个顺序执行系统中,我们将一个接一个的执行每个乘法的操作,最后生成所有指令的最终结果。根据矩阵A的大小,这个问题的顺序解决方案可能是旷日持久的。然而,当数据分解被应用的时候,我们可以想象矩阵A被分解为一个一个小的部分,这些分片数据被相关的workers以并行的方式接受并处理。下图以一个2x2矩阵乘以一个标量值的栗子说明了数据分解应用的概念: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%202/Data%20decomposition%20in%20a%20matrix%20example.png?raw=true) - -上图中出现的矩阵相乘的问题有一定的对称性,每个必要的操作的结果是由一个单独的worker执行的,而且每个worker执行同样数量的操作来解决问题。然而,在现实世界中,worker的数量和已分解的数据数量的关系是不对称的,这将直接影响解决方案的性能。最后,每个worker所产生的结果必须整合起来以便使程序最终输出意义结果。为了进行这种整合,workers之间需要进行信息交换或是共享状态。 - -> 数据分解的粒度选择将会影响一个解决方案的性能。 - diff --git "a/\347\254\254\344\272\214\347\253\240/\345\210\206\346\262\273\346\212\200\346\234\257.md" "b/\347\254\254\344\272\214\347\253\240/\345\210\206\346\262\273\346\212\200\346\234\257.md" deleted file mode 100644 index 89d2a1b..0000000 --- "a/\347\254\254\344\272\214\347\253\240/\345\210\206\346\262\273\346\212\200\346\234\257.md" +++ /dev/null @@ -1,8 +0,0 @@ -##分治技术 - -当你面对一个复杂的问题时,你想做的第一件事就是分解问题以便可以确定哪些部分可以独立的被处理。一般说来,在一个解决方案中,可并行的部分可以被不同的workers分开或者是分布式的执行。分治技术涉及到将一个完整的问题递归的分为不可再分的可被解决的小问题。排序算法,例如归并排序和快排都可以使用这种方法解决。 - -下图显示了在6个元素的向量中应用归并排序,可以看到使用了分治技术: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%202/Merge%20sort%20(divide%20and%20conquer).png?raw=true) - diff --git "a/\347\254\254\344\272\214\347\253\240/\347\224\250\347\256\241\351\201\223\345\210\206\350\247\243\344\273\273\345\212\241.md" "b/\347\254\254\344\272\214\347\253\240/\347\224\250\347\256\241\351\201\223\345\210\206\350\247\243\344\273\273\345\212\241.md" deleted file mode 100644 index b98e3a9..0000000 --- "a/\347\254\254\344\272\214\347\253\240/\347\224\250\347\256\241\351\201\223\345\210\206\350\247\243\344\273\273\345\212\241.md" +++ /dev/null @@ -1,16 +0,0 @@ -##用管道分解任务 - -管道技术被用来组织多个任务,这些诶任务合作的方式来共同解决一个问题。管道将大型的任务分割成独立并行的小任务。管道模型可以类比成汽车工厂的装配线,只不过输入代替底盘称为加工的原始材料。原料经过不同的生产阶段,几个worker一个接一个执行不同的操作直到产生最终结束。这个模型和顺序范式的开发类似,任务一个接一个的作用于数据上,正常情况下,一个任务以上一个任务的结果为输入。那么这个模型和顺序技术的区别是什么呢?管道技术中的每个阶段都拥有自己的workers,并且这些workers是以并行的方式执行的。 - -计算上下文的一个栗子可能是一个批量处理图片,并将抽取出的数据存入数据库的系统。我们将有以下实际的顺序: - -* 接受输入的图像并且以对这些图片以并行的方式进行排列,这些图片将在第二阶段进行处理 -* 解析图像,并且有用的信息将会被送到第三阶段 -* 在第三阶段,过滤器被并行的应用在图像上 -* 来自第三阶段的数据结果被保存在数据库中 - -> 每个阶段的管道技术都用自己workers独立的执行。然而,它建立了数据通信机制,以便进行信息的交换。 - -下图展示了管道的概念: - -![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%202/The%20pipeline%20technique.png?raw=true) diff --git "a/\347\254\254\344\272\224\347\253\240/ReadMe.md" "b/\347\254\254\344\272\224\347\253\240/ReadMe.md" deleted file mode 100644 index 72de8f5..0000000 --- "a/\347\254\254\344\272\224\347\253\240/ReadMe.md" +++ /dev/null @@ -1,8 +0,0 @@ -##Using Multiprocessing and ProcessPoolExecutor -上章中,我们学习了如何使用threading模块解决两个问题。通过本章的学习,我们将学习如何使用multiprocessing模块解决上章的两个问题,我们将使用和上章类似的接口实现。然而,我们会使用多进程机制。 -本章将覆盖如下一个知识点: -* 理解进程的概念 -* 理解多进程通信 -* 使用多进程解决斐波那契数列多输入问题 -* 使用ProcessPoolExecutor模块设计网络爬虫 - diff --git "a/\347\254\254\344\272\224\347\253\240/\344\275\277\347\224\250\345\244\232\350\277\233\347\250\213\350\247\243\345\206\263\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227\345\244\232\350\276\223\345\205\245\351\227\256\351\242\230.md" "b/\347\254\254\344\272\224\347\253\240/\344\275\277\347\224\250\345\244\232\350\277\233\347\250\213\350\247\243\345\206\263\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227\345\244\232\350\276\223\345\205\245\351\227\256\351\242\230.md" deleted file mode 100644 index feddd93..0000000 --- "a/\347\254\254\344\272\224\347\253\240/\344\275\277\347\224\250\345\244\232\350\277\233\347\250\213\350\247\243\345\206\263\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227\345\244\232\350\276\223\345\205\245\351\227\256\351\242\230.md" +++ /dev/null @@ -1,66 +0,0 @@ -下面我们将使用多进程解决多输入情况下的斐波那契数列问题,而不是之前我们使用的多线程的方法。 -multiprocessing_fibonacci.py程序使用multiprocessing模块,为了顺利执行,还导入了如下模块: -```python -import sys, time, random, re, requests -import concurrent.futures -from multiprocessing import, cpu_count, current_process, Manager -``` - -上面一些导入模块在之前的章节中提及过,然而,下面的这些模块我们需要特别注意: -* cpu_count: 该方法允许获得机器cpu的数量 -* current_process: 该方法可以获得当前进程的信息,比如进程名称 -* Manager: 该类通过对象的方式允许功在多进程之间共享python对象 - -下面的代码中我们可以注意到第一个方法有点不同,它将产生15个1到20之间的整数,这些整数将被当作fibo_dict的key使用。 -接下来让我们一起来看producer_task方法,如下: -```python -def producer_task(q, fibo_dict): - for i in range(15): - value = random.randint(1, 20) - fibo_dict[value] = None - - print("Producer [%s] putting value [%d] into queue.." % (current_process().name, value)) - q.put(value) - -``` - -下面将定义一个函数来计算fibo_dict中key对应的斐波那契数列值,和之前章节介绍计算斐波那契序列值不同的是,这里把fibo_dict当作参数传入不同的processes。 - -下面是consumer_task方法,如下: -```python -def consumer_task(q, fibo_dict): - while not q.empty(): - value = q.get(True, 0.05) - a, b = 0, 1 - for item in range(value): - a, b = b, a+b - fibo_dict[value] = a - print("consumer [%s] getting value [%d] from queue..." % (current_process().name, value)) -``` - -更进一步,我们来看main函数中的代码,main函数中下面几个变量被定义: -* data_queue: 该参数由multiprocessing.Queueu来创建,是进程安全的 -* number_of_cpus: 该参数由multiprocessing.cpu_count方法获得,获得机器cpu的个数 -* fibo_dict: 这个字典类型变量从Manager实例获得,保存多进程计算结果 - -然后,我们将创建producer进程,并传入data_queue队列,data_queue队列值由producer_task方法获得: -```python -producer = Process(target=producer_task, args=(data_queue, fibo_dict)) -producer.start() -producer.join() -``` - -我们可以注意到Process实例的初始化过程和我们之前的Thread实例初始化过程类似。初始化函数接收target参数作为进程中要执行的函数,和args参数作为target传入的函数的参数。接下来我们通过start方式开始进程,然后使用join方法,等待producer进程执行完毕。 - -下面一块代码中,我们将定义consumer_list队列,存入初始化过的consumer进程。使用list存储consumer对象的原因是在所有进程结束开始后调用join方法。循环中的每一个worker被调用后,下一个worker将等待上一个worker执行完毕后才开始执行,下面代码将描述这一过程: -```python -consumer_list = [] -cpu = cpu_count() -print(cpu) -for i in range(cpu): - consumer = Process(target=consumer_task, args=(data_queue, fibo_dict)) - consumer.start() - consumer_list.append(consumer) -[consumer.join() for consumer in consumer_list] -``` -最终我们将迭代输出fibo_dict中的结果,如下面截图所示: diff --git "a/\347\254\254\344\272\224\347\253\240/\344\275\277\347\224\250\350\277\233\347\250\213\346\261\240\345\256\236\347\216\260\347\275\221\347\273\234\347\210\254\350\231\253.md" "b/\347\254\254\344\272\224\347\253\240/\344\275\277\347\224\250\350\277\233\347\250\213\346\261\240\345\256\236\347\216\260\347\275\221\347\273\234\347\210\254\350\231\253.md" deleted file mode 100644 index b4c7e5e..0000000 --- "a/\347\254\254\344\272\224\347\253\240/\344\275\277\347\224\250\350\277\233\347\250\213\346\261\240\345\256\236\347\216\260\347\275\221\347\273\234\347\210\254\350\231\253.md" +++ /dev/null @@ -1,50 +0,0 @@ -##使用ProcessPoolExecutor模块设计网络爬虫 -正如concurrent.futures模块提供的ThreadPoolExecutor类方便了操控多线程程序,多进程同样有一个类叫ProcessPoolExecutor。ProcessPoolExecutor类同样由concurrent.futures包提供,我们将使用该类执行我们的网络爬虫程序。为了完成这个任务,我们创建了一个叫process\_pool\_executor\_web\_crawler.py的python模块。 - -代码要导入的包如之前章节中我们介绍的,如requests, Manager模块等等。对于任务的定义,我们延用上章线程程序中的代码,只是改动其中小部分代码,还有一点不一样,进程程序中,我们向任务函数传入参数,而不是使用全局变量。代码如下所示: - -group\_urls\_task函数定义如下: -```python -def group_urls_task(urls, result_dict, html_link_regex) -``` - -crawl\_task函数定义如下: -```python -def crawl_task(url, html_link_regex) - -``` - -现在我们再来看下面一小部分代码,与前一章比有了细微的变化。在main函数中,我们获得Manager类的实例,该实例使得我们获得可以被多进程共享的queue和dict。我们使用Manager.Queue()方法获得queue实例来存储我们将要爬得的url。使用Manager.dict()方法获取dict,来存储爬虫的结果。下面的代码将展示上面的定义: -```python -if __name__ == '__main__': - manager = Manager() - urls = manager.Queue() - urls.put("http://br.bing.com/") - urls.put("https://github.com") - result_dict = manager.dict() -``` - -接着,我们将定义爬虫程序中将要用到的正则表达式和介绍如何获取机器的cpu个数,程序如下: -```python -html_link_regex = \ - re.compile('') -number_of_cpus = cpu_count() -``` - -最后一块代码中,我们会注意到concurrent.futures模块中各API签名具有很强的一致性。下面的代码正是我们上章使用ThreadPoolExecutor模块时使用到的。我们只需把ThreadPoolExecutor变为ProcessPoolExecutor,就能改变程序内部行为并解决计算密集型进程的GIL问题。注意下面的程序,创建ProcessPoolExecutor时会根据机器cpu数限定进程的数目。第一个exucutor是为了收集将被爬的URL,把这些URLs保存在一个字典中,key为url而value为None。第二个executor执行爬虫程序。 -首先是第一个executor: -```python -with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as group_link_processes: - for i in range(urls.qsize()): - group_link_processes.submit(group_urls_task, urls, result_dict, html_link_regex) -``` - -第二个executor程序如下: -```python -with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as crawler_link_processes: - future_tasks = {crawler_link_processes.submit(crawl_task, url, html_link_regex): url for url in result_dict.keys()} - for future in concurrent.futures.as_completed(future_tasks): - result_dict[future.result()[0]] = future.result()[1] -``` - -程序运行结果如下图: diff --git "a/\347\254\254\344\272\224\347\253\240/\345\256\236\347\216\260\345\244\232\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/\347\254\254\344\272\224\347\253\240/\345\256\236\347\216\260\345\244\232\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" deleted file mode 100644 index e07cea7..0000000 --- "a/\347\254\254\344\272\224\347\253\240/\345\256\236\347\216\260\345\244\232\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ /dev/null @@ -1,43 +0,0 @@ -python中的multiprocessing模块支持两种方式在进程间通信,都是基于消息传递机制。之前我们介绍过,由于缺乏同步机制因此不得不采取消息传递机制,在进程间传递复制的数据。 -##使用multiprocessing.Pipe模块 - -pipe管道在两个端点间搭建一种通信机制,通过在进程间建立通道使得进程间可以相互通信。 -为了更好的说明multiprocessing.Pipe对象的使用方法,我们将介绍一个python程序,包含A,B两个进程。进程A发送1到10之间的一个数给进程B,进程B将打印该数,接下来让我们一步步介绍这个程序。 -我们首先导入一些我们程序中需要的包,如下: -```python -import os, random -from multiprocessing import Process, Pipe -``` - -通过os模块的os.getpid()方法使得我们获得进程的PID。os.getpid()将以一种透明的方式返回程序的PID,在我们的程序中,它分别返回producer\_task进程和consumer\_task进程的PID。 -下面我们将定义producer\_task方法,该方法返回1到10之间的一个随机数。producer\_task方法的关键是调用conn.send方法,conn以参数的形式在主函数中被传給producer\_task方法。producer\_task方法如下: -```python -def producer_task(conn): - value = random.randint(1, 10) - conn.send(value) - print('Value [%d] send by PID [%d]' % (value, os.getpid())) - conn.close() -``` -consumer进程将要执行的任务也很简单,它唯一的任务就是接收A进程传递过来的参数,接收本进程的PID,最终打印出来。consumer进程的中传入的consumer_task方法如下: -```python -def consumer_task(conn) - print('Value [%d] received by PID [%d]' % (conn.recv(), os.getpid())) -``` - -最后一块将介绍如何调用Pipe()方法创建两个连接对象分别用于producer进程和consumer进程,然后通过参数形式各自传递到consumer\_task方法和producer\_task方法中去,主函数具体如下所是: -```python -if __name__ == '__main__': - producer_conn, consumer_conn = Pipe() - consumer = Process(target=consumer_task,args=(consumer_conn,)) - producer = Process(target=producer_task,args=(producer_conn,)) - - consumer.start() - producer.start() - - consumer.join() - producer.join() -``` -定义好进程之后,我们便可以调用进程对象的start方法开始执行进程,join方法用于分别等待producer进程和consumer进程执行完毕。下面的截图中我们将看到程序的输出: - -##理解multiprocessing.Queue模块 -之前小节中我们分析了如何在进程间创建通信通道来传递消息,现在我们将分析如何更有效的传递消息,这里我们使用mutilprocessing模块下的Queue对象。multoprocessing.Queue对象方法和queue.Queue对象方法类似。然后内在实现却不尽相同,比如multiprocess模块使用了内部线程feeder,把缓冲区中的数据传入目标进程相关连接的管道中。管道和队列机制均使用了消息传递机制,节省了使用同步机制带来的开销。 diff --git "a/\347\254\254\344\272\224\347\253\240/\346\200\273\347\273\223.md" "b/\347\254\254\344\272\224\347\253\240/\346\200\273\347\273\223.md" deleted file mode 100644 index a236e2d..0000000 --- "a/\347\254\254\344\272\224\347\253\240/\346\200\273\347\273\223.md" +++ /dev/null @@ -1,2 +0,0 @@ -本章中我们介绍了多进程的概念,并使用多进程解决了两个小问题,分别是并行计算斐波那契数列值和设计网络爬虫。 -下一章节我们将使用parallel Python模块执行多进程任务,parallel模块并不是python的内部模块。我们还将学习进程间通信相关的知识,使用pipes在进程间通信。 diff --git "a/\347\254\254\344\272\224\347\253\240/\347\220\206\350\247\243\350\277\233\347\250\213\347\232\204\345\256\232\344\271\211.md" "b/\347\254\254\344\272\224\347\253\240/\347\220\206\350\247\243\350\277\233\347\250\213\347\232\204\345\256\232\344\271\211.md" deleted file mode 100644 index 1ef4f34..0000000 --- "a/\347\254\254\344\272\224\347\253\240/\347\220\206\350\247\243\350\277\233\347\250\213\347\232\204\345\256\232\344\271\211.md" +++ /dev/null @@ -1,18 +0,0 @@ -线程是操作系统中程序执行和资源调度的基本单位。程序的执行由进程来管理,所涉及到的资源包括数据区,子进程,私有栈以及和其他进程间通信。 - -##理解进程模型 -进程中定义了相关的信息和资源来确保对进程的操纵和控制,操作系统有一个叫PCB的结构来专门来存储这些信息和资源。例如,PCB结构保存如下的信息: -1. Process ID: 这是一个无符号整型数据,标识操作系统中的唯一进程。 -2. 程序计数器: 对应下一条要执行的程序指令地址。 -3. I/O信息: 包含一组打开的文件和进程相关的设备。 -4. 内存分配: 该区域保存进程已经使用内存空间、为该进程预留的内存空间和页表信息。 -5. CPU调度: 该区域保存进程优先级信息(and points to the staggering queues)。 -6. 优先级: 定义进程获取CPU资源的优先级。 -7. 当前状态: 表述该进程是准备状态、等待状态还是运行状态。 -8. CPU申请: 保存栈指针和其他信息。 - -##定义进程状态 -进程整个生命周期具有三种状态,分别如下: -* 运行状态: 进程正占用cpu资源。 -* 准备状态: 处于进程队列中的进程已经准备好获取cpu资源。 -* 等待状态: 进程正在等待执行中的任务所需的I/O操作。 diff --git "a/\347\254\254\345\205\253\347\253\240/ReadMe.md" "b/\347\254\254\345\205\253\347\253\240/ReadMe.md" deleted file mode 100644 index 9ef2083..0000000 --- "a/\347\254\254\345\205\253\347\253\240/ReadMe.md" +++ /dev/null @@ -1 +0,0 @@ -##Doing Things Asynchronously \ No newline at end of file diff --git "a/\347\254\254\345\205\253\347\253\240/\344\275\277\347\224\250asyncio.md" "b/\347\254\254\345\205\253\347\253\240/\344\275\277\347\224\250asyncio.md" deleted file mode 100644 index 6f7a1c3..0000000 --- "a/\347\254\254\345\205\253\347\253\240/\344\275\277\347\224\250asyncio.md" +++ /dev/null @@ -1,65 +0,0 @@ -我们可以定义asyncio是一个Python中驱动异步编程的模块。ayncio模块使用下列组合来实现异步编程: -- Event loop: asyncio模块允许每个进程一个事件循环。 -- Coroutines(协程): asyncio的官方文档中指出,coroutine是遵循一定规则的发生器。它最吸引人的特点是在执行的时候能够暂停等待外部处理,当外部处理完成后又可以从原来的位置恢复执行。 -- Futures: Futures代表尚未完成的processing。 -- Tasks: 是asyncio.Future的子类,用于管理coroutines。 - -除了这些机制,asyncio还为应用开发提供了很多其他机制,比如传输和协议,可以使用TCP、SSL、UDP和管道进行通信。关于asyncio更多的信息请查看 https://docs.python.org/3.4/library/asyncio.html。 - -### 理解coroutines和rutures -为了在asyncio中定义coroutine,我们使用@asyncio.coroutine装饰器。为了执行一个操作I/O或者其他可能阻塞循环事件的计算,我们必须使用`yield from`语法来暂停coroutine。但是暂停和恢复的机制怎样工作?Coroutine和asyncio.Future对象一起工作。我们可以总结操作如下: -- Coroutine初始化,asyncio.Future在内部实例化或者作为参数传给coroutine。 -- 到达coroutine使用yield from的地方,coroutine暂停来等待yield from引发的计算。yield from实例等待yield from\构建。 -- 当yield from引发的计算结束,coroutine执行与coroutine关联的asyncio.Future对象的set_result(\)方法,通知事件循环coroutine可以被恢复。 - - -#### 使用coroutine和asyncio.Future -下面是使用coroutine和asyncio.Future对象的一些例子: - import asyncio - @asyncio.coroutine - def sleep_coroutine(f): - yield from asyncio.sleep(2) - f.set_result("Done!") -在上述代码中,定义了一个协程sleep_coroutine,它接收一个asyncio.Future对象作为参数。在sleep_coroutine中,asyncio.sleep(2)会让协程睡眠2秒,asyncio.sleep已经和asyncio兼容。 -在主函数中创建asyncio.Future对象,创建event loop对象。 - if __name__ == '__main__': - future = asyncio.Future() - loop = asyncio.get_event_loop() - loop.run_until_complete(sleep_coroutine(future)) - -> event loop执行的时候,任务和协程才会执行。 - -在最后一行,loop.run_until_complete(sleep_coroutine(future)),很明显就是运行直到sleep_coroutine结束。 - -#### 使用asyncio.Task -asyncio.Task是asyncio.Future的子类,目的是管理协程。以下是一个例子,多个asyncio.Task将会在事件循环中被创建和分派。 - - import asyncio - @asyncio.coroutine - def sleep_coro(name, seconds=1): - print("[%s] coroutine will sleep for %d second(s)…" - % (name, seconds)) - yield yfrom asyncio.sleep(seconds) - print("[%s] done!" % name) - -sleep_coro协程会接收两个参数,name用来标识协程,seconds用来定义睡眠时间。 - -在主函数中,定义了一个包含三个asyncio.Task对象的列表: - - if __name__ == '__main__': - tasks = [asyncio.Task(sleep_coro('Task-A', 10)), - asyncio.Task(sleep_coro('Task-B')), - asyncio.Task(sleep_coro('Task-C'))] - loop.run_until_complete(asyncio.gather(*tasks)) - -程序的运行结果如下: -![](图片链接地址) - -值得注意的是,程序的输出表明任务执行的顺序和申明的顺序一致,它们都不能阻塞event loop。 - -#### 使用和asyncio不兼容的库 - -asyncio是python新加入的模块,一些库还不能很好的兼容。我们重新实现之前章节的例子asyncio_task_sample.py,用time.sleep替换asyncio.sleep。运行结果如下: - -![](图片链接地址) - diff --git "a/\347\254\254\345\205\253\347\253\240/\345\274\202\346\255\245\347\232\204\345\201\232\344\272\213.md" "b/\347\254\254\345\205\253\347\253\240/\345\274\202\346\255\245\347\232\204\345\201\232\344\272\213.md" deleted file mode 100644 index df8a1a1..0000000 --- "a/\347\254\254\345\205\253\347\253\240/\345\274\202\346\255\245\347\232\204\345\201\232\344\272\213.md" +++ /dev/null @@ -1,7 +0,0 @@ -在之前的章节我们学习了用Celery框架分发任务,并且在同一个网络的不同机器上并行执行。现在我们将学习异步编程、事件循环和协程,它们都在Python3.4的asyncio模块占有重要的地位。 -本章将覆盖以下内容: - -- 阻塞、非阻塞和异步操作 -- 理解事件循环 -- 使用异步IO(asyncio) - diff --git "a/\347\254\254\345\205\253\347\253\240/\346\200\273\347\273\223.md" "b/\347\254\254\345\205\253\347\253\240/\346\200\273\347\273\223.md" deleted file mode 100644 index f392db1..0000000 --- "a/\347\254\254\345\205\253\347\253\240/\346\200\273\347\273\223.md" +++ /dev/null @@ -1,7 +0,0 @@ -在本章节,我们学习了异步、阻塞、非阻塞编程。为了了解这些行为,我们使用asyncio模块的基本机制写了一些例子。 - -asyncio模块是对python异步编程进行革命的一个尝试。吉多范罗苏姆在探索性选择和提取基本机制为这些选择提供清晰的API方面非常成功。`yield from`语法产生是为了增强一些使用协程的程序的表现力,使程序员免去写回调函数的负担。除此之外,asyncio模块拥有与其它应用程序集成的能力。 - -快到本书的结束了,写这本书还是很有挑战性的,希望它对你有所帮助。本书中有很多东西没有介绍,比如 IPython, mpi4py, Greenlets, Eventlets, 等等。 - -基于本书提供的内容,你可以自己做实验比较不同的工具。几乎在本书的每一个章节都用了两个相关例子来介绍,这也说明了Python可以在不改变核心代码的基础上灵活的替换不同的工具。 diff --git "a/\347\254\254\345\205\253\347\253\240/\347\220\206\350\247\243\344\272\213\344\273\266\345\276\252\347\216\257.md" "b/\347\254\254\345\205\253\347\253\240/\347\220\206\350\247\243\344\272\213\344\273\266\345\276\252\347\216\257.md" deleted file mode 100644 index c8935cc..0000000 --- "a/\347\254\254\345\205\253\347\253\240/\347\220\206\350\247\243\344\272\213\344\273\266\345\276\252\347\216\257.md" +++ /dev/null @@ -1,42 +0,0 @@ -为了理解事件循环的概念,我们必须了解其内部结构。 -我们将用资源描述符代表套接口描述符和文件描述符。 - -### 轮询功能 - -不同的操作系统为了监控一个或多个资源描述符都实现了轮询功能。轮询方法是事件循环的基础,轮询方法会通知感兴趣的事件,资源描述符就会准备好做交互。然而,感兴趣的可能并不能完成理想的操作。 - -linux有下列轮询方法: - -- select(): POSIX实现有以下几个缺点: - - 监控资源描述符的数量有限 - - O(n)时间复杂度,n代表连接的客户端数 -- poll(): select()的增强版,有以下特点: - - 允许监控更大范围的资源描述符 - - O(n)时间复杂度 - - 支持更多类型的监控事件 - - 和select()对比,可以复用entry数据 -- epoll(): Linux非常强大的方法,拥有O(1)时间复杂度。epoll()方法通过[epoll_wait](http://refspecs.linux-foundation.org/LSB_4.0.0/LSB-Core-generic/LSB-Core-generic/libc-epoll-wait-1.html)()提供两种监控事件。为了定义两种行为,我们假想一个场景,其中生产者往socket写数据,消费者等待数据。 - - 水平触发:当消费者调用epoll_wait(),它将得到资源描述符的状态并且立即返回,表明触发或者不触发读操作。水平触发和事件的状态有关而不是事件本身。 - - 边缘触发:epoll_wait()调用只有当写事件完成之后才会返回。所以边缘触发和事件有关。 - - -轮询方法工作的步骤如下: -1. 创建poller对象 -2. poller中注册或者不注册1个或多个资源描述符 -3. 轮询方法在poller对象中执行 - -> Poller是一个提供使用轮询方法的抽象接口 - -### 使用事件循环 - -我们可以定义事件循环来简化使用轮询方法来监控事件。事件循环利用poller对象,使得程序员不用控制任务的添加、删除和事件的控制。 -事件循环使用回调方法来知道事件的发生。例如,有一个资源描述符A,当一个写事件在A中发生就会调用一个回调函数。一些实现了事件循环的应用如下: -- Tornado web server ( http://www.tornadoweb.org/en/stable/ ) -- Twisted ( https://twistedmatrix.com/trac/ ) -- asyncio ( https://docs.python.org/3.4/library/asyncio.html ) -- Gevent ( http://www.gevent.org/ ) -- Eventlet ( https://pypi.python.org/pypi/eventlet ) - - - - diff --git "a/\347\254\254\345\205\253\347\253\240/\347\220\206\350\247\243\351\230\273\345\241\236\351\235\236\351\230\273\345\241\236\345\222\214\345\274\202\346\255\245\346\223\215\344\275\234.md" "b/\347\254\254\345\205\253\347\253\240/\347\220\206\350\247\243\351\230\273\345\241\236\351\235\236\351\230\273\345\241\236\345\222\214\345\274\202\346\255\245\346\223\215\344\275\234.md" deleted file mode 100644 index ccfb500..0000000 --- "a/\347\254\254\345\205\253\347\253\240/\347\220\206\350\247\243\351\230\273\345\241\236\351\235\236\351\230\273\345\241\236\345\222\214\345\274\202\346\255\245\346\223\215\344\275\234.md" +++ /dev/null @@ -1,18 +0,0 @@ -理解任务执行的不同方式对于构建一个可伸缩的解决方案非常重要。正确的运用异步、阻塞和非阻塞操作能大大改善系统的响应时间。 - -### 理解阻塞操作 - -可以用银行职员服务客户的例子来看阻塞操作。当轮到客户的号码时,银行职员就只为该客户服务直到服务完成。银行职员不能同时为多个客户办理业务。当只有2名银行职员但是每小时来100名顾客时,进度就会很缓慢。这个例子就描述了阻塞操作,当一个任务要等待另一个任务结束时,阻塞其对资源的访问。 - -### 理解非阻塞操作 - -非阻塞操作和异步操作很容易混淆,它们是不同的概念。用一个现实的场景来说明,比如你去银行咨询一个业务,银行职员说现在还没有结果,你稍后再来或者过几天再来。这就是非阻塞操作。 - -> 非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,之后再retry。 - - -### 理解异步操作 - -假设有两个银行职员,没个职员有10个助理。这时,如果来了一个客户,他需要办理的业务耗时比较长,那么就请一个助理到后台单独为该客户服务。这样就不会阻塞其他客户。 - -> 注册一个回调函数,当条件满足时会触发该函数。 diff --git "a/\347\254\254\345\205\255\347\253\240/ReadMe.md" "b/\347\254\254\345\205\255\347\253\240/ReadMe.md" deleted file mode 100644 index b094786..0000000 --- "a/\347\254\254\345\205\255\347\253\240/ReadMe.md" +++ /dev/null @@ -1,7 +0,0 @@ -##Utilizing Parallel Python -之前的章节,我们学习了怎么使用multiprocessing模块和ProcessPoolExecutor模块来解决两个问题.这章将介绍命名管道并展示如何使用Parallel Python(PP)模块实现多进程的并行任务。 -本章会覆盖下面几个知识点: -* 理解进程间通信 -* 了解Parallel Python(PP) -* 在SMP架构上使用PP计算斐波那契序列 -* 使用PP创建分布式的网络爬虫 diff --git "a/\347\254\254\345\205\255\347\253\240/\344\272\206\350\247\243pp\346\250\241\345\235\227.md" "b/\347\254\254\345\205\255\347\253\240/\344\272\206\350\247\243pp\346\250\241\345\235\227.md" deleted file mode 100644 index 2d3fcd5..0000000 --- "a/\347\254\254\345\205\255\347\253\240/\344\272\206\350\247\243pp\346\250\241\345\235\227.md" +++ /dev/null @@ -1,35 +0,0 @@ -##了解PP模块 -上一章中,我们介绍了直接使用系统调用创建进程间通讯的方法,这是一种很低层的机制. 并且它只在Linux或Unix环境下才有效. 接下来我们会使用一个名为PP的python模块来创建IPC通讯,这种通讯不仅仅可以在本地进程间进行,还能通过计算机网络在物理上彼此分散的进程间进程. - -关于PP模块的文档并不多,可以在http://www.parallelpython.com/component/option.com_smf/ 中找到相关文档和FAQ. API提供了大量的使用该工具的说明. 十分的简洁明了 - -使用PP模块的最大优势在于它提供了一个抽象层. PP模块的重要特征如下所示: -- 自动探测进程的数量并以此改进负载均衡 -- 可以在运行期改变要投入的处理器数量 -- 运行期自动均衡负载 -- 通过网络自动探测资源 - -PP模块有两种方式来执行并行代码. 第一种方式基于SMP架构,即在同一台机器上有多个处理器/核心. 第二中方式将网络中的各个机器配置,整合成区块,并将任务分配給这些机器去运行. 无论哪一种方式,进程间消息交换的过程都是抽象的. 这使得我们无需关系其低层的实现方式到底是通过管道还是socket. 我们只需要使用回调函数来通过参数和函数的方式来交换信息就行了. 下面给个例子. - -在PP的API中有一个名为Server的类,使用该类可以实现在本地和远程的进程间封装和分派任务. Server的构造函数(\_\_init\_\_)中有几个参数比较重要: - -* ncpus: 该参数用于指定执行任务的工作进程数量. 若没有指定该参数,则会自动根据机器上处理器/核心的数量来创建工作进程,以后话资源的使用 - -* ppservers: 该参数是一个元组,该元组的元素为并行Python执行服务器(PPES)的名称或IP地址. 一个PPES由连入网络的机器组成. 且该机器通过ppsever.py共组运行并等待待执行的任务. - -其他参数的说明请参阅http://www.parallelpython.com/content/view/15/30/ - -Server对象的实例拥有很多方法,其中submit方法允许我们分配任务到各个工作进程. submit函数具有如下签名: -```python -submit(self, func, args=(), depfuncs=(), modules=(), - callback=None, callbackargs=(), group='default', - globals=None) -``` - -在submit方法中,我们集中关注以下几个参数: -* func: 该函数会被本地进程或远程服务器执行 -* args: 该参数提供了了执行func函数时的参数 -* modules: 该参数说明远程代码(remote code)或进程为了调用func函数,需要导入哪些模块. 例如若被分配的函数用到了time模块,则modules=('time',) -* callback :执行完func后的回调函数,func的执行结果会作为其函数参数. 常用于对func的执行结果作进一步加工 - -还有其他的参数将会在下一张分析代码时进行说明. diff --git "a/\347\254\254\345\205\255\347\253\240/\344\275\277\347\224\250pp\346\250\241\345\235\227\345\210\233\345\273\272\345\210\206\345\270\203\345\274\217\347\275\221\347\273\234\347\210\254\350\231\253.md" "b/\347\254\254\345\205\255\347\253\240/\344\275\277\347\224\250pp\346\250\241\345\235\227\345\210\233\345\273\272\345\210\206\345\270\203\345\274\217\347\275\221\347\273\234\347\210\254\350\231\253.md" deleted file mode 100644 index 05c6789..0000000 --- "a/\347\254\254\345\205\255\347\253\240/\344\275\277\347\224\250pp\346\250\241\345\235\227\345\210\233\345\273\272\345\210\206\345\270\203\345\274\217\347\275\221\347\273\234\347\210\254\350\231\253.md" +++ /dev/null @@ -1,38 +0,0 @@ -目前我们已经使用pp组件在本机上实现多进程并发,接下来我们将在分布式环境下使用pp组件,分布式硬件环境如下: - -- Iceman-Thinkpad-X220: Ubuntu 13.10 -- Iceman-Q47OC-500P4C: Ubuntu 12.04 LTS -- Asgard-desktop: Elementary OS - -我们将在如上列举的三台机器上测试pp组件在分布式环境下的使用。对此,我们实现了分布式网络爬虫。web_crawler_pp_cluster.py方法中,将input_list列举的URL分发到本地以及远端进程执行,web_crawler_pp_cluster.py中的回调函数将组织这些URL以及以及通过它们找到的前三个连接(URL)。 - -让我们分析代码,一步步理解怎样实现上述功能。首先import使用到的库以及定义数据结构。然后定义input_list列表用于存放入口URL和result_dict存放爬取结果。代码如下所示: - -接下来分析aggregate_results方法,该方法还是作为回调函数,相对于前一小结的aggreate_results方法,改变不多。返回meesage的格式的发生了变化,aggregate_resullt方法的传入参数变成tuple,分别保存执行该方法的PID号,hostname和找到的前三个URL。代码如下: - -接下来定义crawl_task方法,作为Server类方法。和之前章节的crawl_task方法功能类似,依据传入的RUL获取网页上的其他链接(RUL),唯一的不同是返回值是tuple,代码如下: - -在main方法和callback方法定义之后,我们需要初始化Server类实例,以至于能够在分布式环境下执行网络爬虫任务。我们注意到pp.Server类有三个参数,第一个参数是执行Server类方法的IP或hostname,我们的例子中,除了本机之外,还需要定义另外两台机器的IP和hostname,定义如下所示: - -pp.Server类的初始化如下: - -我们注意到初始化有三个参数,第一个参数不同的是被赋值为1,使得pp组件创建一个本地进程,如果需要的话,把其他任务转发到远端机器上执行。第二个参数是之前定义的ppservers。第三个参数定义socket连接超时时间,通常为了测试目的,我们把timeout时间设置很长,防止因为网络超时而导致socket连接终止。 - -Server类创建之后,我们将遍历url_list,并提交crawl_task方法。 - -相对于之前的之前章节,变化是传入的组件较之于斐波那契序列不同。 - -我们需要等待网络爬虫执行完毕,得到爬虫结果。 - -执行程序之前,我们需要执行远端机器上的ppserver.py模块。Python ppserver.py -a -d 为shell命令。-a选项用于设置自动发现,使得client自动发现网络中没有设置ip的server。-d选项设置debug模式,显示程序执行过程中产生的log。 - -接下来我们定义输出格式: - -1. 首先,下面的屏幕显示main节点的执行信息,包括执行和分发远端任务。 -2. 然后,执行ppserver.py脚本,执行任务信息如下屏幕内容所示。 -3. 在之前屏幕中,我们打印了有趣的统计信息,包括在远端机器上执行的任务数量,每个人物执行的时间和每个远端机器上的任务总数。截图上反应的另外一个信息是,我们应该限制callback函数任务量,因为callback函数都是在main节点上执行,这可能会成为成为整个系统的瓶颈,当然也取决于不同的应用场景。 -4. 下面的截图是的debug模式下ppserver.py脚本在iceman-Q470C-500P4C机器上的的执行日志。 -5. 下面截图是debug模式下ppserver.py脚本在asgard-desktop机器上的执行日志。 - - - diff --git "a/\347\254\254\345\205\255\347\253\240/\345\234\250SMP\346\236\266\346\236\204\344\270\212\344\275\277\347\224\250pp\346\250\241\345\235\227\350\256\241\347\256\227\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227.md" "b/\347\254\254\345\205\255\347\253\240/\345\234\250SMP\346\236\266\346\236\204\344\270\212\344\275\277\347\224\250pp\346\250\241\345\235\227\350\256\241\347\256\227\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227.md" deleted file mode 100644 index e69de29..0000000 diff --git "a/\347\254\254\345\205\255\347\253\240/\346\200\273\347\273\223.md" "b/\347\254\254\345\205\255\347\253\240/\346\200\273\347\273\223.md" deleted file mode 100644 index 6200dca..0000000 --- "a/\347\254\254\345\205\255\347\253\240/\346\200\273\347\273\223.md" +++ /dev/null @@ -1,2 +0,0 @@ -本章我们学习了使用更低层次的组件在没有直接关系的进程间通信。此外,我们介绍了PP模块,该模块提供本地进程和远程服务器进程通信的方法。PP模块用于创建简单,小型,并发和分布式的python应用。 -下章我们将学习怎么使用Celery执行并发,分布式任务。 diff --git "a/\347\254\254\345\205\255\347\253\240/\347\220\206\350\247\243\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" "b/\347\254\254\345\205\255\347\253\240/\347\220\206\350\247\243\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" deleted file mode 100644 index ea29ec0..0000000 --- "a/\347\254\254\345\205\255\347\253\240/\347\220\206\350\247\243\350\277\233\347\250\213\351\227\264\351\200\232\344\277\241.md" +++ /dev/null @@ -1,81 +0,0 @@ -##理解进程间通讯 -IPC是允许多进程间通信的一种机制。 -有许多IPC的实现方式,他们依赖于不同架构的运行环境。运行在同一台机器上的进程可以由多种进程间通信方式,比如共享内存、消息队列和管道。如果多进程处于物理上分布式集群上上,我们可以使用RPC的方式。 -第五章中我们介绍了Multiprocessing和ProcessPoolExecutor模块,学习了常规的管道的用法。我们学习了同一个父进程的各个子进程间通信方式,但是有时候我们需要在不相关的进程间传递信息(子进程不共有同一个父进程),我们会想,通过不相关进程的地址空间,是否能够在它们之间建立通信。然而一个进程不能获取另外一个进程的地址空间,因此我们需要使用一种新的机制——命名管道。 - -###探索命名管道 -对于POSIX系统,例如linux,我们可以把所有东西归结为文件,我们每操作一个任务,我们可以找到一个文件与之对应,同时还有一个文件描述符与该文件相联系,通过文件描述符就可以操作该文件. - -命名管道是一种允许通过某些特殊文件的文件描述符实现IPC通讯的机制.这些特殊文件使用特殊的模式(例如先入先出)来读写数据.在对信息的管理上命名管道不同于常规管道,命名管道通过文件系统中的特殊文件及其文件描述符来实现,而普通管道是在内存中创建的. - -###在python中使用命名管道 -在python中使用命名管道很容易. 下面我们会通过两个非直接通讯的程序来展示如何使用命名管道. 第一个程序名为write\_to\_named_pip.py,它的作用是写一条22个字节的消息到管道中,该消息包括一个信息字符串及产生消息的PID. 第二个程序名为read\_from\_named\_pip.py, 它会从管道中读取消息并展示消息内容及其PID. - -在执行的最后,read\_from\_named\_pipe.py进程会显示一条形如"I pid [] received a message => Hello from pid [the PID of writer process"的消息 - -为了展示读写进程的相互依赖关系,我们会在两个独立的控制台上运行这两个程序. 在展示结果前,让我们先分析一下两个程序的代码. - -####往命名管道写入数据 -在python中,命名管道是通过系统调用来实现的. 下面我们会逐行解释write\_to\_named\_pipe.py中代码的功能. - -我们首先导入os模块,这样我们在后面的代码中才能调用系统调用. -```python -import os -``` - -接下来我们会解释\_\_main\_\_代码块,在该代码块中创建了命名管道以及一个用于存储消息的FIFO的特殊文件. \_\_main\_\_代码块中的第一行代码定义了命名管道的标签. -```python -named_pipe = "my_pipe" -``` - -接下来我们检查该命名管道是否已经存在,若不存在,则调用mkfifo系统调用来创建这个命名管道. -```python -if not os.path.exists(named_pipe): - os.mkfifo(named_pipe) -``` - -mkfifo调用会创建一个特殊的文件,该文件对通过命名管道读写的消息实现了FIFO机制. - -我们再以一个命名管道和一个行如"Hello from pid [%d]"的消息来作为参数调用函数write\_message. 该函数会将消息写入到(作为参数传递给它的)命名管道所代表的文件中. write\_message函数定义如下: -```python -def write_message(input_pipe, message): - fd = os.open(input_pipe, os.O_WRONLY) - os.write(fd, (message % str(os.getpid()))) - os.close(fd) -``` - -我们可以观察到,在函数的第一行,我们调用一个系统调用:open. 该系统调用若成功的话会返回一个文件描述符, 通过该文件描述符我们就能够读写那个FIFO文件中的数据. 请注意,我们可以通过flags参数控制打开FIFO文件的模式. 由于write\_message函数紧紧需要写数据,因此我们使用如下代码: -```python -fd = os.open(input_pipe, os.O_WRONLY) -``` - -在成功打开命名管道后,我们使用下面代码写入消息: -```python -os.write(fd, (message % os.getpid())) -``` - -最后,请一定记着使用close关闭通讯渠道,这样才能释放被占用的计算机资源. -```python -os.close(fd) -``` - -####从命名管道读取数据 -我们实现了read\_from\_pipe.py来读取命名管道. 当然,改程序也需要借组os模块才能操作命名模块. 改程序的主要代码很简单:首先,我们定义了所使用命名管道的标签,该标签需要与写进程所用的命名管道同名. -```python -named_pipe = "my_pipe" -``` - -然后,我们调用read\_message函数,该函数会读取write\_to\_named\_pipe.py写入的内容. read\_message函数的定义如下: -```python -def read_message(input_type): - fd = os.open(input_pipe, os_RONLY) - message = ( - "I pid [%d] received a message => %s" - % (os.getpid(), os.read(fd, 22)) - os.close(fd) - return message -``` - -上面代码中,open调用相比无需再介绍了,唯一没见过的只有read调用. 该调用从命名管道中读取指定字节的内容. 这里我们从文件描述符中读取22个字节的内容. 消息被读取出来之后,又被该函数作为返回值返回. 最后依然记得调用close调用来关闭通讯渠道. - -最终,下面的截屏显示了write\_to\_named\_pip和read\_from\_named\_pipe程序的执行结果. diff --git "a/\347\254\254\345\233\233\347\253\240/\344\275\277\347\224\250concurrent.futures\346\250\241\345\235\227\347\210\254\345\217\226web\344\277\241\346\201\257.md" "b/\347\254\254\345\233\233\347\253\240/\344\275\277\347\224\250concurrent.futures\346\250\241\345\235\227\347\210\254\345\217\226web\344\277\241\346\201\257.md" deleted file mode 100755 index a32b134..0000000 --- "a/\347\254\254\345\233\233\347\253\240/\344\275\277\347\224\250concurrent.futures\346\250\241\345\235\227\347\210\254\345\217\226web\344\277\241\346\201\257.md" +++ /dev/null @@ -1,75 +0,0 @@ - ##使用concurrent.futures模块爬取web信息 - -下面的章节会实现一个并行的web爬虫. - -在实现时,我们会应道concurrent.futures模块中一个很有意思的类,叫做ThreadPoolExecutor 在上一章节的例子中,我们分析了parallel\_fibonacci.py是如何实现并发的,它只是以最原始的方式来使用进程,在某一特定的时候需要我们手工来创建和初始化一个个线程. 然而在大型程序中还想这样手工管理线程就太困难了. 在开发大型程序时,我们常常要用到线程池机制. 线程池是一种用于在进程中管理预先创建的多个线程的一种数据结构. 使用线程池的目的是为了复用线程,这样就可以避免不断的创建线程所照成的资源浪费. - -基本上和前一章节一样, 我们将会设计一个算法,该算法分阶段地执行一些任务,并且这些任务也会相互影响. 下面,让我们分析一下这个并发网络爬虫的代码 - -在导入必要的模块,并设置好日志文件后,我们使用内置模块re来创建一个正则表达式(re模块的完整文档可以在http://docs.python.org/3/howto/regex.html中找到). 我们会使用该正则表达式来过滤爬取阶段返回的连接集合. 相关代码如下所示: - -```python -html_link_regex = \ -re.compile('') -``` - -接下来我们创建一个同步队列来模拟输入数据. 然后我们创建一个名为result\_dict的字典实例. In this, we will correlate the URLs and their respective links as a list structure. 相关代码如下: - -```python -urls = queue.Queue() -urls.put('http://www.google.com') -urls.put('http://br.bing.com/') -urls.put('https://duckduckgo.com/') -urls.put('https://github.com/') -urls.put('http://br.search.yahoo.com/') -result_dict = {} -``` - -再接下来我们定义一个名为group\_urls\_task的函数,该函数用于从同步队列中抽取出URL并存入result\_dict的key值中. 另一个应该留意的细节是,我们调用Queue的get方法是,带了两个参数,第一个参数为True表示阻塞其他线程访问这个同步队列,第二个参数是0.05表示阻塞的超时事件,这样就防止出现由于同步队列中没有元素而等待太长事件的情况出现. 毕竟,在某些情况下,你不会想化太多的时间来等待新元素的到来. 相关代码如下: - -```python -def group_urls_task(urls): - try: - url = urls.get(True, 0.05) - result_dict[url] = None - logger.info("[%s] putting url [%s] in dictionary..." % ( - threading.current_thread().name, url)) - except queue.Empty: - logging.error('Nothing to be done, queue is empty') -``` - -现在我们需要有一个在爬行阶段执行的任务,该任务将每个url作为参数传递给一个名为crawl\_task的函数. 当将URL所指页面中的所有连接都保存下里之后,爬行阶段就算是完成了. 爬行过程中会返回一个元组,且该元组的第一个元素就是传递給crawl\_task函数的URL参数. 在这个步骤中,会从URL所指页面中抽取出一个连接的列表. 获取URL所指网页的内容需要用到request模块(关于request模块的官方文档请参见https://pypi.python.org/pypi/requests ) - -```python -def crawl_task(url): - links = [] - try: - request_data = requests.get(url) - logger.info("[%s] crawling url [%s] ..." % ( - threading.current_thread().name, url)) - links = html_link_regex.findall(request_data.text) - except: - logger.error(sys.exc_info()[0]) - raise - finally: - return (url, links) -``` - -进一步分析代码,我们会发现创建了一个concurrent.futures模块中定义的ThreadPoolExecutor对象(关于ThreadPoolExecutor对象的详细信息,请参见 http://docs.python.org/3.3/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor)在这个ThreadPoolExecutor对象的构造函数中有一个名为max\_workers的参数,该参数决定了该executor所包含的线程池中的线程数. Within the stage of removal of the URLs from the synchronized queue and insertion of keys into result\_dict, the choice was between using three worker threads.(*这一段不知道怎么翻译*) 该数量可以根据问题的大小而改变. 定义完ThreadPoolExecutor之后,我们还使用with语句来保证结束的清理动作会被执行. 这些清理动作会在超出with语句的作用域时被执行. 在ThreadPoolExecutor对象的作用域内,我们遍历同步队列并且通过ThreadPoolExecutor对象的submit方法来将同步队列作为包含URL的队列引用传递給group\_urls\_task函数. 总之,submit方法接受一个要执行的回调函数及其参数并返回一个Future对象,该Future对象会在未来的某个时候执行该回调函数. 就我们的例子中,该回调函数就是group\_urls\_task,而参数就是同步队列的引用. 然后线程池中的线程就会并行且异步地执行Future对象中预定的函数. 相关代码如下: - -```python -with concurrent.futures.ThreadPoolExecutor(max_workers=3) as\ - group_link_threads: - for i in range(urls.qsize()): - group_link_threads.submit(group_urls_task, urls) -``` - -随后,我们还要再创建一个ThreadPoolExecutor对象, 不过这一次我们使用上一阶段中group\_urls\_task所产生的key值作为参数来执行爬行的动作. 这一次我们所使用的代码有些不同. -```python -future_tasks = {crawler_link_threads.submit(crawl_task, url): url - for url in result_dict.keys()} -``` - -我们映射了一个名为future\_tasks的临时字典对象. 该字段对象包含了submit方法所创建的Future对象,且创建这些Future对象时所使用的参数是result\_dict中的每个URL. 也就是说,根据result\_dict中的每个key,我们创建了future\_tasks中的每个任务. 映射完这个字典对象后,我们还需要搜集这些Future对象执行的结果. 搜集执行结果的方法是,使用concurrent.futures.as\_completed(fs,timeout=None)函数来循环遍历执行futre\_tasks中的各个对象, concurrent.futures.as\_completed(fs, timeout=None)方法会返回一个Future对象的迭代器. 这样我们可以遍历得到这些Future对象的执行结果. 在ThreadPoolExecutor的最后,我们在每个爬行线程中都调用了Future对象的result()方法. 在我们这个例子中,该方法返回结果元组. 这样我们最终得到的future\_tasks结果如下所示. - -又一次,我们可以发现每个线程池中的线程执行是乱序的,但这不重要,重要的是,result\_dict中输出的内容就是最终结果. diff --git "a/\347\254\254\345\233\233\347\253\240/\344\275\277\347\224\250threading\346\250\241\345\235\227\350\247\243\345\206\263\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227\345\244\232\350\276\223\345\205\245\351\227\256\351\242\230.md" "b/\347\254\254\345\233\233\347\253\240/\344\275\277\347\224\250threading\346\250\241\345\235\227\350\247\243\345\206\263\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227\345\244\232\350\276\223\345\205\245\351\227\256\351\242\230.md" deleted file mode 100644 index f3abd50..0000000 --- "a/\347\254\254\345\233\233\347\253\240/\344\275\277\347\224\250threading\346\250\241\345\235\227\350\247\243\345\206\263\346\226\220\346\263\242\351\202\243\345\245\221\345\272\217\345\210\227\345\244\232\350\276\223\345\205\245\351\227\256\351\242\230.md" +++ /dev/null @@ -1,111 +0,0 @@ -##使用多线程解决斐波那契序列多输入问题 -接下来我们将实践python多线程的使用。任务是并行处理斐波那契序列多输入值。为了把问题说清楚,我们将把输入分成四部分,四个线程分别处理一个输入数据。算法描述如下: - -1. 首先使用一个列表存储四个待输入值,这些值将被放入对于线程来说互相锁定的数据结构。 -2. 输入值被放入可被锁定的数据结构之后,负责处理斐波那契序列的线程将被告知可以被执行。这时,我们可以使用python线程的同步机制Condition模块(Condition模块对共享变量提供线程之间的同步操作),模块详情请参考:[http://docs.python.org/3/library/threading.html#threading.Condition](http://docs.python.org/3/library/threading.html#threading.Condition)。 -3. 当每个线程结束斐波那契序列的计算后,分别把结果存入一个字典。 - -接下来我们将列出代码,并且讲述其中有趣的地方: - -代码开始处我们加入了对编码额外的支持,导入logging, threading和queue模块。此外,我们还定义了我们例子中用到主要数据结构。一个字典,被命名为fibo_dict,将用来存储输入输出数据,输入数据为key,计算结果(输出数据)为值。我们同样定义了一个队列对象,该对象中存储线程间的共享数据(包括读写)。我们把该对象命名为shared\_queue。最后我们定义一个列表模拟程序的四个输入值。代码如下: - -```python - - #coding: utf-8 - import logging, threading - from queue import Queue - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(message)s') - ch = logging.StreamHandler() - ch.setLevel(logging.DEBUG) - ch.setFormatter(formatter) - logger.addHandler(ch) - fibo_dict = {} - shared_queue = Queue() - input_list = [3, 10, 5, 7] -``` - -接下来的一行代码,我们从threading模块中定义了一个Condition对象,该对象根据一定的条件同步各线程存取资源的操作。 - -```python - - queue_condition = threading.Condition() -``` - -使用Condition对象用于控制线程的创建队列。 - -下一块代码定义了一个很多线程都需要调用的方法,我们把它命名为fibonacci\_task。fibonacci\_task方法接收condition对象作为线程获取shared\_queue中值的协议。方法中,我们使用了with语句(关于更多with语句的用法,请参考[http://docs.python.org/3/reference/compound_stmts.html#with](http://docs.python.org/3/reference/compound_stmts.html#with))简化控制内容。如果没有with语句,我们则需要显式的使用锁,并且最后释放锁。有了with操作,代码隐式的在代码最开始获得锁,并在代码最后释放锁。fibonacci方法中接下来的是逻辑处理相关代码,告诉当前线程,当shared\_queue为空时,等待。wait()方法是condition中的主要方法之一。线程将一直等待,直到被通知shared\_queue可以被使用。一旦满足shared\_queue可以被使用的条件,当前线程将接收shared\_queue中的值作为输入计算斐波那契序列的值,最后把输入和输出作为key和value存入fibo\_dict字典。最后,我们调用task_done()方法,通知某一个任务已经被分离并执行。代码如下: - -```python - - def fibonacci_task(condition): - with condition: - while shared_queue.empty(): - logger.info("[%s] - waiting for elements in queue.." - % threading.current_thread().name) - condition.wait() - else: - value = shared_queue.get() - a, b = 0, 1 - for item in range(value): - a, b = b, a + b - fibo_dict[item] = a # 这里书中是fibo_dict[value] = a,但是觉得重复赋值没有意义 - shared_queue.task_done() - logger.debug("[%s] fibonacci of key [%d] with - result [%d]" % - (threading.current_thread().name, value, - fibo_dict[value])) -``` - -我们定义的第二个函数是queue\_task,该函数被负责计算shared\_queue的值的线程所调用。我们看到condition对象作为获得shared\_queue的协议。input\_list中的每一个值都将被插入到shared\_queue中去。当所有的值都被插入到shared\_queue中后,告知负责计算斐波那契序列的方法shared\_queue已经可以使用。 - -```python - - def queue_task(condition): - logging.debug('Starting queue_task...') - with condition: - for item in input_list: - shared_queue.put(item) - logging.debug("Notifying fibonacci_task threads - that the queue is ready to consume..") - condition.notifyAll() -``` - -接下来我们将创建四个线程等待shared\_queue可以被使用条件。线程将执行target参数作为回调函数,代码如下: - -```python - - threads = [threading.Thread( - daemon=True, target=fibonacci_task, - args=(queue_condition,)) for i in range(4)] -``` - -接着我们使用thread对象的start方法开始线程: - -```python - - [thread.start() for thread in threads] -``` - -然后我们创建一个线程处理shared\_queue,然后执行该线程。代码如下: - -```python - - prod = threading.Thread(name='queue_task_thread', daemon=True, - target=queue_task, args=(queue_condition,)) - prod.start() -``` - -最后,我们多计算斐波那契序列的所有线程都调用join()方法,使用join()方法的目的是,让主线程等待子线程的调用,直到所有子线程执行完毕之后才结束子线程。 - -```python - - [thread.join() for thread in threads] -``` - -程序的执行结果如下: - -注意到,第一个fibonacci\_task线程被创建和初始化后,它们进入等待状态。同时,queue\_task线程被创建并且生成shared\_queue队列。最后,queue\_task方法告知fibonacci_task线程可以执行它们的任务。 - -注意到,程序每次执行的过程都不一样,这也是多线程的特性之一。 diff --git "a/\347\254\254\345\233\233\347\253\240/\345\256\232\344\271\211\344\273\200\344\271\210\346\230\257\347\272\277\347\250\213.md" "b/\347\254\254\345\233\233\347\253\240/\345\256\232\344\271\211\344\273\200\344\271\210\346\230\257\347\272\277\347\250\213.md" deleted file mode 100755 index 1d709c8..0000000 --- "a/\347\254\254\345\233\233\347\253\240/\345\256\232\344\271\211\344\273\200\344\271\210\346\230\257\347\272\277\347\250\213.md" +++ /dev/null @@ -1,50 +0,0 @@ -##什么是线程 -线程表示的是进程中不同的执行流程. 让我们把一个程序相信成是一个蜂箱,该程序的一个动作是将划分收集到这个蜂箱内. 这些搜集动作是由许多个工蜂同时进行的. 这个例子中,工蜂扮演的就是线程的角色, 它们工作在进程内部并且共享资源来完成它们的任务. - -同一个进程中的线程共享内存空间. 因此开发者的任务就是控制和访问这些内存区域. - -###使用线程的优势和劣势 -是否使用线程需要权衡利弊,这依赖于用于实现解决方案的编程语言和操作系统. - -使用线程的优势如下所示: -- 同一进程的不同线程之间交流的速度,数据单元(data location)的速度和共享信息的速度都很快 -- 创建线程的花费要远远少于创建进程的花费,这时因为它无需拷贝主进程上下文环境中的那些信息 -- 通过处理器的缓存机制可以优化内存存取,这样就能够充分利用数据局部性(data locality)的优势 - -使用线程的劣势如下: -- 数据共享可以加速通讯. 然而开发新手使用线程也很容引进难以解决的错误 -- 数据共享限制了解决方案的灵活性. 若想将其迁移到分布式架构上,会是件很头疼的事情. 总的来说,它限制了算法的可扩展性 - -[就Python编程语言来说, 计算密集型的线程会由于GIL的存在而影响程序的性能] - -###理解不同类型的线程 -存在两种类型的线程:内核线程与用户线程. 其中,内核线程是指由操作系统创建和管理的线程. 内核线程的上下文切换,调度和销毁都由当前操作系统的内核来管理. 而对于用户线程,这些东西都由开发者来控制. - -每种线程都有其优势: - -内核线程的优势如下: -- 一个内核线程其实就是一个进程. 因此即使一个内核线程被阻塞了,其他的内核线程也照样运行 -- 不同的内核线程可以运行在不同的CPU上 - -内核线程的劣势如下: -- 创建线程和线程间同步的消耗太大 -- 实现依赖于平台 - -用户线程的优势如下: -- 用户线程的创建和线程间同步的开销较少 -- 用户线程是平台无关的 - -用户线程的劣势如下: -- 同一进程中的所有用户线程都对应一个内核线程. 因此,若该内核线程被阻塞,则所有相应的用户线程都会被阻塞 -- 不同用户线程无法运行在不同CPU上 - -###定义线程的状态 -在线程的生命周期中,有5中状态: -- 新建: 该过程的主要动作就是创建一个新线程, 创建完新线程后,该线程被发送到待执行的线程队列中 -- 运行: 该状态下,线程获取到并消耗CPU资源 -- 就绪: 该状态下,线程在待执行的线程队列中排队,等待被执行 -- 阻塞: 该状态下,线程由于等待某个事件(例如I/O操作)的出现而被阻塞. 这时线程并不使用CPU -- 死亡: 该状态下,线程释放执行时使用的资源并结束整个线程的生命周期 - -###是使用threading模块还是_thread模块 -Python提供了两个模块来实现基于系统的线程:_thread模块(该模块提供了使用线程相关的较低层的API; 它的文档可以在http://docs.python.org/3.3/library/_thread.html 找到)和threading模块(该模块提供了使用线程相关的较高层的API; 它的文档可以在 http://docs.python.org/3.3/library/threading.html 中找到). threading模块提供的接口要比_thread模块的结构更友好一些. 至于具体选择哪个模块取决于开发者, 若开发者觉得在低层操作线程,实现自己的线程池,处理所及其其他原始特性(features)更随手一些的话,他/她会偏向使用_thread,否则threading会是更明智的选择