Skip to content

Commit 51c1abf

Browse files
author
wangchao
committed
更新完善第四章
1 parent 0ac136a commit 51c1abf

6 files changed

+214
-177
lines changed

docs/chapter4/使用concurrent_futures模块爬取web信息.md renamed to docs/chapter4/crawling_the_web_using_the_concurrent_futures_module.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 使用concurrent.futures模块爬取web信息
22

3+
Crawling_the_Web_using_the_concurrent_futures_module
4+
35
下面的章节会实现一个并行的web爬虫.
46

57
在实现时,我们会应道concurrent.futures模块中一个很有意思的类,叫做ThreadPoolExecutor 在上一章节的例子中,我们分析了parallel\_fibonacci.py是如何实现并发的,它只是以最原始的方式来使用进程,在某一特定的时候需要我们手工来创建和初始化一个个线程. 然而在大型程序中还想这样手工管理线程就太困难了. 在开发大型程序时,我们常常要用到线程池机制. 线程池是一种用于在进程中管理预先创建的多个线程的一种数据结构. 使用线程池的目的是为了复用线程,这样就可以避免不断的创建线程所照成的资源浪费.

docs/chapter4/defining_threads.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# 什么是线程
2+
3+
**线程是进程中的不同执行线**。 让我们把一个程序想象成一个蜂巢,在这个蜂巢内部有一个收集花粉的过程。 这个采集过程是由几只工蜂同时工作来完成的,以解决花粉不足的问题。 工蜂扮演着线程的角色,在进程内部活动并共享资源来执行它们的任务。
4+
5+
线程属于同一个进程,共享同一个内存空间。 因此,开发人员的任务是控制和访问这些内存区域。
6+
7+
## 使用线程的优点和缺点
8+
9+
在决定使用线程时必须考虑一些**优点****缺点**,这取决于用于实现解决方案的语言和操作系统。
10+
11+
使用线程的**优势**如下所示:
12+
13+
- 同一进程内的线程**通信****数据定位****共享信息**的速度快
14+
- 线程的创建比进程的创建成本更低,因为不需要复制主进程上下文中包含的所有信息
15+
- 通过处理器的高速缓存优化内存访问,充分利用**数据局部性**(data locality)的优势。
16+
17+
使用线程的**缺点**如下:
18+
19+
- 数据共享允许快速通信。 但是,它也允许缺乏经验的开发人员引入难以解决的错误。
20+
- 数据共享限制了解决方案的灵活性。 例如,迁移到分布式架构可能会让人头疼。 通常,它们限制了算法的可扩展性。
21+
22+
!!! info ""
23+
24+
在 Python 编程语言中,由于 GIL,使用**计算密集型**(CPU-bound)的线程可能会损害应用程序的性能。
25+
26+
## 理解不同类型的线程
27+
28+
有两种类型的线程,**内核线程****用户线程****内核线程是由操作系统创建和管理的线程**, 其上**下文的交换****调度****结束**都由当前操作系统的内核来进行管理。 对于**用户线程**,这些状态由****(package)开发人员控制。
29+
30+
我们可以引用每种线程的一些优点:
31+
32+
<table>
33+
<thead>
34+
<tr>
35+
<td style="width:95px;">线程类型</td>
36+
<td style="text-align:center;">优点</td>
37+
<td style="text-align:center;">缺点</td>
38+
</tr>
39+
</thead>
40+
<tbody>
41+
<tr>
42+
<td style="vertical-align:middle;">内核线程</td>
43+
<td style="vertical-align:middle;">
44+
一个内核线程其实就是一个进程. 因此即使一个内核线程被阻塞了,其他的内核线程仍然可以运行。<br/>
45+
内核线程可以在不同的 CPU 上运行。
46+
</td>
47+
<td style="vertical-align:middle;">
48+
创建线程和线程间同步的消耗太大<br/>
49+
实现依赖于平台
50+
</td>
51+
</tr>
52+
<tr>
53+
<td style="vertical-align:middle;">用户线程</td>
54+
<td style="vertical-align:middle;">
55+
用户线程的创建和线程间同步的开销较少<br/>
56+
用户线程是平台无关的。
57+
</td>
58+
<td style="vertical-align:middle;">
59+
同一进程中的所有用户线程都对应一个内核线程. 因此,若该内核线程被阻塞,则所有相应的用户线程都会被阻塞。<br/>
60+
不同用户线程无法运行在不同CPU上
61+
</td>
62+
</tr>
63+
</tbody>
64+
</table>
65+
66+
## 线程的状态
67+
68+
线程的生命周期有五种可能的状态。它们如下:
69+
70+
- **新建**(Creation): 该过程的主要动作就是创建一个新线程, 创建完新线程后,该线程被发送到待执行的线程队列中。
71+
- **运行**(Execution): 该状态下,线程获取到并消耗CPU资源。
72+
- **就绪**(Ready): 该状态下,线程在待执行的线程队列中排队,等待被执行
73+
- **阻塞**(Blocked): 该状态下,线程由于等待某个事件(例如I/O操作)的出现而被阻塞. 这时线程并不使用CPU。
74+
- **死亡**(Concluded): 该状态下,线程释放执行时使用的资源并结束整个线程的生命周期。
75+
76+
## 是使用threading模块还是_thread模块
77+
78+
Python提供了两个模块来实现基于系统的线程: **`_thread`模块**(该模块提供了使用线程相关的较低层的API; 它的文档可以在<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`是最明智的选择。
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# 使用多线程解决斐波那契序列多输入问题
2+
3+
现在是时候实现了。任务是在给定多个输入值时并行执行斐波那契数列的各项。 出于教学目的,我们将固定四个元素中的输入值和四个线程来处理每个元素,模拟 worker 和要执行的任务之间的完美对称。 该算法将按如下方式工作:
4+
5+
1. 首先使用一个列表存储四个待输入值,这些值将被放入对于线程来说互相锁定的数据结构。
6+
2. 输入值被放入可被锁定的数据结构之后,负责处理斐波那契序列的线程将被告知可以被执行。这时,我们可以使用python线程的同步机制`Condition`模块(`Condition`模块对共享变量提供线程之间的同步操作),模块详情请参考:<http://docs.python.org/3/library/threading.html#threading.Condition>{target="_blank"}。
7+
3. 当每个线程结束斐波那契序列的计算后,分别把结果存入一个字典。
8+
9+
接下来我们将列出代码,并且讲述其中有趣的地方:
10+
11+
代码开始处我们加入了对编码额外的支持,导入`logging`, `threading``queue`模块。此外,我们还定义了我们例子中用到主要数据结构。
12+
一个字典,被命名为`fibo_dict`,将用来存储输入输出数据,输入数据为`key`,计算结果(输出数据)为值。
13+
我们同样定义了一个队列对象,该对象中存储线程间的共享数据(包括读写)。
14+
我们把该对象命名为`shared_queue`。最后我们定义一个列表模拟程序的四个输入值。代码如下:
15+
16+
```python
17+
#coding: utf-8
18+
19+
import logging, threading
20+
21+
from queue import Queue
22+
23+
logger = logging.getLogger()
24+
logger.setLevel(logging.DEBUG)
25+
formatter = logging.Formatter('%(asctime)s - %(message)s')
26+
27+
ch = logging.StreamHandler()
28+
ch.setLevel(logging.DEBUG)
29+
ch.setFormatter(formatter)
30+
logger.addHandler(ch)
31+
32+
fibo_dict = {}
33+
shared_queue = Queue()
34+
input_list = [3, 10, 5, 7]
35+
```
36+
37+
!!! info "下载示例代码"
38+
39+
您可以从<http://www.packtpub.com>{target="_blank"}上的帐户下载您购买的所有 Packt 书籍的示例代码文件。 如果您在其他地方购买了本书,您可以访问 <http://www.packtpub.com/support>{target="_blank"} 并注册以便将文件直接通过电子邮件发送给您。
40+
41+
在下面的代码行中,我们将从名为 `Condition` 的线程模块中定义一个对象。 该对象旨在根据特定条件同步对资源的访问。
42+
43+
```python
44+
queue_condition = threading.Condition()
45+
```
46+
47+
使用 Condition 对象的用于控制队列的创建和在其中进行条件处理。
48+
49+
下一段代码是定义将由多个线程执行的函数。我们将称它为 `fibonacci_task`。`fibonacci_task`函数接收`condition`对象作为参数,它将控制`fibonacci_task`对`share_queue`的访问。在这个函数中,我们使用了`with`语句(关于`with`语句的更多信息,请参考<http://docs.python.org/3/reference/compound_stmts.html#with>)来简化内容的管理。如果没有`with`语句,我们将不得不明确地获取锁并释放它。有了`with`语句,我们可以在开始时获取锁,并在内部块的退出时释放它。`fibonacci_task`函数的下一步是进行逻辑评估,告诉当前线程:"虽然`shared_queue`是空的,但要等待。" 这就是`condition`对象的`wait()`方法的主要用途。线程将等待,直到它得到通知说`shared_queue`可以自由处理。一旦我们的条件得到满足,当前线程将在`shared_queue`中获得一个元素,它马上计算斐波那契数列的值,并在`fibo_dict`字典中生成一个条目。最后,我们调用`task_done()`方法,目的是告知某个队列的任务已经被提取并执行。代码如下:
50+
51+
```python
52+
53+
def fibonacci_task(condition):
54+
with condition:
55+
while shared_queue.empty():
56+
logger.info("[%s] - waiting for elements in queue.." % threading.current_thread().name)
57+
condition.wait()
58+
else:
59+
value = shared_queue.get()
60+
a, b = 0, 1
61+
for item in range(value):
62+
a, b = b, a + b
63+
fibo_dict[value] = a
64+
shared_queue.task_done()
65+
logger.debug("[%s] fibonacci of key [%d] with result [%d]" % (threading.current_thread().name, value, fibo_dict[value]))
66+
```
67+
68+
我们定义的第二个函数是`queue_task`函数,它将由负责为`shared_queue`填充要处理的元素的线程执行。我们可以注意到获取`condition`作为访问`shared_queue`的一个参数。对于`input_list`中的每一个项目,线程都会将它们插入`shared_queue`中。
69+
70+
将所有元素插入 `shared_queue` 后,该函数通知负责计算斐波那契数列的线程队列已准备好使用。 这是通过使用 `condition.notifyAll()` 完成的,如下所示:
71+
72+
```python
73+
def queue_task(condition):
74+
logging.debug('Starting queue_task...')
75+
with condition:
76+
for item in input_list:
77+
shared_queue.put(item)
78+
logging.debug("Notifying fibonacci_task threadsthat the queue is ready to consume..")
79+
condition.notifyAll() # python3.10 中使用 notify_all()
80+
```
81+
82+
在下一段代码中,我们创建了一组包含四个线程的集合,它们将等待来自 `shared_queue` 的准备条件。 然后我们强调了线程类的构造函数,它允许我们定义函数。该线程将使用目标参数执行,该函数在`args`中接收的参数如下:
83+
84+
```python
85+
threads = [
86+
threading.Thread(daemon=True, target=fibonacci_task,args=(queue_condition,))
87+
for i in range(4)
88+
]
89+
```
90+
91+
接着我们使用`thread`对象的`start`方法开始线程:
92+
93+
```python
94+
[thread.start() for thread in threads]
95+
```
96+
97+
然后我们创建一个线程处理`shared_queue`,然后执行该线程。代码如下:
98+
99+
```python
100+
prod = threading.Thread(name='queue_task_thread', daemon=True, target=queue_task, args=(queue_condition,))
101+
prod.start()
102+
```
103+
104+
最后,我们对所有计算斐波那契数列的线程调用了`join()`方法。这个调用的目的是让让主线程等待子线程的调用,直到所有子线程执行完毕之后才结束主线程。请参考下面的代码:
105+
106+
```python
107+
[thread.join() for thread in threads]
108+
```
109+
110+
程序的执行结果如下:
111+
112+
```shell
113+
$ python temp.py
114+
2023-03-01 12:19:26,873 - [Thread-1 (fibonacci_task)] - waiting for elements in queue..
115+
2023-03-01 12:19:26,873 - [Thread-2 (fibonacci_task)] - waiting for elements in queue..
116+
2023-03-01 12:19:26,874 - [Thread-3 (fibonacci_task)] - waiting for elements in queue..
117+
2023-03-01 12:19:26,874 - [Thread-4 (fibonacci_task)] - waiting for elements in queue..
118+
2023-03-01 12:19:26,874 - Starting queue_task...
119+
2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume..
120+
2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume..
121+
2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume..
122+
2023-03-01 12:19:26,874 - Notifying fibonacci_task threadsthat the queue is ready to consume..
123+
2023-03-01 12:19:26,875 - [Thread-1 (fibonacci_task)] fibonacci of key [3] with result [2]
124+
2023-03-01 12:19:26,875 - [Thread-2 (fibonacci_task)] fibonacci of key [10] with result [55]
125+
2023-03-01 12:19:26,875 - [Thread-4 (fibonacci_task)] fibonacci of key [5] with result [5]
126+
2023-03-01 12:19:26,875 - [Thread-3 (fibonacci_task)] fibonacci of key [7] with result [13]
127+
```
128+
129+
请注意,首先创建并初始化 `fibonacci_task` 线程,然后它们进入等待状态。 同时,创建 `queue_task` 并填充 `shared_queue`。 最后,`queue_task` 通知 `fibonacci_task` 线程它们可以执行它们的任务。
130+
131+
请注意,`fibonacci_task` 线程的执行不遵循顺序逻辑,每次执行的顺序可能不同。 这就是使用线程的一个特点:**非确定性**(non-determinism)。

docs/chapter4/什么是线程.md

Lines changed: 0 additions & 62 deletions
This file was deleted.

0 commit comments

Comments
 (0)