Skip to content

Commit 7a438b1

Browse files
committed
add translated for 'Freeze your Python with str.encode and threads'
1 parent 5f9b6c1 commit 7a438b1

4 files changed

Lines changed: 348 additions & 337 deletions

File tree

Others/README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,8 @@
9292

9393
- [实用Python:EAFP VS. LBYL](./实用Python:EAFP VS. LBYL.md)
9494

95-
你在本文中应该获得的关键是,EAFP在Python中是一种合法的编码风格,并且当其有意义时,你应该随意使用它。
95+
你在本文中应该获得的关键是,EAFP在Python中是一种合法的编码风格,并且当其有意义时,你应该随意使用它。
96+
97+
- [使用str.encode和threads冻结你的Python](./使用str.encode和threads冻结你的Python.md)
98+
99+
导入锁与线程的爱恨情仇,又一个使用Python 3的理由。
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
原文:[Freeze your Python with str.encode and threads](https://blog.sqreen.io/freeze-python-str-encode-threads/)
2+
3+
---
4+
5+
[![](https://blog.sqreen.io/wp-content/uploads/2016/07/frozen-python-2040.png)](https://blog.sqreen.io/wp-content/uploads/2016/07/frozen-python-2040.png "frozen-python-2040")
6+
7+
当我在[sqreen.io Python agent](https://www.sqreen.io/?utm_medium=social&utm_source=blog&utm_campaign=Freeze%20your%20Python%20with%20str.encode%20and%20threads)工作的时候,我发现了一个相当讨厌,但是分析起来却挺有意思的bug,它让我深入到Python内部。
8+
9+
### 这个讨厌的bug
10+
11+
我正在写一段代码,它产生一个线程,检索一些JSON,然后存储以用于后续使用。基本上,代码看起来是这样的:
12+
13+
```python
14+
from threading import Thread, Event
15+
16+
class MyThread(Thread):
17+
18+
def __init__(self):
19+
self.event = Event()
20+
21+
def run(self):
22+
data = self.retrieve_json()
23+
self.data = data
24+
self.event.set()
25+
print("Data retrieval done!")
26+
while True:
27+
pass
28+
29+
thread = MyThread()
30+
thread.start()
31+
timeout = thread.event.wait(5)
32+
33+
if timeout is not None:
34+
print("JSON retrieval was too slow")
35+
```
36+
37+
一切似乎都还好,并且当网络延迟过高的时候,错误信息会正确显示:
38+
39+
```sh
40+
$> python2 app.py
41+
Data retrieval done!
42+
```
43+
44+
接下来,我想将字典键和值从unicode转换成latin-1字符串:
45+
46+
```python
47+
class MyThread(Thread):
48+
49+
def run(self):
50+
data = self.retrieve_json()
51+
# Convert unicode strings into latin1 strings because of WSGI
52+
self.data = {key.encode('latin1'): value.encode('latin1') for (key, value) in data.keys()}
53+
self.event.set()
54+
print("Data retrieval done!")
55+
while True:
56+
pass
57+
```
58+
59+
我敢肯定,在接收JSON时,你们都做过这种类型的转换,不是吗?
60+
61+
添加这个代码后,无论我提供了什么样子的值,错误信息开始显示超时,从5s到1小时。而更诡异的是,`"Data retrieval done!"`信息总是在错误信息之后打印出来:
62+
63+
```sh
64+
$> python2 app.py
65+
JSON retrieval was too slow
66+
Data retrieval done!
67+
```
68+
69+
然而,当我在Python 3上跑的时候,一切如我所预期:
70+
71+
```sh
72+
$> python3 app.py
73+
Data retrieval done!
74+
```
75+
76+
一些非常奇怪的事情一直发生,而我决定调查一下。
77+
78+
### 隔离
79+
80+
我试图找出问题所在,于是有了这个最小化的再现脚本,让我们把它命名为`"str_encode_thread.py"`
81+
82+
```python
83+
import time
84+
import threading
85+
86+
87+
def to_latin_1(event, value):
88+
print(time.time(), "Before encode")
89+
value.encode('latin-1')
90+
print(time.time(), "After encode")
91+
event.set()
92+
print(time.time(), "Event set")
93+
94+
event = threading.Event()
95+
thread = threading.Thread(target=to_latin_1, args=(event, u'42'))
96+
thread.start()
97+
98+
event.wait(10)
99+
```
100+
101+
如果你用Python 3启动这个脚本,将会看到:
102+
103+
```sh
104+
$> python3 str_encode_thread.py
105+
1467046041.643346 Before encode
106+
1467046041.643446 After encode
107+
1467046041.643483 Event set
108+
```
109+
110+
而用Python 2,你会看到:
111+
112+
```sh
113+
$> python2 str_encode_thread.py
114+
(1467046099.495261, 'Before encode')
115+
(1467046099.495937, 'After encode')
116+
(1467046099.495961, 'Event set')
117+
```
118+
119+
目前一切OK。而当我们尝试导入这个脚本时,好玩的事情来了。如果你用Python 3导入这个脚本,那么一切仍然是正常的:
120+
121+
```sh
122+
$> time python3 -c "import str_encode_thread"
123+
1467046239.921475 Before encode
124+
1467046239.921578 After encode
125+
1467046239.921602 Event set
126+
python3 -c "import str_encode_thread" 0,05s user 0,01s system 84% cpu 0,081 total
127+
```
128+
129+
而用Python 2导入时,我们讨厌的朋友再次出现了:
130+
131+
```sh
132+
$> time python2 -c "import str_encode_thread"
133+
(1467046276.826755, 'Before encode')
134+
(1467046286.830485, 'After encode') # <- 10 seconds (╯°□°)╯︵ ┻━┻
135+
(1467046286.830519, 'Event set')
136+
python2 -c "import str_encode_thread" 0,03s user 0,03s system 0% cpu 10,109 total
137+
```
138+
139+
如果你仔细看看`"After encode"`消息,可以看到,它在`"Before encode"`消息打印10s之后才打印,这意味着线程莫名其妙被冻结了。
140+
141+
### 掉进兔子洞,不知如何是好
142+
143+
为嘛一个简单的`"value.encode('latin1', 'encode')"`会阻塞一个线程呢?
144+
145+
幸好,有一个[非常棒的名为faulthandler的包](https://pypi.python.org/pypi/faulthandler/2.4),它在这种情况下非常有用。它可以在unix信号、用户信号或者python错误上展示回溯。从Python 3.3开始,它就是标准库的一部分了,而对于老一点的Python版本,可以用pip进行安装。
146+
147+
本例中,我们将使用[dump_traceback_later](http://faulthandler.readthedocs.io/#dumping-the-tracebacks-after-a-timeout)来看看该线程在哪里阻塞。
148+
149+
带faulthandler的新脚本并没有什么不同 (不要忘了`"pip install faulthandler"`):
150+
151+
```python
152+
import time
153+
import threading
154+
import faulthandler
155+
156+
157+
faulthandler.dump_traceback_later(5)
158+
159+
160+
def to_latin_1(event, value):
161+
print(time.time(), "Before encode")
162+
value.encode('latin-1')
163+
print(time.time(), "After encode")
164+
event.set()
165+
print(time.time(), "Event set")
166+
167+
event = threading.Event()
168+
thread = threading.Thread(target=to_latin_1, args=(event, u'42'))
169+
thread.start()
170+
171+
event.wait(10)
172+
```
173+
174+
让我们运行新的脚本:
175+
176+
```sh
177+
time python2 -c "import str_encode_thread"
178+
(1468083195.13011, 'Before encode')
179+
Timeout (0:00:05)!
180+
Thread 0x0000700000401000 (most recent call first):
181+
File "/usr/lib/python2.7/encodings/__init__.py", line 100 in search_function
182+
File "str_encode_thread.py", line 11 in to_latin_1
183+
File "/usr/lib/python2.7/threading.py", line 754 in run
184+
File "/usr/lib/python2.7/threading.py", line 801 in __bootstrap_inner
185+
File "/usr/lib/python2.7/threading.py", line 774 in __bootstrap
186+
187+
Current thread 0x00007fff71258000 (most recent call first):
188+
File "/usr/lib/python2.7/threading.py", line 359 in wait
189+
File "/usr/lib/python2.7/threading.py", line 614 in wait
190+
File "str_encode_thread.py", line 20 in <module>
191+
File "<string>", line 1 in <module>
192+
(1468083205.133041, 'After encode')
193+
(1468083205.133106, 'Event set')
194+
python2 -c "import str_encode_thread" 0,03s user 0,02s system 0% cpu 10,043 total
195+
```
196+
197+
回溯中我们能看到什么?
198+
199+
主线程`"0x00007fff71258000"`在我们模块的第20行阻塞了,在那里是`"event.wait(10)"`,这完全正常。
200+
201+
其他线程`"0x0000700000401000"`正运行我们模块的第11行`"value.encode('latin-1')"`,并且调用内置模块`"encoding"`的函数`"search_function"`,它冻结了我们的线程。这个函数做了什么,为什么它是阻塞的?
202+
203+
[源代码](https://hg.python.org/cpython/file/v2.7.12rc1/Lib/encodings/__init__.py#l99)告诉我们,它试图导入一个以`"encodings."`开头的模块。
204+
205+
```python
206+
In [1]: import sys
207+
208+
In [2]: before_modules = sys.modules.keys()
209+
210+
In [3]: '42'.encode('latin-1')
211+
Out[3]: '42'
212+
213+
In [4]: after_modules = sys.modules.keys()
214+
215+
In [5]: print("Imported modules", set(after_modules) - set(before_modules))
216+
('Imported modules', set(['encodings.latin_1']))
217+
```
218+
219+
明白了!`"str.encode(X)"`试图导入模块`"encodings.X"`。所以修复应该是容易的:在主线程中,使用该函数之前导入该模块,这样就可以了,是吧?
220+
221+
```python
222+
import time
223+
import threading
224+
import encodings.latin_1
225+
226+
227+
def to_latin_1(event, value):
228+
print(time.time(), "Before encode")
229+
value.encode('latin-1')
230+
print(time.time(), "After encode")
231+
event.set()
232+
print(time.time(), "Event set")
233+
234+
event = threading.Event()
235+
thread = threading.Thread(target=to_latin_1, args=(event, u'42'))
236+
thread.start()
237+
238+
event.wait(10)
239+
```
240+
241+
让我们用python2再试试:
242+
243+
```sh
244+
$> time python2 -c "import str_encode_thread"
245+
(1467049098.995541, 'Before encode')
246+
(1467049109.000293, 'After encode')
247+
(1467049109.000324, 'Event set')
248+
python2 -c "import str_encode_thread" 0,03s user 0,02s system 0% cpu 10,054 total
249+
```
250+
251+
我们仍然有相同的问题!怎么了?
252+
253+
### 必须更深入
254+
255+
我们知道,`"str.encode"`试图导入一个可能冻结我们线程的模块。
256+
257+
在这一点上,我到我最喜欢的IRC频道 (#python-fr)寻求帮助,最终我们找到了原因!
258+
259+
`"import module"`做了什么?如果我们相信[Python文档](https://docs.python.org/2/library/imp.html#examples),那它基本上做了这些:
260+
261+
```python
262+
import imp
263+
import sys
264+
265+
def __import__(name, globals=None, locals=None, fromlist=None):
266+
# Fast path: see if the module has already been imported.
267+
try:
268+
return sys.modules[name]
269+
except KeyError:
270+
pass
271+
272+
# If any of the following calls raises an exception,
273+
# there's a problem we can't handle -- let the caller handle it.
274+
275+
fp, pathname, description = imp.find_module(name)
276+
277+
try:
278+
return imp.load_module(name, fp, pathname, description)
279+
finally:
280+
# Since we may exit via an exception, close fp explicitly.
281+
if fp:
282+
fp.close()
283+
```
284+
285+
没有啥特别的:如果已经导入了该模块,那么从`"sys.modules"`返回该模块,否则,尝试找到该模块的源文件,加载并返回。
286+
287+
这里也不应该阻塞,但若我们继续看`"imp"`模块文档,可以找到一个[非常有趣的名为`"lock_held"`的函数](https://docs.python.org/2/library/imp.html#imp.lock_held)
288+
289+
> 在多线程平台上,执行导入的线程持有一个内部锁,直到导入完成。这个锁阻塞了其他线程进行导入,直到初始导入完成,这反过来阻止其他线程看到由初始线程在完成导入(以及可能有的由此导入触发的其他导入)过程中构建的未完成的模块对象。
290+
291+
真相越来越近……
292+
293+
### 锁和线程,爱恨交加
294+
295+
当搜索`"python import lock"`,我们可以迅速找到关于[在线程代码中导入](https://docs.python.org/2/library/threading.html#importing-in-threaded-code)的章节,它只是告诉你这是一个可怕的想法,并有可能造成死锁。
296+
297+
让我们总结下我们的发现:
298+
299+
```python
300+
import imp
301+
import time
302+
import threading
303+
304+
305+
def to_latin_1(event, value):
306+
print(time.time(), "Before encode", threading.current_thread().name, imp.lock_held())
307+
value.encode('latin-1')
308+
print(time.time(), "After encode")
309+
event.set()
310+
print(time.time(), "Event set")
311+
312+
event = threading.Event()
313+
thread = threading.Thread(target=to_latin_1, args=(event, u'42'))
314+
thread.start()
315+
316+
event.wait(10)
317+
```
318+
319+
```sh
320+
$> python2 -c "import str_encode_thread"
321+
(1467050582.565935, 'Before encode', 'Thread-1', True)
322+
(1467050592.567373, 'After encode')
323+
(1467050592.567418, 'Event set')
324+
```
325+
326+
当我们导入我们的模块`"str_encode_thread"`时,导入锁由主线程获得,因此当我们的自定义线程试图执行`"value.encode('latin-1')"`时,stdlib试图导入`"encoding.latin_1"`模块,这会被阻塞,因为导入锁已经在主线程手里。然后,我们通过`"event.wait(10)"`阻塞了主线程,接着这两个线程都阻塞了10秒,太糟糕了……当等待超时,`"import str_encode_thread"`结束,释放导入锁,从而唤醒了我们的自定义线程。
327+
328+
如果我们没有设置一个最大化的事件超时,我们将永远让Python解释器死锁!
329+
330+
在处理线程时,并无神奇解决方案。如果你手动导入一个模块而无需使用锁,那么你可能不知道发生了什么,但是它只会通过抛出额外有趣的bug来“解决”死锁。
331+
332+
例如,当你调用`"time.strptime"`时,它试图以一种不安全的方式导入`"_strptime"`模块,这可能会引发一些难以调试的问题,像[这个boto问题](https://github.com/boto/boto/issues/1898)[对应的Python bug页面](http://bugs.python.org/issue7980)。这个问题可以通过在主线程之前`"import _strptime"`,这样线程就不会因为锁或者看到一个未完成的模块而阻塞,从而解决。
333+
334+
### **Python 3**才是未来
335+
336+
那为什么在Python 3就一切正常?除了打破你所有的导入名,强制你在代码中到处用`".encode"`和`".decode" (这是件好事!)之外,Python 3对导入锁进行了重构,而[现在每个模块都处理全局导入锁](https://mail.python.org/pipermail/python-dev/2013-August/127902.html)。现在,你不大可能触发这个问题了。
337+
338+
### 我们都应该使用**Python 3**
339+
340+
这可能是一个非常讨厌的问题,但是深入Python的一个鲜为人知的机制却是非常有趣的。
341+
342+
**TL;DR**: Python 2中,还有一个可重入锁,当该锁由其他线程持有时,如果你的线程试图进行导入,它将阻塞它们。Python 3中,该导入锁现在是每个模块持有的。

Python Weekly/Python Weekly Issue 252.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838
[我勒个去,怎样构建一个Django, Django REST Framework, Angular 1.1.x和Webpack项目?](http://gregblogs.com/how-the-do-i-build-a-django-django-rest-framework-angular-1-1-x-and-webpack-project/)
3939

40-
[使用str.encode和threads冻结你的Python](https://blog.sqreen.io/freeze-python-str-encode-threads/)
40+
[使用str.encode和threads冻结你的Python](https://blog.sqreen.io/freeze-python-str-encode-threads/) [中文版](../Others/使用str.encode和threads冻结你的Python.md)
4141

4242
[预览用于AWS的Python无服务器微框架](https://medium.com/@raiderrobert/how-to-make-a-webhook-receiver-in-django-1ce260f4efff)
4343

0 commit comments

Comments
 (0)