├── Parallel Programming with Python.pdf ├── README.md ├── 第一章 ├── ReadMe.md ├── 为什么使用并行编程.md ├── 发现Python并行编程的工具.md ├── 在并行编程通信.md ├── 小心Python GIL.md ├── 并行、并发与分布式编程的对比分析.md ├── 总结.md ├── 探索并行化的几种模式.md └── 识别并行编程的问题.md ├── 第七章 ├── ReadMe.md ├── 分发简单任务.md ├── 建立环境.md ├── 总结.md ├── 根据任务类型定义队列.md ├── 理解Celery.md ├── 理解Celery架构.md ├── 用Celery来分发任务.md ├── 用Celery来构建一个分布式网络爬虫系统.md └── 用Celery来获得斐波那契数列的项.md ├── 第三章 ├── ReadMe.md ├── 从多个输入中得到斐波那契最大的值.md ├── 总结.md ├── 爬取网页.md └── 识别一个可并行的问题.md ├── 第二章 ├── ReadMe.md ├── 使用数据分解.md ├── 分治技术.md ├── 处理和映射.md ├── 总结.md ├── 用管道分解任务.md └── 设计并行算法.md ├── 第五章 ├── ReadMe.md ├── 使用多进程解决斐波那契序列多输入问题.md ├── 使用进程池实现网络爬虫.md ├── 实现多进程间通信.md ├── 总结.md └── 理解进程的定义.md ├── 第八章 ├── ReadMe.md ├── 使用asyncio.md ├── 异步的做事.md ├── 总结.md ├── 理解事件循环.md └── 理解阻塞非阻塞和异步操作.md ├── 第六章 ├── ReadMe.md ├── 了解pp模块.md ├── 使用pp模块创建分布式网络爬虫.md ├── 在SMP架构上使用pp模块计算斐波那契序列.md ├── 总结.md └── 理解进程间通信.md └── 第四章 ├── ReadMe.md ├── 使用concurrent.futures模块爬取web信息.md ├── 使用threading模块解决斐波那契序列多输入问题.md ├── 定义什么是线程.md └── 总结.md /Parallel Programming with Python.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voidly/Parallel-Programming-with-Python/c14a6e132d1e6c5bb2d836b86d394e03d602ff6c/Parallel Programming with Python.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parallel-Programming-with-Python 2 | Parallel Programming with Python,a book about parallel in python,We will translate it into Chinese. 3 | 4 | 话说在ICT实习的时期,因感兴趣于Python,于是和两位朋友 [@tanghaodong25](https://github.com/tanghaodong25) 以及 [@gorlf](https://github.com/gorlf) 商量翻译一本Python相关的书籍,挑来选去感觉Parallel Programming with Python比较赞,于是拍板翻译之,另外感谢[@lujun9972](https://github.com/lujun9972)的加入,由于平时开发比较忙加上比较懒,耗时几个月才完成~ 5 | 6 | ## Contributors 7 | 8 | * [@尐鱼](https://github.com/Voidly) 9 | * [@tanghaodong25](https://github.com/tanghaodong25) 10 | * [@gorlf](https://github.com/gorlf) 11 | * [@lujun9972](https://github.com/lujun9972) 12 | 13 | 另外因水平有限,翻译中难免有所疏漏与瑕疵,欢迎指出,欢迎吐槽~若要进行转载请注明作者,毕竟翻译也是很花费时间与精力的不是,所以请尊重我们的劳动成果~ 14 | 15 | ## [第一章:并行、并发分布式编程对比分析](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) 16 | 17 | * #### [为什么使用并行编程](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) 18 | 19 | * #### [探索并行化的几种模式](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) 20 | 21 | * #### [并行编程间的通信](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) 22 | 23 | * #### [识别并行编程的问题](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) 24 | 25 | * #### [发现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) 26 | 27 | * #### [小心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) 28 | 29 | * #### [总结](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) 30 | 31 | 32 | ## [第二章:设计并行算法](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) 33 | 34 | * #### [分治技术](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) 35 | 36 | * #### [使用数据分解](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) 37 | 38 | * #### [用管道分解任务](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) 39 | 40 | * #### [处理和映射](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) 41 | 42 | * #### [总结](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) 43 | 44 | 45 | ## [第三章:识别一个可并行的问题](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) 46 | 47 | * #### [从多个输入中得到斐波那契最大的值](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) 48 | 49 | * #### [爬取网页](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) 50 | 51 | * #### [总结](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) 52 | 53 | 54 | ## [第四章:使用threading和concurrent.futures模块](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%9B%9B%E7%AB%A0/ReadMe.md) 55 | 56 | * #### [定义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) 57 | 58 | * #### [使用多线程解决斐波那契序列多输入问题](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) 59 | 60 | * #### [使用网络爬虫实现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) 61 | 62 | * #### [总结](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) 63 | 64 | 65 | ## [第五章:使用多进程和进程池](https://github.com/Voidly/Parallel-Programming-with-Python/tree/master/%E7%AC%AC%E4%BA%94%E7%AB%A0) 66 | 67 | * #### [理解进程的定义](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) 68 | 69 | * #### [实现多进程间通信](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) 70 | 71 | * #### [使用多进程解决斐波那契序列多输入问题](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) 72 | 73 | * #### [使用进程池实现网络爬虫](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) 74 | 75 | * #### [总结](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) 76 | 77 | 78 | ## [第六章:利用并行Python](https://github.com/Voidly/Parallel-Programming-with-Python/blob/master/%E7%AC%AC%E5%85%AD%E7%AB%A0/ReadMe.md) 79 | 80 | * #### [理解进程间通信](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) 81 | 82 | 83 | * #### [发现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) 84 | 85 | * #### [在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) 86 | 87 | * #### [使用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) 88 | 89 | * #### [总结](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) 90 | 91 | 92 | ## [第七章:用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) 93 | 94 | * #### [理解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) 95 | 96 | * #### [理解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) 97 | 98 | * #### [建立环境](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) 99 | 100 | * #### [分发简单任务](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) 101 | 102 | * #### [用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) 103 | 104 | * #### [根据任务类型定义队列](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) 105 | 106 | * #### [用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) 107 | 108 | * #### [总结](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) 109 | 110 | ## [第八章:异步做的事](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) 111 | 112 | * #### [理解阻塞非阻塞和异步操作](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) 113 | 114 | * #### [理解事件循环](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) 115 | 116 | * #### [使用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) 117 | 118 | * #### [总结](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) 119 | 120 | -------------------------------------------------------------------------------- /第一章/ReadMe.md: -------------------------------------------------------------------------------- 1 | ##Contextualizing Parallel, Concurrent, and Distributed Programming 2 | 3 | - 并行、并发分布式编程对比分析 4 | - 为什么使用并行编程 5 | - 探索并行化的几种方式 6 | - 并行编程间的通信 7 | * 理解状态共享 8 | * 理解消息传递 9 | - 识别并行编程的问题 10 | * 死锁 11 | * 饥饿 12 | * 竞态条件 13 | - 发现Python并行编程的工具 14 | - Python线程模块 15 | - Python多进程模块 16 | - Python并行模块 17 | - Celery-一个分布式任务队列 18 | - 小心PythonGIL 19 | - 总结 20 | 21 | -------------------------------------------------------------------------------- /第一章/为什么使用并行编程.md: -------------------------------------------------------------------------------- 1 | ##为什么使用并行编程 2 | 3 | 自从计算系统进化以来,它们已经开始提供一个能使我们以并行的方式运行指定软件的独立的某部分的机制,从而提升响应速度以及一般的性能。此外,我们可以很容易的验证配备有多个处理器以及多核的机器。那么,为什么不利用这个架构呢? 4 | 5 | 在系统开发的所有情况下,从智能手机、平板电脑到研究中心的重型计算,并行编程都是现实的。并行编程的一个坚实的基础将允许开发人员优化一个应用的性能。这会增强用户体验以及计算资源的消费,从而减少完成复杂任务的处理时间。 6 | 7 | 举一个并行性的例子,让我们想象一个场景,在这个场景中,有一些任务,其中一个任务是从数据库中检索一些信息,而这个数据库规模又很大。再假如,这个应用还需要顺序执行,在这个应用中,这些任务必须以一定的逻辑顺序,一个接一个的执行。当用户请求数据时,在返回的数据没有结束之前,其它系统将一直被阻塞。然而,利用并行编程,我们将会创造一个新的worker来在数据库中查询信息,而不会阻塞这个应用的其它功能,从而提高它的使用。 8 | -------------------------------------------------------------------------------- /第一章/发现Python并行编程的工具.md: -------------------------------------------------------------------------------- 1 | ##发现Python并行编程的工具 2 | 3 | 由Guido Van Rossum创造的语言,是一种多泛型的,多用途的语言。由于它非常简单且易于维护,被世界各处广泛接受。它也被称为含有电池的语言。它有广泛的模块使其用起来更流畅。在并行编程中,Python有简化实现的内置和外部模块。本书基于Python3.X的。 4 | 5 | ###Python的threading模块 6 | 7 | Python的threading模块提供了一个抽象层次的模块_thread,这是一个低层次的模块。当开发一个基于线程的并行系统的艰巨任务时,它为程序员提供了一些函数来帮助程序员的开发。线程模块的官方文档可以在找到。 9 | 10 | ###Python的mutliprocess模块 11 | 12 | multiprocessing模块旨在为基于进程的并行的使用提供一个简单的API。这个模块与线程模块类似,它简化了基于进程的并行系统的开发,这一点与线程模块没有什么不同。在Python社区中,基于进程的方法很流行,因为它是在解决出现在Python中CPU-Bound threads和GIL的使用的问题时的一个解决方案。多进程模块的官方文档可以在找到。 14 | 15 | ###Python的parallel模块 16 | 17 | Python的parallel模块是外部模块,它提供了丰富的API,这些API利用进程的方法创建并行和分布式系统。这个模块是轻量级并且易安装的,它与其他的Python程序一起集成的。parallel模块可以在找到。在那么多特性中,我们着重强调以下几点: 18 | 19 | * 最优配置的自动检测 20 | * 运行时可以改变多个工作进程 21 | * 动态的负载均衡 22 | * 容错性 23 | * 自发现计算资源 24 | 25 | ###Celery-分布式任务队列 26 | 27 | Celery是一个用于创建分布式系统的极其优秀的模块,并且拥有很好的文档。它在并发形式上使用了至少三种不同类型的方法来执行任务:multiprocessing, Eventlet,和 Gevent。这项工作将会集中精力在多进程的方法的使用上。而且,只需要通过配置就能实现进程间的互联,这被留下来作为一个研究,以便读者能够建立一个与他/她的实验的一个比较。 28 | 29 | Celery模块可以在官方的项目页面得到。 30 | 31 | 32 | -------------------------------------------------------------------------------- /第一章/在并行编程通信.md: -------------------------------------------------------------------------------- 1 | ##在并行编程通信 2 | 3 | 在并行编程中,workers被送来执行任务,而执行任务常常需要建立通信,以便可以合作解决一个问题。 4 | 在大多数情况下,通信以一种可以在workers之间进行数据交换的方式被建立。当说到并行编程,有两种通信方式广为人知:共享状态和消息传递。在下面的章节中,将对这两种方式进行简要描述。 5 | 6 | ###理解共享状态 7 | 8 | 在workers中最有名的一种通信方式就是共享状态。分享状态似乎是一个简单的使用,但是这会有许多的陷阱,因为若其中某个进程对共享的资源执行了一项无效的操作会影响到所有其它的进程,从而导致一个不好的结果。这也是使在多台计算机之间进行分布式的程序成为不可能的显而易见的原因。 9 | 10 | 为了说明这一点,我们将使用一个真实的案例。假设你是一个具体的银行的一个客户,而这个银行只用一个收银员。当你去银行,你必须要排队等到轮到你的时候。当你在队列中时,你注意到收银员一次只能为一个顾客服务,而收银员不可能同时为两个顾客提供服务而不出错。电脑运算拥有多种手段来以可控的方式访问数据,如mutex(互斥?)。 11 | 12 | Mutex可以理解为一种特殊的过程变量,表示了访问数据的可靠性等级。也就是说,在真实世界的栗子中,顾客有一个编号,在某一特定的时刻,这个编号将会被激活,然后收银员仅对于这个顾客提供服务。在进程结束时,该名顾客将会释放收银员让其为下一个顾客服务,以此类推。 13 | 14 | > 在某些情况下,当程序正在运行时,在一个变量中数据会有一个常数值,数据仅仅以只读的目的被分享。所以访问控制不是必须的,因为永远不会出现完整性问题。 15 | 16 | ###理解信息传递 17 | 18 | 运用消息传递是为了避免来自共享状态带来的数据访问控制以及同步的问题。消息传递包含一种在运行的进程中进行消息交换的机制。每当我们用分布式架构开发程序的时候,就能见到消息传递的使用,在网络中,消息交换被放在一个重要的位置。Erlang等语言,在它的并行体系结构中,就是使用这个模型来实现通信。由于每次数据交换都复制一份数据的拷贝,因此不会出现并发访问的问题。尽管内存使用看起来比共享内存状态要高,但是这个模型还是有一些优势的。优势如下: 19 | 20 | * 缺乏数据的一致性访问 21 | * 数据即可在本地交换(不同的进程)也能在分布式环境中交换 22 | * 不太可能出现扩展性问题,并且允许不同系统相互写作。 23 | * 一般来说,据程序员来说易于维护。 24 | -------------------------------------------------------------------------------- /第一章/小心Python GIL.md: -------------------------------------------------------------------------------- 1 | ##小心Python GIL 2 | 3 | GIL是一种用于实现标准Python(也被称为CPython)的一种机制,是为了避免不同的线程同时执行字节码。在这门语言的使用者中,GIL存在的原因被激烈的讨论着。GIL被用于保护被CPython解释器使用的内存储器,因为CPython解析器并未实现为线程的并发访问的同步机制。在任何情况下,当我们决定使用线程时,GIL将会导致一个问题,这些往往是CPU受限。比如说,I/O Threads超出了GIL的范围。也许GIL机在Python的演变过程中带来的好处要多于坏处。显然,我们不能仅仅将效率作为评判一个事情是好是坏的唯一的标准。 4 | 5 | 很多情况下,使用多进程配合消息传递能更好的平衡可维护性、可扩展性以及性能之间的关系。然而,在某些情况下即使由于GIL的存在会降低效率也还是会需要线程。这时,所能做的就是写一些代码片段作为C语言的扩展,并且把它们嵌入到Python程序中。因此也是有替代品的,这应由开发人员分析真正的需求。那么问题来了,GIL一般来说是一个恶棍吗?重要的是要记住,PyPy团队正致力于将GIL从Python中移除的STM的实现。想了解有关此项目的更多细节,请访问. 6 | 7 | -------------------------------------------------------------------------------- /第一章/并行、并发与分布式编程的对比分析.md: -------------------------------------------------------------------------------- 1 | ##并行、并发以及分布式编程的对比分析 2 | 3 | 并行编程可以被定义为一种模型,这个模型旨在创造一种能与被准备用于同时执行代码指令的环境相兼容的程序。并行技术被用于开发软件还不是很长。几年前,处理器在其它组件中只有一个在一段时间空间中一次只能执行一条指令的算术逻辑单元(ALU)。多年来,只有一个以Hz为衡量单位的时钟,被用来决定在给定的时间间隔里一个处理器能执行的指令数量。时钟的数量越多,那么执行的更多指令数量的潜力更大,比如说KHz(每秒数以千计的操作)、MHz(每秒数以百万计的操作)、以及现在的GHz(每秒数以千万计的操作)。 4 | 5 | 总结,每个周期处理器执行的指令越多,那么它执行的越快。在80年代,一个革命性的处理器步入人们的生活,Intel 80386,允许任务以预先执行的方式来执行,也就是说,它会阶段性的中断当前的程序来为另一个程序提供处理器时间;这意味着一种基于时间分片的伪并行处理的诞生。 6 | 7 | 在80年代末期,出现了Intel80486,它实现了一种管道系统,在实践中,这个系统将执行阶段划分为不同的子阶段。实际上,在一个处理器周期中,我们可能有不同的指令在一个子阶段同时被执行。 8 | 9 | 所有的在前一节所提到的进步带来一些性能上的提升,但是这些仍然不够,我们所面对的微妙的问题,而这,被称为摩尔定律()。 10 | 11 | 探寻满足时钟高能耗的过程最终会被物理规则的限制所阻;处理器将消耗更高的能量,从而产生更多的热。此外,还有一个重大的问题,便携式电脑的市场在90年代正在加速扩张。所以,有一种能够使这些设备的电源能够持续长时间的远离插座的处理器是极其重要的。几个来自不同的制造商的技术和家庭的处理器诞生了。在服务器和大型主机领域,值得一提的是英特尔的Core(R)系列产品,该系列产品允许即使只有一个物理芯片,也能让操作系统模拟出存在不止一个的处理器。 12 | 13 | 在Core(R)系列产品中,处理器有重大的内部变化和特色组件被称为内核,它有自己的ALU和L2、L3缓存,以及其它的用于执行指令的元素。这些内核也被称为逻辑处理器,让我们可以同时并行的执行一个程序的不同的部分,甚至同时执行不同的程序。这个时代的内核是低能耗的使用与功率处理都比前一代处理器更优。作为并行工作的内核,模拟独立的处理器,我们可以有多核芯片和一个比较次的时钟,从而根据任务来获得与单核芯片却有更高的时钟相比的更高的性能。 14 | 15 | 当然,如此多的进化改变了我们软件设计的方法。现如今,我们必须考虑并行的设计系统来使我们更合理的使用资源,而不是浪费它们,从而为用户提供更好地体验,并且不仅仅在个人电脑上节省资源,而且要在处理中心节省资源。尤其是现在并行编程已融入开发人员的日常生活,并且很明显不会消失。 16 | 17 | 本章节包含以下几个主题: 18 | 19 | * 为什么使用并行编程? 20 | * 介绍并行化的常见形式 21 | * 在并行编程中通信 22 | * 识别并行编程的问题 23 | * 发现Python的并行编程工具 24 | * 小心Python的Global Interpreter Lock(GIL) 25 | -------------------------------------------------------------------------------- /第一章/总结.md: -------------------------------------------------------------------------------- 1 | ##总结 2 | 3 | 在这一章节,我们学了一些并行编程的概念,并学习了一些模型的优点以及缺点。在谈到并行性的时候对于一些问题或者潜在问题进行了一些简短的解释。我们也对Python的一些内置和外部的模块进行了简短的介绍,这让开发人员在建立并行系统时会更加轻松。 4 | 5 | 在下一章节,我们会学习一些设计并行算法的技术。 -------------------------------------------------------------------------------- /第一章/探索并行化的几种模式.md: -------------------------------------------------------------------------------- 1 | ##探索并行化的几种模式 2 | 3 | 当我们试图定义并行系统的主要模式时,有困惑很正常。常常会有人提到并发系统和并行系统,这两个术语看起来像是讲的同一件事。然而实际上有着轻微的差异。 4 | 5 | 在并发程序中,我们有这么一个场景,在这个场景中,一个程序分派几个workers,这些workers争着使用CPU来执行任务。在纷争发生的阶段将被CPU调度器所控制,CPU调度器的功能是决定在一个特定的时刻,哪个worker更适合使用资源。在大多数情况下,CPU调度器执行清理过程的任务太快以至于我们会有伪并行的印象。因此,并发编程是来自并行编程的一种抽象。 6 | 7 | > 并发系统允许多个任务争夺同一个CPU。 8 | 9 | 下图显示了一个并发程序方案: 10 | 11 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Concurrent%20programming%20scheme.png?raw=true) 12 | 13 | 并行编程可以被定义为一种方法,在那个方法中,程序数据创造workers在多核环境中同时执行指定的任务,在这些任务中它们不需要并发的接触CPU。 14 | 15 | > 并行系统同时的运行任务。 16 | 17 | 下面的图显示了并行系统的概念: 18 | 19 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Parallel%20programming%20scheme.png?raw=true) 20 | 21 | 分布式编程旨在在物理分离的计算机器(节点)之间通过消息交换数据来分享进程 22 | 23 | 分布式编程变得越来越受欢迎的原因有很多,下面是被探讨的受欢迎的原因: 24 | 25 | * **容错性**:由于系统是去中心化的,我们可以分发执行到同一个网络的不同机器,从而执行指定机器的个人维护而不影响整个系统的功能。 26 | * **横向扩展**:通常我们可以在分布式系统中增加处理的性能。我们可以在不需要终止正在执行的应用的情况下连接新的设备。 27 | * **云计算**:随着硬件成本的降低,我们需要这种业务类型的增长,在这种增长中,我们可以获得巨大的机器集群,这些集群对用户来说以一种合作的方式运行并且以一种透明的方式运行程序。 28 | 29 | > 分布式系统在物理隔离的节点上运行任务。 30 | 31 | 下图显示了一个分布式系统的方案: 32 | 33 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%201/Distributed%20programming%20scheme.png?raw=true) 34 | -------------------------------------------------------------------------------- /第一章/识别并行编程的问题.md: -------------------------------------------------------------------------------- 1 | ##识别并行编程的问题 2 | 3 | 当在并行魔鬼居住的大地上战斗之时,勇敢的键盘战士将会遇到一些经典的问题。当没有经验的程序员使用workers和共享状态相结合时,这些问题时有发生。这些问题将会在下面的小节中进行描述。 4 | 5 | ###死锁 6 | 7 | 死锁是这么一种情形,有两个或两个以上的workers继续为了资源的释放而无限期的等待,而资源由于某些原因被这一组的一个worker所占用。为了更好的理解,我们将使用另一个真实的案例。想象银行的入口有一个旋转门。顾客A走向了允许他进入银行的一侧,而顾客B试图从旋转门入口的一侧离开银行,这样的话门将被卡在那里,两个顾客哪里都去不了。这种情形在现实中会很搞笑,但是在编程中将会是一个悲剧。 8 | 9 | > 死锁是这么一个现象,进程都在等待一个释放它们任务出现的情况,而这个情况永远不会出现。 10 | 11 | ###饥饿 12 | 13 | 这个问题是由于一个或者多个进程不公平的竞争所引起的副作用,这会花费更多的时间来执行任务。想象有一组进程,A进程正在执行繁重的任务,而且这个任务还有数据处理优先级。现在,想象一下,高优先级的进程A持续不断的占用CPU,而低优先级的进程B将永远没有机会。因此可以说进程B在CPU周期中是饥饿的。 14 | 15 | > 饥饿是由于进程排名中差劲的调整策略引起的。 16 | 17 | ###竞态条件 18 | 19 | 当一个进程的结果取决于执行结果的顺序,而这个顺序由于缺乏同步机制而被破坏,这个时候我们将面临竞态条件。在大的系统中,它们造成的问题将会难以过滤。举个栗子,有一对夫妇有一个联名的账户,在他们操作之前初始的余额是$100.下表显示了常规的有保护机制、预期的事实顺序以及结果情况: 20 | 21 | ![](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) 22 | 23 | 在下表中,有问题的场景出现了。假设账户没有同步机制,并且操作的顺序也和预期不一样。 24 | 25 | ![](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) 26 | 27 | 由于在操作的顺序中意外的没有同步,最终的结果将会明显不一致。并行编程的一个特点就是不确定性。当两个workers都正在运行或者它们中的一个第一个运行的时候,结果将会是不可预见的。因此同步机制是必不可少的。 28 | 29 | > 不确定性,如果缺乏同步机制,将会导致竞态条件的问题。 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /第七章/ReadMe.md: -------------------------------------------------------------------------------- 1 | ##Distributing Tasks with Celery -------------------------------------------------------------------------------- /第七章/分发简单任务.md: -------------------------------------------------------------------------------- 1 | 在之前,我们已经建立好环境。下面测试一下环境,发送一个计算平方根的任务。 2 | 定义任务模块tasks.py。在开始,导入必须的模块。 3 | 4 | from math import sqrt 5 | from celery import Celery 6 | 7 | 然后,创建Celery实例,代表客户端应用: 8 | 9 | app = Celery('tasks', broker='redis://192.168.25.21:6379/0') 10 | 11 | 在初始化时我们传入了模块的名称和broker的地址。 12 | 然后,启动result backend,如下: 13 | 14 | app.config.CELERY_RESULT_BACKEND = 'redis://192.168.25.21:6379/0' 15 | 16 | 用@app.tack装饰器定义任务: 17 | 18 | @app.task 19 | def square_root(value): 20 | return sqrt(value) 21 | 22 | 到此,我们完成了tasks.py模块的定义,我们需要初始化服务端的workers。我们创建了一个单独的目录叫做8397_07_broker。拷贝tasks.py模块到这个目录,运行如下命令: 23 | 24 | $celery –A tasks worker –-loglevel=INFO 25 | 26 | 上述命令初始化了Clery Server,—A代表Celery应用。下图是初始化的部分截图 27 | 28 | ![](图片链接地址) 29 | 30 | 现在,Celery Server等待接收任务并且发送给workers。 31 | 下一步就是在客户端创建应用调用tasks。 32 | 33 | > 上述步骤不能忽略,因为下面会用在之前创建的东西。 34 | 35 | 在客户端机器,我们有celery_env虚拟环境,现在创建一个task_dispatcher.py模块很简单,如下步骤; 36 | 1. 导入logging模块来显示程序执行信息,导入Celery模块: 37 | 38 | import logging 39 | from celery import Celery 40 | 41 | 2. 下一步是创建Celery实例,和服务端一样: 42 | 43 | #logger configuration... 44 | app = Celery('tasks', 45 | broker='redis://192.168.25.21:6379/0') 46 | app.conf.CELERY_RESULT_BACKEND = 47 | 'redis://192.168.25.21:6397/0' 48 | 49 | 50 | 由于我们在接下的内容中要复用这个模块来实现任务的调用,下面我们创建一个方法来封装sqrt_task(value)的发送,我们将创建manage_sqrt_task(value)方法: 51 | 52 | def manage_sqrt_task(value): 53 | result = app.send_task('tasks.sqrt_task', args=(value,)) 54 | logging.info(result.get()) 55 | 56 | 从上述代码我们发现客户端应用不需要知道服务端的实现。通过Celery类中的send_task方法,我们传入module.task格式的字符串和以元组的方式传入参数就可以调用一个任务。最后,我们看一看log中的结果。 57 | 在__main__中,我们调用了manage_sqrt_task(value)方法: 58 | 59 | if __name__ == '__main__': 60 | manage_sqrt_task(4) 61 | 62 | 下面的截图是执行task_dispatcher.py文件的结果: 63 | 64 | ![](图片链接地址) 65 | 66 | 在客户端,通过get()方法得到结果,这是通过send_task()返回的AsyncResult实例中的重要特征。结果如下图: 67 | 68 | ![](图片链接地址) 69 | 70 | 71 | -------------------------------------------------------------------------------- /第七章/建立环境.md: -------------------------------------------------------------------------------- 1 | ### 建立环境 2 | 3 | 在本章,我们将使用两台linux机器。第一台主机名foshan,作为客户端,执行app Celery分发的任务。另一台机器主机名Phoenix,作为broker、result backend和任务队列。 4 | 5 | ### 配置客户端机器 6 | 7 | 首先配置客户端机器。在这台机器上,用pyvenv建立Python3.3的虚拟环境。用pyvenv的目的是隔离每个项目的开发环境。执行以下命令能够创建虚拟环境: 8 | 9 | $pyvenv celery_env 10 | 11 | 上述命令在当前路径创建一个名为celery_env的文件夹,里面包含所有Python开发环境必须的结构。下图是该目录所包含的内容: 12 | 13 | ![](图片链接地址) 14 | 15 | 在创建了虚拟环境之后,我们就可以开始工作并安装需要使用的包。然而,首先我们得激活这个环境,执行以下命名: 16 | 17 | $source bin/activate 18 | 19 | 当命令行提示符改变了,例如在左边出现celery_env,就说明激活完成。所有你安装的包都只在这个目录下有效,而不是在整个系统中有效。 20 | 21 | > 用--system-site-packages标识可以创建能够访问系统site-packages的虚拟环境,但是不推荐使用。 22 | 23 | 现在,我们有一个虚拟环境,假设已经安装好了setuptools或者pip。下面为客户端安装必须的包,如下命令: 24 | 25 | $pip install celery 26 | 27 | 下图是已经安装好的framework v3.1.9,将在本书中使用该版本。 28 | 29 | ![](图片链接地址) 30 | 31 | 现在我们要在Celery中安装支持的Redis,这样客户端就可以通过broker传输消息了。用如下命令: 32 | 33 | $pip install celery[redis] 34 | 35 | 现在我们的客户端环境配置好了,在开始编码之前,我们必须配置好服务器端的环境。 36 | 37 | 38 | ### 配置服务器 39 | 40 | 为了配置服务器,我们首先安装Redis,Redis将作为broker和result backend。使用如下命令: 41 | 42 | $sudo apt-get install redis-server 43 | 44 | 启动Redis: 45 | 46 | $redis-server 47 | 48 | 如果成功,会出现类似下图中的输出 49 | 50 | ![](图片链接地址) 51 | 52 | -------------------------------------------------------------------------------- /第七章/总结.md: -------------------------------------------------------------------------------- 1 | 在本章,我们论述了Celery的分布式任务队列,并且对Celery的架构、组件、基本环境搭建、运行简单应用有了了解。如果单独讲Celery可以写一本书,通过本章需要你对Celery有基本的认识。 2 | 在下一章,我们将学习asyncio模块和怎样以异步的方式执行程序。下一章也会涉及到协程(coroutines)和asyncio中协程的应用。 -------------------------------------------------------------------------------- /第七章/根据任务类型定义队列.md: -------------------------------------------------------------------------------- 1 | 计算斐波那契数列的任务已经实现并且运行。所有任务都被发送到默认的Celery队列。然而,有方法路由任务到不同的队列。让我们重构服务器端的架构来实现路由任务。我们将根据任务类型定义队列。 2 | 在服务器端启动Celery server时,我们将建立三种不同的队列,它们将被workers消费。斐波那契数列任务有fibo_queue,平方根任务有sqrt_queue,网络爬虫任务有 webcrawler_queue。然而,划分队列有什么好处呢。以下列举一些: 3 | 4 | - 相同类型的任务划分为一组,使得监控更加简单 5 | - 定义worker从一个队列取任务,性能更佳 6 | - 可在性能更好的机器上建立任务更重的队列 7 | 8 | 为了再server上建立队列,我们只需用如下命令初始化Celery: 9 | 10 | $celery –A tasks –Q sqrt_queue,fibo_queue,webcrawler_queue worker --loglevel=info 11 | 12 | 下图是在服务端截图: 13 | 14 | ![](图片链接地址) 15 | 16 | 在进行下一个例子之前,我们路由现有的任务到队列中。在服务端,在task_dispatcher.py模块中,我们将修改send_task调用来将任务发送到不同的队列。修改如下: 17 | 18 | app.send_task('tasks.sqrt_task', args=(value,), 19 | queue='sqrt_queue', routing_key='sqrt_queue') 20 | 21 | 然后,修改fibo_task调用,如下: 22 | 23 | app.send_task('tasks.fibo_task', args=(x,), queue='fibo_queue', 24 | routing_key='fibo_queue') 25 | 26 | > 如果有兴趣监控队列、统计任务数量或者其他,请参考Celery文档 http://celery.readthedocs.org/en/latest/userguide/monitoring.html。 27 | > 在任何情况用Redis,redis-cli都可以作为一个工具。队列、任务、workders都可以被监控,详见 http://celery.readthedocs.org/en/latest/userguide/monitoring.html#workers. 28 | 29 | -------------------------------------------------------------------------------- /第七章/理解Celery.md: -------------------------------------------------------------------------------- 1 | Celery是一个框架,该框架提供机制来简化构建分布式系统的过程。Celery框架通过在同一个网络内的机器之间交换信息来分发任务。任务(task)在Celery中是一个关键的概念,任何形式的job在分发之前都必须封装成任务。 2 | 3 | ## 为什么使用Celery 4 | 5 | Celery有如下优点: 6 | - 分发任务是透明的 7 | - 在并发worker启动时,对其改变很小 8 | - 支持同步、异步、周期和计划任务 9 | - 如果出现了错误,会重新执行任务 10 | 11 | > 很多开发者都认为同步任务和实时任务是一样的,实际上它们是完全不同的。对于实时任务,它有一个时间窗口,任务执行必须在Deadline之前完成。如果经过分析,任务在时间窗口内完成不了,那么它将被终止或者暂停直到下次能够完成,而同步任务是当任务执行完后才返回结果。 12 | -------------------------------------------------------------------------------- /第七章/理解Celery架构.md: -------------------------------------------------------------------------------- 1 | Celery架构基于可插拔组件和用*message transport(broker)*实现的消息交换机制。具体的如下图所示: 2 | ![](图片链接地址) 3 | 4 | 现在,让我们详细的介绍Celery的每个组件。 5 | 6 | ### 处理任务 7 | 8 | 在上图中的*client*组件,有创建和分派任务到brokers的方法。 9 | 分析如下示例代码来演示通过使用@app.task装饰器来定义一个任务,它可以被一个Celery应用的实例访问,下面代码展示了一个简单的`Hello World app`: 10 | 11 | @app.task 12 | def hello_world(): 13 | return "Hello I'm a celery task" 14 | 15 | 16 | > 任何可以被调用的方法或对象都可以成为任务 17 | 18 | 正如我们之前所说,有各种类型的任务:同步、异步、周期和计划任务。当我们调用任务,它返回类型AsyncResult的实例。AsyncResult对象中可以查看任务状态,当任务结束后可以查看返回结果。然而,为了利用这个机制,另一个叫做*result backend*的组件必须启动,这将在本章的后面讲解。为了分派任务,我们可以用下面这些方法: 19 | - delay(arg, kwarg=value) : 它会调用apply_aync方法。 20 | - apply_async((arg,), {'kwarg': value}) : 该方法可以为任务设置很多参数,一些参数如下。 21 | - countdown : 默认任务是立即执行,该参数设置经过countdown秒之后执行。 22 | - expires : 代表经过多长时间终止。 23 | - retry : 如果连接或者发送任务失败,该参数可是重试。 24 | - queue : 任务队列。 25 | - serializer : 数据格式,其他还有json、yaml等等 26 | - link : 连接一个或多个即将执行的任务。 27 | - link_error : 连接一个或多个执行失败的任务。 28 | - apply((arg,), {'kwarg': value}) : 在本地进程以同步的方式执行任务,因而阻塞直到结果就绪。 29 | 30 | > Celery 提供了查看任务状态的机制,这在跟踪进程的真实状态非常有用。更多的关于内建任务状态的资料请查看http://celery.readthedocs.org/en/latest/reference/celery.states.html 31 | 32 | ### 发现消息传输(broker) 33 | 34 | broker是Celery中的核心组件,通过broker可以发送和接受消息,来完成和workers的通信。Celery支持大量的brokers。然而,对于某些broker,不是所有的Celery机制都实现了。实现功能最全的是**RabbitMQ**和**Redis**。在本书中,我们将采用Redis作为broker。broker提供在不同客户端应用之间通信的方法,客户端应用发送任务,workers执行任务。可以有多台带有broker的机器等待接收消息,然后发送消息给workers。 35 | 36 | 37 | ### 理解workers 38 | 39 | Workers负责执行接收到的任务。Celery提供了一系列的机制,我们可以选择最合适的方式来控制workers的行为。这些机制如下: 40 | - 并发模式:例如进程、线程、协程(Eventlet)和Gevent 41 | - 远程控制:利用该机制,可以通过高优先级队列发送消息到某个特定的worker来改变行为,包括runtime。 42 | - 撤销任务:利用该机制,可以指挥一个或多个workers来忽略一个或多个任务。 43 | 44 | 更多的特性可以在运行时设定或者改变。比如,worker在某一段时间执行的任务数,worker在哪个队列消费等等。软玉worker更多的信息可以参考 http://docs.celeryproject.org/en/latest/userguide/workers.html#remote-control 45 | 46 | ### 理解result backends 47 | 48 | Result backend组件存储任务的状态和任务返回给客户端应用的结果。Celery支持的result backend之中,比较出彩的有 RabbitMQ, Redis, MongoDB, Memcached。每个result backend都有各自的优缺点,详见 http://docs.celeryproject.org/en/latest/userguide/tasks.html#task-result-backends 49 | 50 | 现在,我们对Celery架构有了一个大概的认识。下面我们建立一个开发环境来实现一些例子。 -------------------------------------------------------------------------------- /第七章/用Celery来分发任务.md: -------------------------------------------------------------------------------- 1 | 在之前的章节,我们学习了Python并行编程。我们用Python并行模块实现了一些实例,包括斐波那契数列和网络爬虫。我们知道了怎样用管道进行进程间通信和如何在同一个网络的不同机器上分发进程。在本章节,我们将学习如何利用Celery框架在同一个网络的不同机器之间分发任务。 2 | 在本章,我们将覆盖一下几个主题: 3 | - 理解Celery 4 | - 理解Celery架构 5 | - 建立环境 6 | - 分发简单任务 7 | - 用Celery来获得斐波那契数列的项 8 | - 用Celery来构建一个分布式网络爬虫系统 -------------------------------------------------------------------------------- /第七章/用Celery来构建一个分布式网络爬虫系统.md: -------------------------------------------------------------------------------- 1 | 现在我们将用Celery构建网络爬虫。我们已经有了webcrawler_queue,负责hcrawler任务。然而,在服务器端,我们将在tasks.py模块创建crawl_task任务。 2 | 首先,导入re(正则表达式)和requests(HTTP lib)模块,代码如下: 3 | 4 | import re 5 | import requests 6 | 7 | 然后,定义正则表达式,和之前的章节一样; 8 | 9 | hTML_link_regex = re.compile( 10 | '') 11 | 12 | 然后,替换crawl_task方法,添加@app.task装饰器,修改返回信息,如下: 13 | 14 | @app.task 15 | def crawl_task(url): 16 | request_data = requests.get(url) 17 | links = html_link_regex.findall(request_data.text) 18 | message = "The task %s found the following links %s.."\ 19 | Return message 20 | 21 | links列表不一定要和下图匹配: 22 | 23 | ![](图片链接地址) 24 | 25 | 在客户端task_dipatcher.py模块实现crawl_task调用。 26 | 首先,我们需要列出数据的输入url_list。代码如下: 27 | 28 | 29 | url_list = ['http://www.google.com', 30 | 'http://br.bing.com', 31 | 'http://duckduckgo.com', 32 | 'http://github.com', 33 | 'http://br.search.yahoo.com'] 34 | 35 | 创建manage_crawl_task方法。 36 | 37 | def manage_crawl_task(url_list): 38 | async_result_dict = {url: app.send_task('tasks.crawl_task', 39 | args=(url,), queue='webcrawler_queue', 40 | routing_key='webcrawler_queue') for url in url_list} 41 | for key, value in async_result_dict.items(): 42 | if value.ready(): 43 | logger.info("%s -> %s" % (key, value.get())) 44 | else: 45 | logger.info("The task [%s] is not ready" % 46 | value.task_id) 47 | 48 | 和之前创建的manage_fibo_task方法一样,async_result_dict字典包含当前URL和AsyncResult结果。然后我们检查任务的状态获取任务结果。 49 | 50 | 现在我们在__main__中调用该方法: 51 | 52 | if __main__ == '__main__': 53 | #manage_sqrt_task(4) 54 | #manage_fibo_task(input_list) 55 | manage_crawl_task(url_list) 56 | 57 | 运行task_dispatcher.py代码,在服务器端有如下输出: 58 | 59 | ![](图片链接地址) 60 | 61 | 最后,客户端的输出如下: 62 | 63 | ![](图片链接地址) 64 | 65 | Celery是一个强大的工具,在本章我们只是用到了基本的东西。更多的内容建议自己在真实的项目中动手去尝试。 66 | -------------------------------------------------------------------------------- /第七章/用Celery来获得斐波那契数列的项.md: -------------------------------------------------------------------------------- 1 | 让我们再一次计算多个输入的斐波那契数列的项,每个都用分布式的方法。现在的方法和之前的方法改变很小。 2 | 我们将终止Celery的执行(Ctrl+C)并且在tasks.py模块(之前创建过)加入fibo_task任务。 3 | 4 | @app.task 5 | def fibo_task(value): 6 | a, b = 0,1 7 | for item in range(value): 8 | a, b = b, a + b 9 | message = "The Fibonacci calculated with task id %s" \ 10 | " was %d" % (fibo_task.request.id, a) 11 | Return (value, message) 12 | 13 | 通过\得到任务的ID,请求对象是task的对象,task对象提供了task执行的上下文。通过上下文可以得到task的ID等信息。 14 | 15 | 在tasks.py模块加入了新的任务之后,再一次初始化Celery,结果如下图: 16 | 17 | ![](图片链接地址) 18 | 19 | 现在我们把fibo_task任务装载到Celery server,我们将在客户端实现对该任务的调用。 20 | 21 | 在task_dispatcher.py模块,我们会申明input_list,如下: 22 | 23 | input_list = [4, 3, 8, 6, 10] 24 | 25 | 和前面的做法一样,定义manage_fibo_task方法: 26 | 27 | def manage_fibo_task(value_list): 28 | async_result_dict = {x: app.send_task('tasks.fibo_task', 29 | args=(x,)) for x in value_list} 30 | for key, value in async_result_dict.items(): 31 | logger.info("Value [%d] -> %s" % (key, value.get()[1])) 32 | 33 | 在manage_fibo_task方法中,创建了一个叫做async_result_dict的字典,key是传入的要计算的值,value是send_task方法返回的AyncResult对象。通过这个方法,我们可以查看任务的结果和状态。 34 | 最后,遍历字典得到输入值和输出结果。AsyncResult的get方法能够得到结果。 35 | get()方法会阻塞进程。一个好的方法是调用ready()方法来检查结果是否返回了。 36 | 上述循环可以修改为如下; 37 | 38 | for key, value in async_result_dict.items(): 39 | if value.ready(): 40 | logger.info("Value [%d] -> %s" % (key, value.get()[1])) 41 | else: 42 | logger.info("Task [%s] is not ready" % value.task_id) 43 | 44 | 不同的任务会有不同的延迟时间,为了防止无限等待,可以用get(timeout=x)方法设置超时。 45 | 最后,添加manage_fibo_task的调用,传入input_list。代码如下: 46 | 47 | if __name__ == '__main__': 48 | #manage_sqrt_task(4) 49 | manage_fibo_task(input_list) 50 | 51 | 当我们执行task_dispatcher.py后,输入如下: 52 | 53 | ![](图片链接地址) 54 | 55 | 在客户端有如下输出: 56 | 57 | ![](图片链接地址) 58 | 59 | -------------------------------------------------------------------------------- /第三章/ReadMe.md: -------------------------------------------------------------------------------- 1 | ##Identifying a Parallelizable Problem 2 | 3 | * 识别一个并行化的问题 4 | * 从多个输入中得到斐波那契最大的值 5 | * 爬取网页 6 | * 小结 -------------------------------------------------------------------------------- /第三章/从多个输入中得到斐波那契最大的值.md: -------------------------------------------------------------------------------- 1 | ##从多个输入中得到斐波那契最大的值 2 | 3 | 众所周知,斐波那契数列被定义如下: 4 | 5 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%203/fibonacci.png?raw=true) 6 | 7 | 实际上,按照从0到10计算斐波那契的值,结果将会是0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 和55. 8 | 9 | 用迭代法计算斐波那契最高的值的Python代码如下: 10 | ```python 11 | def fibonacci(input): 12 | a, b = 0, 1 13 | for item in range(input): 14 | a, b = b, a + b 15 | return a 16 | ``` 17 | 斐波那契函数为一个特定的输入的数据计算一个最大的斐波那契值。让我们想象一个场景,在这个场景中,需要计算斐波那契的值,而本网站将会从一个用户那里接收到几个输入。假设用户提供一个数组的值作为输入,因此使这些计算按顺序将会很有趣。但是假使是100万个用户同时发出请求那么将会怎么样?在这种情况下,一些用户不得不等待很长一段时间才能得到他们的答案。 18 | 19 | 让我们只考虑先前文中的Python实现的斐波那契函数代码。我们怎么在有一个数组数据输入的情况下使用并行性来实现它?在前一章已经展示过多种实现的方式,这里我们使用其中一种方式-数据分解。我们可以将数组分为几个单元,每个单元关联一个任务然后被一个worker执行。下图描述了一个建议的解决方案: 20 | 21 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%203/Parallel%20Fibonacci%20for%20multiples%20inputs.png?raw=true) 22 | 23 | 24 | > 对读者的建议,实现使用缓存机制存储计算的值来避免浪费CPU时间来完成练习。我们建议使用像memcached的缓存。 25 | 26 | -------------------------------------------------------------------------------- /第三章/总结.md: -------------------------------------------------------------------------------- 1 | ##总结 2 | 3 | 在这一章节,我们学习了并行中出现的常见问题以及可能的解决方案。下面将会题展示任何使用不同的Python库来解决上面展示的问题。 4 | 5 | 在下一章节,当使用threading模块,我们专注于的线程的解决方案,在用到mutliprocess模块时,解决方案也会涉及到使用多进程,以此类推。 6 | -------------------------------------------------------------------------------- /第三章/爬取网页.md: -------------------------------------------------------------------------------- 1 | ##爬取网页 2 | 3 | 贯穿本书的另一个正在研究的问题是是实现一个并行的网络爬虫。一个网络爬虫由一个个浏览网页并从页面获取信息的电脑程序组成。被分析的场景是一个问题,在这个问题中,一个顺序执行的网络爬虫会被提供一组可变数量的统一资源定位器(URLs)为参数,它需要检索每个URL所提供的所有链接。假设输入的URLs的数量相当的大,我们可以在以下方法中寻找并行性的解决方案: 4 | 5 | 1. 将所有的URLs分成组组合到一个数据结构中。 6 | 2. 把这些URLs分配給多个任务,这样写任务会爬取每个URL中的包含信息。 7 | 3. 将这些任务分派给多个并行的workers来执行。 8 | 4. 由于前一阶段的结果必须传给下一个阶段,这将会改进未加工的存储的数据,因此保存它们关联它们到原始的URLs。 9 | 10 | 正如我们在上面编号的步骤中观察到的为了一个提出的解决方案,可以与以下两个方法结合: 11 | 12 | * 数据分解:这发生在我们划分和关联URLs到任务上。 13 | * 用管道进行任务分解:这包含三个阶段的管道,这发生在我们链接接收、存储以及组织爬取的结果的任务。 14 | 15 | 下图显示了解决方案: 16 | 17 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%203/Parallel%20Web%20crawler.png?raw=true) 18 | -------------------------------------------------------------------------------- /第三章/识别一个可并行的问题.md: -------------------------------------------------------------------------------- 1 | ##识别一个可并行的问题 2 | 3 | 前一章我们从不同角度探讨了并行方面的一些问题。现在我们将分析一些具体的问题,这些将在具体实现时自始至终会对我们有指导的作用。 4 | 5 | 这个章节包含以下的议题: 6 | 7 | * 获得有多个输入的斐波那契最高的值 8 | * 网页爬虫 9 | -------------------------------------------------------------------------------- /第二章/ReadMe.md: -------------------------------------------------------------------------------- 1 | ##Designing Parallel Algorithms 2 | 3 | * 设计并行算法 4 | * 分治技术 5 | * 使用数据分解 6 | * 用管道分解任务 7 | * 处理和映射 8 | * 识别独立的任务 9 | * 识别需要数据交换的任务 10 | * 负载均衡 11 | * 小结 -------------------------------------------------------------------------------- /第二章/使用数据分解.md: -------------------------------------------------------------------------------- 1 | ##使用数据分解 2 | 3 | 并行化问题的方法之一是通过数据分解。想象一下有这么一个场景,在这个场景中我们要以标量4乘以一个2x2矩阵(这个矩阵被称为矩阵A).在一个顺序执行系统中,我们将一个接一个的执行每个乘法的操作,最后生成所有指令的最终结果。根据矩阵A的大小,这个问题的顺序解决方案可能是旷日持久的。然而,当数据分解被应用的时候,我们可以想象矩阵A被分解为一个一个小的部分,这些分片数据被相关的workers以并行的方式接受并处理。下图以一个2x2矩阵乘以一个标量值的栗子说明了数据分解应用的概念: 4 | 5 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%202/Data%20decomposition%20in%20a%20matrix%20example.png?raw=true) 6 | 7 | 上图中出现的矩阵相乘的问题有一定的对称性,每个必要的操作的结果是由一个单独的worker执行的,而且每个worker执行同样数量的操作来解决问题。然而,在现实世界中,worker的数量和已分解的数据数量的关系是不对称的,这将直接影响解决方案的性能。最后,每个worker所产生的结果必须整合起来以便使程序最终输出意义结果。为了进行这种整合,workers之间需要进行信息交换或是共享状态。 8 | 9 | > 数据分解的粒度选择将会影响一个解决方案的性能。 10 | 11 | -------------------------------------------------------------------------------- /第二章/分治技术.md: -------------------------------------------------------------------------------- 1 | ##分治技术 2 | 3 | 当你面对一个复杂的问题时,你想做的第一件事就是分解问题以便可以确定哪些部分可以独立的被处理。一般说来,在一个解决方案中,可并行的部分可以被不同的workers分开或者是分布式的执行。分治技术涉及到将一个完整的问题递归的分为不可再分的可被解决的小问题。排序算法,例如归并排序和快排都可以使用这种方法解决。 4 | 5 | 下图显示了在6个元素的向量中应用归并排序,可以看到使用了分治技术: 6 | 7 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%202/Merge%20sort%20(divide%20and%20conquer).png?raw=true) 8 | 9 | -------------------------------------------------------------------------------- /第二章/处理和映射.md: -------------------------------------------------------------------------------- 1 | ##处理和映射 2 | 3 | workers的数量并不足以在一个单一的步骤里解决一个特定的问题。因此前面的章节给出的分解技术是很有必要的。然而分解技术不应该被随意的应用,有一些因素会影响解决方案的性能。在数据或者任务分解之后,我们应该问这么一个问题:“我们应该在workers中如何划分进程的负载来获得比较好的性能?”这不是一个很好回答的问题,因为这取决于正在研究的问题。 4 | 5 | 基本上,我们在定义过程映射时可以提到两个重要的步骤: 6 | 7 | * 识别独立的任务 8 | * 识别需要数据交换的任务 9 | 10 | ###识别独立的任务 11 | 12 | 在系统中识别独立的任务将允许我们在不同的workers之间分配任务,因为这些任务不需要持续的通信。因为不需要一个数据单元,所以任务可以在不同的workers间执行而不会影响其它任务的执行。 13 | 14 | ###识别需要数据交换的任务 15 | 16 | 将需要相互通讯的任务组合起来放到单个worker中可以提高性能。当有大的通信负载的时候的时候这个真的可以提高性能,因为它能减少任务间信息交换的开销。 17 | 18 | ###平衡负载 19 | 20 | 在并行解决方案中一个典型的问题是如何为不同的工作单元分配计算资源。我们越是将任务分配给不同的workers处理,我们将是需要越多的通信。另一方面,我们越是将任务组合起来分配給一个worker,与通信相关的开销越小。然而,我们可能会增加空转,也就是说,浪费了计算的能力。在并行编程中,浪费并不好。此外,越是将数据聚合在一个worker中,越会减少通过简单的添加更多的设备而增加计算能力的可扩展的灵活性。在一个基于通信的架构(轻微的数据聚合)中,为集群或者网格简单的增加机器从而提升处理性能甚至不需要中断正在运行的系统。 21 | -------------------------------------------------------------------------------- /第二章/总结.md: -------------------------------------------------------------------------------- 1 | ##总结 2 | 3 | 在这一章节,我们讨论了一些创建并行解决方案的方法。你的注意力应重点放在在不同的workers之间划分处理负载,考虑聚合而不是数据。 4 | 5 | 在下一章节,我们将学习如何识别并行的问题。 6 | -------------------------------------------------------------------------------- /第二章/用管道分解任务.md: -------------------------------------------------------------------------------- 1 | ##用管道分解任务 2 | 3 | 管道技术被用来组织多个任务,这些诶任务合作的方式来共同解决一个问题。管道将大型的任务分割成独立并行的小任务。管道模型可以类比成汽车工厂的装配线,只不过输入代替底盘称为加工的原始材料。原料经过不同的生产阶段,几个worker一个接一个执行不同的操作直到产生最终结束。这个模型和顺序范式的开发类似,任务一个接一个的作用于数据上,正常情况下,一个任务以上一个任务的结果为输入。那么这个模型和顺序技术的区别是什么呢?管道技术中的每个阶段都拥有自己的workers,并且这些workers是以并行的方式执行的。 4 | 5 | 计算上下文的一个栗子可能是一个批量处理图片,并将抽取出的数据存入数据库的系统。我们将有以下实际的顺序: 6 | 7 | * 接受输入的图像并且以对这些图片以并行的方式进行排列,这些图片将在第二阶段进行处理 8 | * 解析图像,并且有用的信息将会被送到第三阶段 9 | * 在第三阶段,过滤器被并行的应用在图像上 10 | * 来自第三阶段的数据结果被保存在数据库中 11 | 12 | > 每个阶段的管道技术都用自己workers独立的执行。然而,它建立了数据通信机制,以便进行信息的交换。 13 | 14 | 下图展示了管道的概念: 15 | 16 | ![](https://github.com/Voidly/Img/blob/master/Parallel%20Programming%20with%20Python/Chapter%202/The%20pipeline%20technique.png?raw=true) 17 | -------------------------------------------------------------------------------- /第二章/设计并行算法.md: -------------------------------------------------------------------------------- 1 | ##设计并行算法 2 | 3 | 在开发并行系统时,在你编码之前,有几个方面你必须要留意。为了在任务中获得成功,从一开始将并行的问题列出来是至关重要的。在这一章节,我们将接触一些技术方面的解决方案。 4 | 5 | 这个章节将包含一些几个议题: 6 | 7 | - 分治技术 8 | - 数据分解 9 | - 用管道分解任务 10 | - 处理和映射 -------------------------------------------------------------------------------- /第五章/ReadMe.md: -------------------------------------------------------------------------------- 1 | ##Using Multiprocessing and ProcessPoolExecutor 2 | 上章中,我们学习了如何使用threading模块解决两个问题。通过本章的学习,我们将学习如何使用multiprocessing模块解决上章的两个问题,我们将使用和上章类似的接口实现。然而,我们会使用多进程机制。 3 | 本章将覆盖如下一个知识点: 4 | * 理解进程的概念 5 | * 理解多进程通信 6 | * 使用多进程解决斐波那契数列多输入问题 7 | * 使用ProcessPoolExecutor模块设计网络爬虫 8 | 9 | -------------------------------------------------------------------------------- /第五章/使用多进程解决斐波那契序列多输入问题.md: -------------------------------------------------------------------------------- 1 | 下面我们将使用多进程解决多输入情况下的斐波那契数列问题,而不是之前我们使用的多线程的方法。 2 | multiprocessing_fibonacci.py程序使用multiprocessing模块,为了顺利执行,还导入了如下模块: 3 | ```python 4 | import sys, time, random, re, requests 5 | import concurrent.futures 6 | from multiprocessing import, cpu_count, current_process, Manager 7 | ``` 8 | 9 | 上面一些导入模块在之前的章节中提及过,然而,下面的这些模块我们需要特别注意: 10 | * cpu_count: 该方法允许获得机器cpu的数量 11 | * current_process: 该方法可以获得当前进程的信息,比如进程名称 12 | * Manager: 该类通过对象的方式允许功在多进程之间共享python对象 13 | 14 | 下面的代码中我们可以注意到第一个方法有点不同,它将产生15个1到20之间的整数,这些整数将被当作fibo_dict的key使用。 15 | 接下来让我们一起来看producer_task方法,如下: 16 | ```python 17 | def producer_task(q, fibo_dict): 18 | for i in range(15): 19 | value = random.randint(1, 20) 20 | fibo_dict[value] = None 21 | 22 | print("Producer [%s] putting value [%d] into queue.." % (current_process().name, value)) 23 | q.put(value) 24 | 25 | ``` 26 | 27 | 下面将定义一个函数来计算fibo_dict中key对应的斐波那契数列值,和之前章节介绍计算斐波那契序列值不同的是,这里把fibo_dict当作参数传入不同的processes。 28 | 29 | 下面是consumer_task方法,如下: 30 | ```python 31 | def consumer_task(q, fibo_dict): 32 | while not q.empty(): 33 | value = q.get(True, 0.05) 34 | a, b = 0, 1 35 | for item in range(value): 36 | a, b = b, a+b 37 | fibo_dict[value] = a 38 | print("consumer [%s] getting value [%d] from queue..." % (current_process().name, value)) 39 | ``` 40 | 41 | 更进一步,我们来看main函数中的代码,main函数中下面几个变量被定义: 42 | * data_queue: 该参数由multiprocessing.Queueu来创建,是进程安全的 43 | * number_of_cpus: 该参数由multiprocessing.cpu_count方法获得,获得机器cpu的个数 44 | * fibo_dict: 这个字典类型变量从Manager实例获得,保存多进程计算结果 45 | 46 | 然后,我们将创建producer进程,并传入data_queue队列,data_queue队列值由producer_task方法获得: 47 | ```python 48 | producer = Process(target=producer_task, args=(data_queue, fibo_dict)) 49 | producer.start() 50 | producer.join() 51 | ``` 52 | 53 | 我们可以注意到Process实例的初始化过程和我们之前的Thread实例初始化过程类似。初始化函数接收target参数作为进程中要执行的函数,和args参数作为target传入的函数的参数。接下来我们通过start方式开始进程,然后使用join方法,等待producer进程执行完毕。 54 | 55 | 下面一块代码中,我们将定义consumer_list队列,存入初始化过的consumer进程。使用list存储consumer对象的原因是在所有进程结束开始后调用join方法。循环中的每一个worker被调用后,下一个worker将等待上一个worker执行完毕后才开始执行,下面代码将描述这一过程: 56 | ```python 57 | consumer_list = [] 58 | cpu = cpu_count() 59 | print(cpu) 60 | for i in range(cpu): 61 | consumer = Process(target=consumer_task, args=(data_queue, fibo_dict)) 62 | consumer.start() 63 | consumer_list.append(consumer) 64 | [consumer.join() for consumer in consumer_list] 65 | ``` 66 | 最终我们将迭代输出fibo_dict中的结果,如下面截图所示: 67 | -------------------------------------------------------------------------------- /第五章/使用进程池实现网络爬虫.md: -------------------------------------------------------------------------------- 1 | ##使用ProcessPoolExecutor模块设计网络爬虫 2 | 正如concurrent.futures模块提供的ThreadPoolExecutor类方便了操控多线程程序,多进程同样有一个类叫ProcessPoolExecutor。ProcessPoolExecutor类同样由concurrent.futures包提供,我们将使用该类执行我们的网络爬虫程序。为了完成这个任务,我们创建了一个叫process\_pool\_executor\_web\_crawler.py的python模块。 3 | 4 | 代码要导入的包如之前章节中我们介绍的,如requests, Manager模块等等。对于任务的定义,我们延用上章线程程序中的代码,只是改动其中小部分代码,还有一点不一样,进程程序中,我们向任务函数传入参数,而不是使用全局变量。代码如下所示: 5 | 6 | group\_urls\_task函数定义如下: 7 | ```python 8 | def group_urls_task(urls, result_dict, html_link_regex) 9 | ``` 10 | 11 | crawl\_task函数定义如下: 12 | ```python 13 | def crawl_task(url, html_link_regex) 14 | 15 | ``` 16 | 17 | 现在我们再来看下面一小部分代码,与前一章比有了细微的变化。在main函数中,我们获得Manager类的实例,该实例使得我们获得可以被多进程共享的queue和dict。我们使用Manager.Queue()方法获得queue实例来存储我们将要爬得的url。使用Manager.dict()方法获取dict,来存储爬虫的结果。下面的代码将展示上面的定义: 18 | ```python 19 | if __name__ == '__main__': 20 | manager = Manager() 21 | urls = manager.Queue() 22 | urls.put("http://br.bing.com/") 23 | urls.put("https://github.com") 24 | result_dict = manager.dict() 25 | ``` 26 | 27 | 接着,我们将定义爬虫程序中将要用到的正则表达式和介绍如何获取机器的cpu个数,程序如下: 28 | ```python 29 | html_link_regex = \ 30 | re.compile('') 31 | number_of_cpus = cpu_count() 32 | ``` 33 | 34 | 最后一块代码中,我们会注意到concurrent.futures模块中各API签名具有很强的一致性。下面的代码正是我们上章使用ThreadPoolExecutor模块时使用到的。我们只需把ThreadPoolExecutor变为ProcessPoolExecutor,就能改变程序内部行为并解决计算密集型进程的GIL问题。注意下面的程序,创建ProcessPoolExecutor时会根据机器cpu数限定进程的数目。第一个exucutor是为了收集将被爬的URL,把这些URLs保存在一个字典中,key为url而value为None。第二个executor执行爬虫程序。 35 | 首先是第一个executor: 36 | ```python 37 | with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as group_link_processes: 38 | for i in range(urls.qsize()): 39 | group_link_processes.submit(group_urls_task, urls, result_dict, html_link_regex) 40 | ``` 41 | 42 | 第二个executor程序如下: 43 | ```python 44 | with concurrent.futures.ProcessPoolExecutor(max_workers=number_of_cpus) as crawler_link_processes: 45 | future_tasks = {crawler_link_processes.submit(crawl_task, url, html_link_regex): url for url in result_dict.keys()} 46 | for future in concurrent.futures.as_completed(future_tasks): 47 | result_dict[future.result()[0]] = future.result()[1] 48 | ``` 49 | 50 | 程序运行结果如下图: 51 | -------------------------------------------------------------------------------- /第五章/实现多进程间通信.md: -------------------------------------------------------------------------------- 1 | python中的multiprocessing模块支持两种方式在进程间通信,都是基于消息传递机制。之前我们介绍过,由于缺乏同步机制因此不得不采取消息传递机制,在进程间传递复制的数据。 2 | ##使用multiprocessing.Pipe模块 3 | 4 | pipe管道在两个端点间搭建一种通信机制,通过在进程间建立通道使得进程间可以相互通信。 5 | 为了更好的说明multiprocessing.Pipe对象的使用方法,我们将介绍一个python程序,包含A,B两个进程。进程A发送1到10之间的一个数给进程B,进程B将打印该数,接下来让我们一步步介绍这个程序。 6 | 我们首先导入一些我们程序中需要的包,如下: 7 | ```python 8 | import os, random 9 | from multiprocessing import Process, Pipe 10 | ``` 11 | 12 | 通过os模块的os.getpid()方法使得我们获得进程的PID。os.getpid()将以一种透明的方式返回程序的PID,在我们的程序中,它分别返回producer\_task进程和consumer\_task进程的PID。 13 | 下面我们将定义producer\_task方法,该方法返回1到10之间的一个随机数。producer\_task方法的关键是调用conn.send方法,conn以参数的形式在主函数中被传給producer\_task方法。producer\_task方法如下: 14 | ```python 15 | def producer_task(conn): 16 | value = random.randint(1, 10) 17 | conn.send(value) 18 | print('Value [%d] send by PID [%d]' % (value, os.getpid())) 19 | conn.close() 20 | ``` 21 | consumer进程将要执行的任务也很简单,它唯一的任务就是接收A进程传递过来的参数,接收本进程的PID,最终打印出来。consumer进程的中传入的consumer_task方法如下: 22 | ```python 23 | def consumer_task(conn) 24 | print('Value [%d] received by PID [%d]' % (conn.recv(), os.getpid())) 25 | ``` 26 | 27 | 最后一块将介绍如何调用Pipe()方法创建两个连接对象分别用于producer进程和consumer进程,然后通过参数形式各自传递到consumer\_task方法和producer\_task方法中去,主函数具体如下所是: 28 | ```python 29 | if __name__ == '__main__': 30 | producer_conn, consumer_conn = Pipe() 31 | consumer = Process(target=consumer_task,args=(consumer_conn,)) 32 | producer = Process(target=producer_task,args=(producer_conn,)) 33 | 34 | consumer.start() 35 | producer.start() 36 | 37 | consumer.join() 38 | producer.join() 39 | ``` 40 | 定义好进程之后,我们便可以调用进程对象的start方法开始执行进程,join方法用于分别等待producer进程和consumer进程执行完毕。下面的截图中我们将看到程序的输出: 41 | 42 | ##理解multiprocessing.Queue模块 43 | 之前小节中我们分析了如何在进程间创建通信通道来传递消息,现在我们将分析如何更有效的传递消息,这里我们使用mutilprocessing模块下的Queue对象。multoprocessing.Queue对象方法和queue.Queue对象方法类似。然后内在实现却不尽相同,比如multiprocess模块使用了内部线程feeder,把缓冲区中的数据传入目标进程相关连接的管道中。管道和队列机制均使用了消息传递机制,节省了使用同步机制带来的开销。 44 | -------------------------------------------------------------------------------- /第五章/总结.md: -------------------------------------------------------------------------------- 1 | 本章中我们介绍了多进程的概念,并使用多进程解决了两个小问题,分别是并行计算斐波那契数列值和设计网络爬虫。 2 | 下一章节我们将使用parallel Python模块执行多进程任务,parallel模块并不是python的内部模块。我们还将学习进程间通信相关的知识,使用pipes在进程间通信。 3 | -------------------------------------------------------------------------------- /第五章/理解进程的定义.md: -------------------------------------------------------------------------------- 1 | 线程是操作系统中程序执行和资源调度的基本单位。程序的执行由进程来管理,所涉及到的资源包括数据区,子进程,私有栈以及和其他进程间通信。 2 | 3 | ##理解进程模型 4 | 进程中定义了相关的信息和资源来确保对进程的操纵和控制,操作系统有一个叫PCB的结构来专门来存储这些信息和资源。例如,PCB结构保存如下的信息: 5 | 1. Process ID: 这是一个无符号整型数据,标识操作系统中的唯一进程。 6 | 2. 程序计数器: 对应下一条要执行的程序指令地址。 7 | 3. I/O信息: 包含一组打开的文件和进程相关的设备。 8 | 4. 内存分配: 该区域保存进程已经使用内存空间、为该进程预留的内存空间和页表信息。 9 | 5. CPU调度: 该区域保存进程优先级信息(and points to the staggering queues)。 10 | 6. 优先级: 定义进程获取CPU资源的优先级。 11 | 7. 当前状态: 表述该进程是准备状态、等待状态还是运行状态。 12 | 8. CPU申请: 保存栈指针和其他信息。 13 | 14 | ##定义进程状态 15 | 进程整个生命周期具有三种状态,分别如下: 16 | * 运行状态: 进程正占用cpu资源。 17 | * 准备状态: 处于进程队列中的进程已经准备好获取cpu资源。 18 | * 等待状态: 进程正在等待执行中的任务所需的I/O操作。 19 | -------------------------------------------------------------------------------- /第八章/ReadMe.md: -------------------------------------------------------------------------------- 1 | ##Doing Things Asynchronously -------------------------------------------------------------------------------- /第八章/使用asyncio.md: -------------------------------------------------------------------------------- 1 | 我们可以定义asyncio是一个Python中驱动异步编程的模块。ayncio模块使用下列组合来实现异步编程: 2 | - Event loop: asyncio模块允许每个进程一个事件循环。 3 | - Coroutines(协程): asyncio的官方文档中指出,coroutine是遵循一定规则的发生器。它最吸引人的特点是在执行的时候能够暂停等待外部处理,当外部处理完成后又可以从原来的位置恢复执行。 4 | - Futures: Futures代表尚未完成的processing。 5 | - Tasks: 是asyncio.Future的子类,用于管理coroutines。 6 | 7 | 除了这些机制,asyncio还为应用开发提供了很多其他机制,比如传输和协议,可以使用TCP、SSL、UDP和管道进行通信。关于asyncio更多的信息请查看 https://docs.python.org/3.4/library/asyncio.html。 8 | 9 | ### 理解coroutines和rutures 10 | 为了在asyncio中定义coroutine,我们使用@asyncio.coroutine装饰器。为了执行一个操作I/O或者其他可能阻塞循环事件的计算,我们必须使用`yield from`语法来暂停coroutine。但是暂停和恢复的机制怎样工作?Coroutine和asyncio.Future对象一起工作。我们可以总结操作如下: 11 | - Coroutine初始化,asyncio.Future在内部实例化或者作为参数传给coroutine。 12 | - 到达coroutine使用yield from的地方,coroutine暂停来等待yield from引发的计算。yield from实例等待yield from\构建。 13 | - 当yield from引发的计算结束,coroutine执行与coroutine关联的asyncio.Future对象的set_result(\)方法,通知事件循环coroutine可以被恢复。 14 | 15 | 16 | #### 使用coroutine和asyncio.Future 17 | 下面是使用coroutine和asyncio.Future对象的一些例子: 18 | import asyncio 19 | @asyncio.coroutine 20 | def sleep_coroutine(f): 21 | yield from asyncio.sleep(2) 22 | f.set_result("Done!") 23 | 在上述代码中,定义了一个协程sleep_coroutine,它接收一个asyncio.Future对象作为参数。在sleep_coroutine中,asyncio.sleep(2)会让协程睡眠2秒,asyncio.sleep已经和asyncio兼容。 24 | 在主函数中创建asyncio.Future对象,创建event loop对象。 25 | if __name__ == '__main__': 26 | future = asyncio.Future() 27 | loop = asyncio.get_event_loop() 28 | loop.run_until_complete(sleep_coroutine(future)) 29 | 30 | > event loop执行的时候,任务和协程才会执行。 31 | 32 | 在最后一行,loop.run_until_complete(sleep_coroutine(future)),很明显就是运行直到sleep_coroutine结束。 33 | 34 | #### 使用asyncio.Task 35 | asyncio.Task是asyncio.Future的子类,目的是管理协程。以下是一个例子,多个asyncio.Task将会在事件循环中被创建和分派。 36 | 37 | import asyncio 38 | @asyncio.coroutine 39 | def sleep_coro(name, seconds=1): 40 | print("[%s] coroutine will sleep for %d second(s)…" 41 | % (name, seconds)) 42 | yield yfrom asyncio.sleep(seconds) 43 | print("[%s] done!" % name) 44 | 45 | sleep_coro协程会接收两个参数,name用来标识协程,seconds用来定义睡眠时间。 46 | 47 | 在主函数中,定义了一个包含三个asyncio.Task对象的列表: 48 | 49 | if __name__ == '__main__': 50 | tasks = [asyncio.Task(sleep_coro('Task-A', 10)), 51 | asyncio.Task(sleep_coro('Task-B')), 52 | asyncio.Task(sleep_coro('Task-C'))] 53 | loop.run_until_complete(asyncio.gather(*tasks)) 54 | 55 | 程序的运行结果如下: 56 | ![](图片链接地址) 57 | 58 | 值得注意的是,程序的输出表明任务执行的顺序和申明的顺序一致,它们都不能阻塞event loop。 59 | 60 | #### 使用和asyncio不兼容的库 61 | 62 | asyncio是python新加入的模块,一些库还不能很好的兼容。我们重新实现之前章节的例子asyncio_task_sample.py,用time.sleep替换asyncio.sleep。运行结果如下: 63 | 64 | ![](图片链接地址) 65 | 66 | -------------------------------------------------------------------------------- /第八章/异步的做事.md: -------------------------------------------------------------------------------- 1 | 在之前的章节我们学习了用Celery框架分发任务,并且在同一个网络的不同机器上并行执行。现在我们将学习异步编程、事件循环和协程,它们都在Python3.4的asyncio模块占有重要的地位。 2 | 本章将覆盖以下内容: 3 | 4 | - 阻塞、非阻塞和异步操作 5 | - 理解事件循环 6 | - 使用异步IO(asyncio) 7 | 8 | -------------------------------------------------------------------------------- /第八章/总结.md: -------------------------------------------------------------------------------- 1 | 在本章节,我们学习了异步、阻塞、非阻塞编程。为了了解这些行为,我们使用asyncio模块的基本机制写了一些例子。 2 | 3 | asyncio模块是对python异步编程进行革命的一个尝试。吉多范罗苏姆在探索性选择和提取基本机制为这些选择提供清晰的API方面非常成功。`yield from`语法产生是为了增强一些使用协程的程序的表现力,使程序员免去写回调函数的负担。除此之外,asyncio模块拥有与其它应用程序集成的能力。 4 | 5 | 快到本书的结束了,写这本书还是很有挑战性的,希望它对你有所帮助。本书中有很多东西没有介绍,比如 IPython, mpi4py, Greenlets, Eventlets, 等等。 6 | 7 | 基于本书提供的内容,你可以自己做实验比较不同的工具。几乎在本书的每一个章节都用了两个相关例子来介绍,这也说明了Python可以在不改变核心代码的基础上灵活的替换不同的工具。 8 | -------------------------------------------------------------------------------- /第八章/理解事件循环.md: -------------------------------------------------------------------------------- 1 | 为了理解事件循环的概念,我们必须了解其内部结构。 2 | 我们将用资源描述符代表套接口描述符和文件描述符。 3 | 4 | ### 轮询功能 5 | 6 | 不同的操作系统为了监控一个或多个资源描述符都实现了轮询功能。轮询方法是事件循环的基础,轮询方法会通知感兴趣的事件,资源描述符就会准备好做交互。然而,感兴趣的可能并不能完成理想的操作。 7 | 8 | linux有下列轮询方法: 9 | 10 | - select(): POSIX实现有以下几个缺点: 11 | - 监控资源描述符的数量有限 12 | - O(n)时间复杂度,n代表连接的客户端数 13 | - poll(): select()的增强版,有以下特点: 14 | - 允许监控更大范围的资源描述符 15 | - O(n)时间复杂度 16 | - 支持更多类型的监控事件 17 | - 和select()对比,可以复用entry数据 18 | - 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写数据,消费者等待数据。 19 | - 水平触发:当消费者调用epoll_wait(),它将得到资源描述符的状态并且立即返回,表明触发或者不触发读操作。水平触发和事件的状态有关而不是事件本身。 20 | - 边缘触发:epoll_wait()调用只有当写事件完成之后才会返回。所以边缘触发和事件有关。 21 | 22 | 23 | 轮询方法工作的步骤如下: 24 | 1. 创建poller对象 25 | 2. poller中注册或者不注册1个或多个资源描述符 26 | 3. 轮询方法在poller对象中执行 27 | 28 | > Poller是一个提供使用轮询方法的抽象接口 29 | 30 | ### 使用事件循环 31 | 32 | 我们可以定义事件循环来简化使用轮询方法来监控事件。事件循环利用poller对象,使得程序员不用控制任务的添加、删除和事件的控制。 33 | 事件循环使用回调方法来知道事件的发生。例如,有一个资源描述符A,当一个写事件在A中发生就会调用一个回调函数。一些实现了事件循环的应用如下: 34 | - Tornado web server ( http://www.tornadoweb.org/en/stable/ ) 35 | - Twisted ( https://twistedmatrix.com/trac/ ) 36 | - asyncio ( https://docs.python.org/3.4/library/asyncio.html ) 37 | - Gevent ( http://www.gevent.org/ ) 38 | - Eventlet ( https://pypi.python.org/pypi/eventlet ) 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /第八章/理解阻塞非阻塞和异步操作.md: -------------------------------------------------------------------------------- 1 | 理解任务执行的不同方式对于构建一个可伸缩的解决方案非常重要。正确的运用异步、阻塞和非阻塞操作能大大改善系统的响应时间。 2 | 3 | ### 理解阻塞操作 4 | 5 | 可以用银行职员服务客户的例子来看阻塞操作。当轮到客户的号码时,银行职员就只为该客户服务直到服务完成。银行职员不能同时为多个客户办理业务。当只有2名银行职员但是每小时来100名顾客时,进度就会很缓慢。这个例子就描述了阻塞操作,当一个任务要等待另一个任务结束时,阻塞其对资源的访问。 6 | 7 | ### 理解非阻塞操作 8 | 9 | 非阻塞操作和异步操作很容易混淆,它们是不同的概念。用一个现实的场景来说明,比如你去银行咨询一个业务,银行职员说现在还没有结果,你稍后再来或者过几天再来。这就是非阻塞操作。 10 | 11 | > 非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回,之后再retry。 12 | 13 | 14 | ### 理解异步操作 15 | 16 | 假设有两个银行职员,没个职员有10个助理。这时,如果来了一个客户,他需要办理的业务耗时比较长,那么就请一个助理到后台单独为该客户服务。这样就不会阻塞其他客户。 17 | 18 | > 注册一个回调函数,当条件满足时会触发该函数。 19 | -------------------------------------------------------------------------------- /第六章/ReadMe.md: -------------------------------------------------------------------------------- 1 | ##Utilizing Parallel Python 2 | 之前的章节,我们学习了怎么使用multiprocessing模块和ProcessPoolExecutor模块来解决两个问题.这章将介绍命名管道并展示如何使用Parallel Python(PP)模块实现多进程的并行任务。 3 | 本章会覆盖下面几个知识点: 4 | * 理解进程间通信 5 | * 了解Parallel Python(PP) 6 | * 在SMP架构上使用PP计算斐波那契序列 7 | * 使用PP创建分布式的网络爬虫 8 | -------------------------------------------------------------------------------- /第六章/了解pp模块.md: -------------------------------------------------------------------------------- 1 | ##了解PP模块 2 | 上一章中,我们介绍了直接使用系统调用创建进程间通讯的方法,这是一种很低层的机制. 并且它只在Linux或Unix环境下才有效. 接下来我们会使用一个名为PP的python模块来创建IPC通讯,这种通讯不仅仅可以在本地进程间进行,还能通过计算机网络在物理上彼此分散的进程间进程. 3 | 4 | 关于PP模块的文档并不多,可以在http://www.parallelpython.com/component/option.com_smf/ 中找到相关文档和FAQ. API提供了大量的使用该工具的说明. 十分的简洁明了 5 | 6 | 使用PP模块的最大优势在于它提供了一个抽象层. PP模块的重要特征如下所示: 7 | - 自动探测进程的数量并以此改进负载均衡 8 | - 可以在运行期改变要投入的处理器数量 9 | - 运行期自动均衡负载 10 | - 通过网络自动探测资源 11 | 12 | PP模块有两种方式来执行并行代码. 第一种方式基于SMP架构,即在同一台机器上有多个处理器/核心. 第二中方式将网络中的各个机器配置,整合成区块,并将任务分配給这些机器去运行. 无论哪一种方式,进程间消息交换的过程都是抽象的. 这使得我们无需关系其低层的实现方式到底是通过管道还是socket. 我们只需要使用回调函数来通过参数和函数的方式来交换信息就行了. 下面给个例子. 13 | 14 | 在PP的API中有一个名为Server的类,使用该类可以实现在本地和远程的进程间封装和分派任务. Server的构造函数(\_\_init\_\_)中有几个参数比较重要: 15 | 16 | * ncpus: 该参数用于指定执行任务的工作进程数量. 若没有指定该参数,则会自动根据机器上处理器/核心的数量来创建工作进程,以后话资源的使用 17 | 18 | * ppservers: 该参数是一个元组,该元组的元素为并行Python执行服务器(PPES)的名称或IP地址. 一个PPES由连入网络的机器组成. 且该机器通过ppsever.py共组运行并等待待执行的任务. 19 | 20 | 其他参数的说明请参阅http://www.parallelpython.com/content/view/15/30/ 21 | 22 | Server对象的实例拥有很多方法,其中submit方法允许我们分配任务到各个工作进程. submit函数具有如下签名: 23 | ```python 24 | submit(self, func, args=(), depfuncs=(), modules=(), 25 | callback=None, callbackargs=(), group='default', 26 | globals=None) 27 | ``` 28 | 29 | 在submit方法中,我们集中关注以下几个参数: 30 | * func: 该函数会被本地进程或远程服务器执行 31 | * args: 该参数提供了了执行func函数时的参数 32 | * modules: 该参数说明远程代码(remote code)或进程为了调用func函数,需要导入哪些模块. 例如若被分配的函数用到了time模块,则modules=('time',) 33 | * callback :执行完func后的回调函数,func的执行结果会作为其函数参数. 常用于对func的执行结果作进一步加工 34 | 35 | 还有其他的参数将会在下一张分析代码时进行说明. 36 | -------------------------------------------------------------------------------- /第六章/使用pp模块创建分布式网络爬虫.md: -------------------------------------------------------------------------------- 1 | 目前我们已经使用pp组件在本机上实现多进程并发,接下来我们将在分布式环境下使用pp组件,分布式硬件环境如下: 2 | 3 | - Iceman-Thinkpad-X220: Ubuntu 13.10 4 | - Iceman-Q47OC-500P4C: Ubuntu 12.04 LTS 5 | - Asgard-desktop: Elementary OS 6 | 7 | 我们将在如上列举的三台机器上测试pp组件在分布式环境下的使用。对此,我们实现了分布式网络爬虫。web_crawler_pp_cluster.py方法中,将input_list列举的URL分发到本地以及远端进程执行,web_crawler_pp_cluster.py中的回调函数将组织这些URL以及以及通过它们找到的前三个连接(URL)。 8 | 9 | 让我们分析代码,一步步理解怎样实现上述功能。首先import使用到的库以及定义数据结构。然后定义input_list列表用于存放入口URL和result_dict存放爬取结果。代码如下所示: 10 | 11 | 接下来分析aggregate_results方法,该方法还是作为回调函数,相对于前一小结的aggreate_results方法,改变不多。返回meesage的格式的发生了变化,aggregate_resullt方法的传入参数变成tuple,分别保存执行该方法的PID号,hostname和找到的前三个URL。代码如下: 12 | 13 | 接下来定义crawl_task方法,作为Server类方法。和之前章节的crawl_task方法功能类似,依据传入的RUL获取网页上的其他链接(RUL),唯一的不同是返回值是tuple,代码如下: 14 | 15 | 在main方法和callback方法定义之后,我们需要初始化Server类实例,以至于能够在分布式环境下执行网络爬虫任务。我们注意到pp.Server类有三个参数,第一个参数是执行Server类方法的IP或hostname,我们的例子中,除了本机之外,还需要定义另外两台机器的IP和hostname,定义如下所示: 16 | 17 | pp.Server类的初始化如下: 18 | 19 | 我们注意到初始化有三个参数,第一个参数不同的是被赋值为1,使得pp组件创建一个本地进程,如果需要的话,把其他任务转发到远端机器上执行。第二个参数是之前定义的ppservers。第三个参数定义socket连接超时时间,通常为了测试目的,我们把timeout时间设置很长,防止因为网络超时而导致socket连接终止。 20 | 21 | Server类创建之后,我们将遍历url_list,并提交crawl_task方法。 22 | 23 | 相对于之前的之前章节,变化是传入的组件较之于斐波那契序列不同。 24 | 25 | 我们需要等待网络爬虫执行完毕,得到爬虫结果。 26 | 27 | 执行程序之前,我们需要执行远端机器上的ppserver.py模块。Python ppserver.py -a -d 为shell命令。-a选项用于设置自动发现,使得client自动发现网络中没有设置ip的server。-d选项设置debug模式,显示程序执行过程中产生的log。 28 | 29 | 接下来我们定义输出格式: 30 | 31 | 1. 首先,下面的屏幕显示main节点的执行信息,包括执行和分发远端任务。 32 | 2. 然后,执行ppserver.py脚本,执行任务信息如下屏幕内容所示。 33 | 3. 在之前屏幕中,我们打印了有趣的统计信息,包括在远端机器上执行的任务数量,每个人物执行的时间和每个远端机器上的任务总数。截图上反应的另外一个信息是,我们应该限制callback函数任务量,因为callback函数都是在main节点上执行,这可能会成为成为整个系统的瓶颈,当然也取决于不同的应用场景。 34 | 4. 下面的截图是的debug模式下ppserver.py脚本在iceman-Q470C-500P4C机器上的的执行日志。 35 | 5. 下面截图是debug模式下ppserver.py脚本在asgard-desktop机器上的执行日志。 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /第六章/在SMP架构上使用pp模块计算斐波那契序列.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Voidly/Parallel-Programming-with-Python/c14a6e132d1e6c5bb2d836b86d394e03d602ff6c/第六章/在SMP架构上使用pp模块计算斐波那契序列.md -------------------------------------------------------------------------------- /第六章/总结.md: -------------------------------------------------------------------------------- 1 | 本章我们学习了使用更低层次的组件在没有直接关系的进程间通信。此外,我们介绍了PP模块,该模块提供本地进程和远程服务器进程通信的方法。PP模块用于创建简单,小型,并发和分布式的python应用。 2 | 下章我们将学习怎么使用Celery执行并发,分布式任务。 3 | -------------------------------------------------------------------------------- /第六章/理解进程间通信.md: -------------------------------------------------------------------------------- 1 | ##理解进程间通讯 2 | IPC是允许多进程间通信的一种机制。 3 | 有许多IPC的实现方式,他们依赖于不同架构的运行环境。运行在同一台机器上的进程可以由多种进程间通信方式,比如共享内存、消息队列和管道。如果多进程处于物理上分布式集群上上,我们可以使用RPC的方式。 4 | 第五章中我们介绍了Multiprocessing和ProcessPoolExecutor模块,学习了常规的管道的用法。我们学习了同一个父进程的各个子进程间通信方式,但是有时候我们需要在不相关的进程间传递信息(子进程不共有同一个父进程),我们会想,通过不相关进程的地址空间,是否能够在它们之间建立通信。然而一个进程不能获取另外一个进程的地址空间,因此我们需要使用一种新的机制——命名管道。 5 | 6 | ###探索命名管道 7 | 对于POSIX系统,例如linux,我们可以把所有东西归结为文件,我们每操作一个任务,我们可以找到一个文件与之对应,同时还有一个文件描述符与该文件相联系,通过文件描述符就可以操作该文件. 8 | 9 | 命名管道是一种允许通过某些特殊文件的文件描述符实现IPC通讯的机制.这些特殊文件使用特殊的模式(例如先入先出)来读写数据.在对信息的管理上命名管道不同于常规管道,命名管道通过文件系统中的特殊文件及其文件描述符来实现,而普通管道是在内存中创建的. 10 | 11 | ###在python中使用命名管道 12 | 在python中使用命名管道很容易. 下面我们会通过两个非直接通讯的程序来展示如何使用命名管道. 第一个程序名为write\_to\_named_pip.py,它的作用是写一条22个字节的消息到管道中,该消息包括一个信息字符串及产生消息的PID. 第二个程序名为read\_from\_named\_pip.py, 它会从管道中读取消息并展示消息内容及其PID. 13 | 14 | 在执行的最后,read\_from\_named\_pipe.py进程会显示一条形如"I pid [] received a message => Hello from pid [the PID of writer process"的消息 15 | 16 | 为了展示读写进程的相互依赖关系,我们会在两个独立的控制台上运行这两个程序. 在展示结果前,让我们先分析一下两个程序的代码. 17 | 18 | ####往命名管道写入数据 19 | 在python中,命名管道是通过系统调用来实现的. 下面我们会逐行解释write\_to\_named\_pipe.py中代码的功能. 20 | 21 | 我们首先导入os模块,这样我们在后面的代码中才能调用系统调用. 22 | ```python 23 | import os 24 | ``` 25 | 26 | 接下来我们会解释\_\_main\_\_代码块,在该代码块中创建了命名管道以及一个用于存储消息的FIFO的特殊文件. \_\_main\_\_代码块中的第一行代码定义了命名管道的标签. 27 | ```python 28 | named_pipe = "my_pipe" 29 | ``` 30 | 31 | 接下来我们检查该命名管道是否已经存在,若不存在,则调用mkfifo系统调用来创建这个命名管道. 32 | ```python 33 | if not os.path.exists(named_pipe): 34 | os.mkfifo(named_pipe) 35 | ``` 36 | 37 | mkfifo调用会创建一个特殊的文件,该文件对通过命名管道读写的消息实现了FIFO机制. 38 | 39 | 我们再以一个命名管道和一个行如"Hello from pid [%d]"的消息来作为参数调用函数write\_message. 该函数会将消息写入到(作为参数传递给它的)命名管道所代表的文件中. write\_message函数定义如下: 40 | ```python 41 | def write_message(input_pipe, message): 42 | fd = os.open(input_pipe, os.O_WRONLY) 43 | os.write(fd, (message % str(os.getpid()))) 44 | os.close(fd) 45 | ``` 46 | 47 | 我们可以观察到,在函数的第一行,我们调用一个系统调用:open. 该系统调用若成功的话会返回一个文件描述符, 通过该文件描述符我们就能够读写那个FIFO文件中的数据. 请注意,我们可以通过flags参数控制打开FIFO文件的模式. 由于write\_message函数紧紧需要写数据,因此我们使用如下代码: 48 | ```python 49 | fd = os.open(input_pipe, os.O_WRONLY) 50 | ``` 51 | 52 | 在成功打开命名管道后,我们使用下面代码写入消息: 53 | ```python 54 | os.write(fd, (message % os.getpid())) 55 | ``` 56 | 57 | 最后,请一定记着使用close关闭通讯渠道,这样才能释放被占用的计算机资源. 58 | ```python 59 | os.close(fd) 60 | ``` 61 | 62 | ####从命名管道读取数据 63 | 我们实现了read\_from\_pipe.py来读取命名管道. 当然,改程序也需要借组os模块才能操作命名模块. 改程序的主要代码很简单:首先,我们定义了所使用命名管道的标签,该标签需要与写进程所用的命名管道同名. 64 | ```python 65 | named_pipe = "my_pipe" 66 | ``` 67 | 68 | 然后,我们调用read\_message函数,该函数会读取write\_to\_named\_pipe.py写入的内容. read\_message函数的定义如下: 69 | ```python 70 | def read_message(input_type): 71 | fd = os.open(input_pipe, os_RONLY) 72 | message = ( 73 | "I pid [%d] received a message => %s" 74 | % (os.getpid(), os.read(fd, 22)) 75 | os.close(fd) 76 | return message 77 | ``` 78 | 79 | 上面代码中,open调用相比无需再介绍了,唯一没见过的只有read调用. 该调用从命名管道中读取指定字节的内容. 这里我们从文件描述符中读取22个字节的内容. 消息被读取出来之后,又被该函数作为返回值返回. 最后依然记得调用close调用来关闭通讯渠道. 80 | 81 | 最终,下面的截屏显示了write\_to\_named\_pip和read\_from\_named\_pipe程序的执行结果. 82 | -------------------------------------------------------------------------------- /第四章/ReadMe.md: -------------------------------------------------------------------------------- 1 | #使用threading和concurrent.futures模块 2 | 3 | 在上面的章节里,我们列举了一些能够被并行化解决的潜在问题. 在本章中,我们会分析如何使用Python中的threading模块来解决这些问题. 4 | 5 | 本章包含如下议题: 6 | - 定义什么是线程 7 | - 选择使用threading库还是_thread库 8 | - 使用threading模块来为多个输入同时计算Fibonacci序列 9 | - 使用concurrent.futures模块爬取web信息 10 | -------------------------------------------------------------------------------- /第四章/使用concurrent.futures模块爬取web信息.md: -------------------------------------------------------------------------------- 1 | ##使用concurrent.futures模块爬取web信息 2 | 3 | 下面的章节会实现一个并行的web爬虫. 4 | 5 | 在实现时,我们会应道concurrent.futures模块中一个很有意思的类,叫做ThreadPoolExecutor 在上一章节的例子中,我们分析了parallel\_fibonacci.py是如何实现并发的,它只是以最原始的方式来使用进程,在某一特定的时候需要我们手工来创建和初始化一个个线程. 然而在大型程序中还想这样手工管理线程就太困难了. 在开发大型程序时,我们常常要用到线程池机制. 线程池是一种用于在进程中管理预先创建的多个线程的一种数据结构. 使用线程池的目的是为了复用线程,这样就可以避免不断的创建线程所照成的资源浪费. 6 | 7 | 基本上和前一章节一样, 我们将会设计一个算法,该算法分阶段地执行一些任务,并且这些任务也会相互影响. 下面,让我们分析一下这个并发网络爬虫的代码 8 | 9 | 在导入必要的模块,并设置好日志文件后,我们使用内置模块re来创建一个正则表达式(re模块的完整文档可以在http://docs.python.org/3/howto/regex.html中找到). 我们会使用该正则表达式来过滤爬取阶段返回的连接集合. 相关代码如下所示: 10 | 11 | ```python 12 | html_link_regex = \ 13 | re.compile('') 14 | ``` 15 | 16 | 接下来我们创建一个同步队列来模拟输入数据. 然后我们创建一个名为result\_dict的字典实例. In this, we will correlate the URLs and their respective links as a list structure. 相关代码如下: 17 | 18 | ```python 19 | urls = queue.Queue() 20 | urls.put('http://www.google.com') 21 | urls.put('http://br.bing.com/') 22 | urls.put('https://duckduckgo.com/') 23 | urls.put('https://github.com/') 24 | urls.put('http://br.search.yahoo.com/') 25 | result_dict = {} 26 | ``` 27 | 28 | 再接下来我们定义一个名为group\_urls\_task的函数,该函数用于从同步队列中抽取出URL并存入result\_dict的key值中. 另一个应该留意的细节是,我们调用Queue的get方法是,带了两个参数,第一个参数为True表示阻塞其他线程访问这个同步队列,第二个参数是0.05表示阻塞的超时事件,这样就防止出现由于同步队列中没有元素而等待太长事件的情况出现. 毕竟,在某些情况下,你不会想化太多的时间来等待新元素的到来. 相关代码如下: 29 | 30 | ```python 31 | def group_urls_task(urls): 32 | try: 33 | url = urls.get(True, 0.05) 34 | result_dict[url] = None 35 | logger.info("[%s] putting url [%s] in dictionary..." % ( 36 | threading.current_thread().name, url)) 37 | except queue.Empty: 38 | logging.error('Nothing to be done, queue is empty') 39 | ``` 40 | 41 | 现在我们需要有一个在爬行阶段执行的任务,该任务将每个url作为参数传递给一个名为crawl\_task的函数. 当将URL所指页面中的所有连接都保存下里之后,爬行阶段就算是完成了. 爬行过程中会返回一个元组,且该元组的第一个元素就是传递給crawl\_task函数的URL参数. 在这个步骤中,会从URL所指页面中抽取出一个连接的列表. 获取URL所指网页的内容需要用到request模块(关于request模块的官方文档请参见https://pypi.python.org/pypi/requests ) 42 | 43 | ```python 44 | def crawl_task(url): 45 | links = [] 46 | try: 47 | request_data = requests.get(url) 48 | logger.info("[%s] crawling url [%s] ..." % ( 49 | threading.current_thread().name, url)) 50 | links = html_link_regex.findall(request_data.text) 51 | except: 52 | logger.error(sys.exc_info()[0]) 53 | raise 54 | finally: 55 | return (url, links) 56 | ``` 57 | 58 | 进一步分析代码,我们会发现创建了一个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对象中预定的函数. 相关代码如下: 59 | 60 | ```python 61 | with concurrent.futures.ThreadPoolExecutor(max_workers=3) as\ 62 | group_link_threads: 63 | for i in range(urls.qsize()): 64 | group_link_threads.submit(group_urls_task, urls) 65 | ``` 66 | 67 | 随后,我们还要再创建一个ThreadPoolExecutor对象, 不过这一次我们使用上一阶段中group\_urls\_task所产生的key值作为参数来执行爬行的动作. 这一次我们所使用的代码有些不同. 68 | ```python 69 | future_tasks = {crawler_link_threads.submit(crawl_task, url): url 70 | for url in result_dict.keys()} 71 | ``` 72 | 73 | 我们映射了一个名为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结果如下所示. 74 | 75 | 又一次,我们可以发现每个线程池中的线程执行是乱序的,但这不重要,重要的是,result\_dict中输出的内容就是最终结果. 76 | -------------------------------------------------------------------------------- /第四章/使用threading模块解决斐波那契序列多输入问题.md: -------------------------------------------------------------------------------- 1 | ##使用多线程解决斐波那契序列多输入问题 2 | 接下来我们将实践python多线程的使用。任务是并行处理斐波那契序列多输入值。为了把问题说清楚,我们将把输入分成四部分,四个线程分别处理一个输入数据。算法描述如下: 3 | 4 | 1. 首先使用一个列表存储四个待输入值,这些值将被放入对于线程来说互相锁定的数据结构。 5 | 2. 输入值被放入可被锁定的数据结构之后,负责处理斐波那契序列的线程将被告知可以被执行。这时,我们可以使用python线程的同步机制Condition模块(Condition模块对共享变量提供线程之间的同步操作),模块详情请参考:[http://docs.python.org/3/library/threading.html#threading.Condition](http://docs.python.org/3/library/threading.html#threading.Condition)。 6 | 3. 当每个线程结束斐波那契序列的计算后,分别把结果存入一个字典。 7 | 8 | 接下来我们将列出代码,并且讲述其中有趣的地方: 9 | 10 | 代码开始处我们加入了对编码额外的支持,导入logging, threading和queue模块。此外,我们还定义了我们例子中用到主要数据结构。一个字典,被命名为fibo_dict,将用来存储输入输出数据,输入数据为key,计算结果(输出数据)为值。我们同样定义了一个队列对象,该对象中存储线程间的共享数据(包括读写)。我们把该对象命名为shared\_queue。最后我们定义一个列表模拟程序的四个输入值。代码如下: 11 | 12 | ```python 13 | 14 | #coding: utf-8 15 | import logging, threading 16 | from queue import Queue 17 | logger = logging.getLogger() 18 | logger.setLevel(logging.DEBUG) 19 | formatter = logging.Formatter('%(asctime)s - %(message)s') 20 | ch = logging.StreamHandler() 21 | ch.setLevel(logging.DEBUG) 22 | ch.setFormatter(formatter) 23 | logger.addHandler(ch) 24 | fibo_dict = {} 25 | shared_queue = Queue() 26 | input_list = [3, 10, 5, 7] 27 | ``` 28 | 29 | 接下来的一行代码,我们从threading模块中定义了一个Condition对象,该对象根据一定的条件同步各线程存取资源的操作。 30 | 31 | ```python 32 | 33 | queue_condition = threading.Condition() 34 | ``` 35 | 36 | 使用Condition对象用于控制线程的创建队列。 37 | 38 | 下一块代码定义了一个很多线程都需要调用的方法,我们把它命名为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()方法,通知某一个任务已经被分离并执行。代码如下: 39 | 40 | ```python 41 | 42 | def fibonacci_task(condition): 43 | with condition: 44 | while shared_queue.empty(): 45 | logger.info("[%s] - waiting for elements in queue.." 46 | % threading.current_thread().name) 47 | condition.wait() 48 | else: 49 | value = shared_queue.get() 50 | a, b = 0, 1 51 | for item in range(value): 52 | a, b = b, a + b 53 | fibo_dict[item] = a # 这里书中是fibo_dict[value] = a,但是觉得重复赋值没有意义 54 | shared_queue.task_done() 55 | logger.debug("[%s] fibonacci of key [%d] with 56 | result [%d]" % 57 | (threading.current_thread().name, value, 58 | fibo_dict[value])) 59 | ``` 60 | 61 | 我们定义的第二个函数是queue\_task,该函数被负责计算shared\_queue的值的线程所调用。我们看到condition对象作为获得shared\_queue的协议。input\_list中的每一个值都将被插入到shared\_queue中去。当所有的值都被插入到shared\_queue中后,告知负责计算斐波那契序列的方法shared\_queue已经可以使用。 62 | 63 | ```python 64 | 65 | def queue_task(condition): 66 | logging.debug('Starting queue_task...') 67 | with condition: 68 | for item in input_list: 69 | shared_queue.put(item) 70 | logging.debug("Notifying fibonacci_task threads 71 | that the queue is ready to consume..") 72 | condition.notifyAll() 73 | ``` 74 | 75 | 接下来我们将创建四个线程等待shared\_queue可以被使用条件。线程将执行target参数作为回调函数,代码如下: 76 | 77 | ```python 78 | 79 | threads = [threading.Thread( 80 | daemon=True, target=fibonacci_task, 81 | args=(queue_condition,)) for i in range(4)] 82 | ``` 83 | 84 | 接着我们使用thread对象的start方法开始线程: 85 | 86 | ```python 87 | 88 | [thread.start() for thread in threads] 89 | ``` 90 | 91 | 然后我们创建一个线程处理shared\_queue,然后执行该线程。代码如下: 92 | 93 | ```python 94 | 95 | prod = threading.Thread(name='queue_task_thread', daemon=True, 96 | target=queue_task, args=(queue_condition,)) 97 | prod.start() 98 | ``` 99 | 100 | 最后,我们多计算斐波那契序列的所有线程都调用join()方法,使用join()方法的目的是,让主线程等待子线程的调用,直到所有子线程执行完毕之后才结束子线程。 101 | 102 | ```python 103 | 104 | [thread.join() for thread in threads] 105 | ``` 106 | 107 | 程序的执行结果如下: 108 | 109 | 注意到,第一个fibonacci\_task线程被创建和初始化后,它们进入等待状态。同时,queue\_task线程被创建并且生成shared\_queue队列。最后,queue\_task方法告知fibonacci_task线程可以执行它们的任务。 110 | 111 | 注意到,程序每次执行的过程都不一样,这也是多线程的特性之一。 112 | -------------------------------------------------------------------------------- /第四章/定义什么是线程.md: -------------------------------------------------------------------------------- 1 | ##什么是线程 2 | 线程表示的是进程中不同的执行流程. 让我们把一个程序相信成是一个蜂箱,该程序的一个动作是将划分收集到这个蜂箱内. 这些搜集动作是由许多个工蜂同时进行的. 这个例子中,工蜂扮演的就是线程的角色, 它们工作在进程内部并且共享资源来完成它们的任务. 3 | 4 | 同一个进程中的线程共享内存空间. 因此开发者的任务就是控制和访问这些内存区域. 5 | 6 | ###使用线程的优势和劣势 7 | 是否使用线程需要权衡利弊,这依赖于用于实现解决方案的编程语言和操作系统. 8 | 9 | 使用线程的优势如下所示: 10 | - 同一进程的不同线程之间交流的速度,数据单元(data location)的速度和共享信息的速度都很快 11 | - 创建线程的花费要远远少于创建进程的花费,这时因为它无需拷贝主进程上下文环境中的那些信息 12 | - 通过处理器的缓存机制可以优化内存存取,这样就能够充分利用数据局部性(data locality)的优势 13 | 14 | 使用线程的劣势如下: 15 | - 数据共享可以加速通讯. 然而开发新手使用线程也很容引进难以解决的错误 16 | - 数据共享限制了解决方案的灵活性. 若想将其迁移到分布式架构上,会是件很头疼的事情. 总的来说,它限制了算法的可扩展性 17 | 18 | [就Python编程语言来说, 计算密集型的线程会由于GIL的存在而影响程序的性能] 19 | 20 | ###理解不同类型的线程 21 | 存在两种类型的线程:内核线程与用户线程. 其中,内核线程是指由操作系统创建和管理的线程. 内核线程的上下文切换,调度和销毁都由当前操作系统的内核来管理. 而对于用户线程,这些东西都由开发者来控制. 22 | 23 | 每种线程都有其优势: 24 | 25 | 内核线程的优势如下: 26 | - 一个内核线程其实就是一个进程. 因此即使一个内核线程被阻塞了,其他的内核线程也照样运行 27 | - 不同的内核线程可以运行在不同的CPU上 28 | 29 | 内核线程的劣势如下: 30 | - 创建线程和线程间同步的消耗太大 31 | - 实现依赖于平台 32 | 33 | 用户线程的优势如下: 34 | - 用户线程的创建和线程间同步的开销较少 35 | - 用户线程是平台无关的 36 | 37 | 用户线程的劣势如下: 38 | - 同一进程中的所有用户线程都对应一个内核线程. 因此,若该内核线程被阻塞,则所有相应的用户线程都会被阻塞 39 | - 不同用户线程无法运行在不同CPU上 40 | 41 | ###定义线程的状态 42 | 在线程的生命周期中,有5中状态: 43 | - 新建: 该过程的主要动作就是创建一个新线程, 创建完新线程后,该线程被发送到待执行的线程队列中 44 | - 运行: 该状态下,线程获取到并消耗CPU资源 45 | - 就绪: 该状态下,线程在待执行的线程队列中排队,等待被执行 46 | - 阻塞: 该状态下,线程由于等待某个事件(例如I/O操作)的出现而被阻塞. 这时线程并不使用CPU 47 | - 死亡: 该状态下,线程释放执行时使用的资源并结束整个线程的生命周期 48 | 49 | ###是使用threading模块还是_thread模块 50 | 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会是更明智的选择 51 | -------------------------------------------------------------------------------- /第四章/总结.md: -------------------------------------------------------------------------------- 1 | ##总结 2 | 在本章,我们聚焦于使用线程的理论方法. 通过使用threading模块和concurrent.futures模块,我们实现了上一章所展示的案例,并通过这种方式展示这些模块的机制和灵活性 3 | 4 | 下一章我们会使用multiprocessing和ProcessPoolExecutor这两个模块来再依次解决这两个问题. 5 | 6 | --------------------------------------------------------------------------------