原文地址:http://blog.chinaunix.net/uid-200142-id-4047292.html
如果你经常使用python开发GUI程序的话,那么就知道,有时你需要很长时间来执行一个任务。当然,如果你使用命令行程序来做的话,你回非常惊讶。大部分情况下,这会堵塞GUI的事件循环,用户会看到程序卡死。如何才能避免这种情况呢?当然是利用线程或进程了!本文,我们将探索如何使用wxPython和theading模块来实现。
wxpython线程安全方法
wxPython中,有三个“线程安全”的函数。如果你在更新UI界面时,三个函数都不使用,那么你可能会遇到奇怪的问题。有时GUI也忙运行挺正常,有时却会无缘无故的崩溃。因此就需要这三个线程安全的函数:wx.PostEvent, wx.CallAfter和wx.CallLater。据Robin Dunn(wxPython作者)描述,wx.CallAfter使用了wx.PostEvent来给应用程序对象发生事件。应用程序会有个事件处理程序绑定到事件上,并在收到事件后,执行处理程序来做出反应。我认为wx.CallLater是在特定时间后调用了wx.CallAfter函数,已实现规定时间后发送事件。
Robin Dunn还指出Python全局解释锁 (GIL)也会避免多线程同时执行python字节码,这会限制程序使用CPU内核的数量。另外,他还说,“wxPython发布GIL是为了在调用wx API时,其他线程也可以运行”。换句话说,在多核机器上使用多线程,可能效果会不同。
总之,大概的意思是桑wx函数中,wx.CallLater是最抽象的线程安全函数, wx.CallAfter次之,wx.PostEvent是最低级的。下面的实例,演示了如何使用wx.CallAfter和wx.PostEvent函数来更新wxPython程序。
wxPython, Theading, wx.CallAfter and PubSub
wxPython邮件列表中,有些专家会告诉其他人使用wx.CallAfter,并利用PubSub实现wxPython应用程序与其他线程进行通讯,我也赞成。如下代码是具体实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
|
import
time
import
wx
from
threading
import
Thread
from
wx.lib.pubsub
import
Publisher
########################################################################
class
TestThread(Thread):
"""Test Worker Thread Class."""
#----------------------------------------------------------------------
def
__init__(
self
):
"""Init Worker Thread Class."""
Thread.__init__(
self
)
self
.start()
# start the thread
#----------------------------------------------------------------------
def
run(
self
):
"""Run Worker Thread."""
# This is the code executing in the new thread.
for
i
in
range
(
6
):
time.sleep(
10
)
wx.CallAfter(
self
.postTime, i)
time.sleep(
5
)
wx.CallAfter(Publisher().sendMessage,
"update"
,
"Thread finished!"
)
#----------------------------------------------------------------------
def
postTime(
self
, amt):
"""
Send time to GUI
"""
amtOfTime
=
(amt
+
1
)
*
10
Publisher().sendMessage(
"update"
, amtOfTime)
########################################################################
class
MyForm(wx.Frame):
#----------------------------------------------------------------------
def
__init__(
self
):
wx.Frame.__init__(
self
,
None
, wx.ID_ANY,
"Tutorial"
)
# Add a panel so it looks the correct on all platforms
panel
=
wx.Panel(
self
, wx.ID_ANY)
self
.displayLbl
=
wx.StaticText(panel, label
=
"Amount of time since thread started goes here"
)
self
.btn
=
btn
=
wx.Button(panel, label
=
"Start Thread"
)
btn.Bind(wx.EVT_BUTTON,
self
.onButton)
sizer
=
wx.BoxSizer(wx.VERTICAL)
sizer.Add(
self
.displayLbl,
0
, wx.
ALL
|wx.CENTER,
5
)
sizer.Add(btn,
0
, wx.
ALL
|wx.CENTER,
5
)
panel.SetSizer(sizer)
# create a pubsub receiver
Publisher().subscribe(
self
.updateDisplay,
"update"
)
#----------------------------------------------------------------------
def
onButton(
self
, event):
"""
Runs the thread
"""
TestThread()
self
.displayLbl.SetLabel(
"Thread started!"
)
btn
=
event.GetEventObject()
btn.Disable()
#----------------------------------------------------------------------
def
updateDisplay(
self
, msg):
"""
Receives data from thread and updates the display
"""
t
=
msg.data
if
isinstance
(t,
int
):
self
.displayLbl.SetLabel(
"Time since thread started: %s seconds"
%
t)
else
:
self
.displayLbl.SetLabel(
"%s"
%
t)
self
.btn.Enable()
#----------------------------------------------------------------------
# Run the program
if
__name__
=
=
"__main__"
:
app
=
wx.PySimpleApp()
frame
=
MyForm().Show()
app.MainLoop()
|
我们会用time模块来模拟耗时过程,请随意将自己的代码来代替,而在实际项目中,我用来打开Adobe Reader,并将其发送给打印机。这并没什么特别的,但我不用线程的话,应用程序中的打印按钮就会在文档发送过程中卡住,UI界面也会被挂起,直到文档发送完毕。即使一秒,两秒对用户来说都有卡的感觉。
总之,让我们来看看是如何工作的。在我们编写的Thread类中,我们重写了run方法。该线程在被实例化时即被启动,因为我们在__init__方法中有“self.start”代码。run方法中,我们循环6次,每次sheep10秒,然后使用wx.CallAfter和PubSub更新UI界面。循环结束后,我们发送结束消息给应用程序,通知用户。
你会注意到,在我们的代码中,我们是在按钮的事件处理程序中启动的线程。我们还禁用按钮,这样就不能开启多余的线程来。如果我们让一堆线程跑的话,UI界面就会随机的显示“已完成”,而实际却没有完成,这就会产生混乱。对用户来说是一个考验,你可以显示线程PID,来区分线程,你可能要在可以滚动的文本控件中输出信息,这样你就能看到各线程的动向。
最后可能就是PubSub接收器和事件的处理程序了:
1
2
3
4
5
6
7
8
9
10
|
def
updateDisplay(
self
, msg):
"""
Receives data from thread and updates the display
"""
t
=
msg.data
if
isinstance
(t,
int
):
self
.displayLbl.SetLabel(
"Time since thread started: %s seconds"
%
t)
else
:
self
.displayLbl.SetLabel(
"%s"
%
t)
self
.btn.Enable()
|
看我们如何从线程中提取消息,并用来更新界面?我们还使用接受到数据的类型来告诉我们什么显示给了用户。很酷吧?现在,我们玩点相对低级一点点,看wx.PostEvent是如何办的。
wx.PostEvent与线程
下面的代码是基于wxPython wiki编写的,这看起来比wx.CallAfter稍微复杂一下,但我相信我们能理解。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
import
time
import
wx
from
threading
import
Thread
# Define notification event for thread completion
EVT_RESULT_ID
=
wx.NewId()
def
EVT_RESULT(win, func):
"""Define Result Event."""
win.Connect(
-
1
,
-
1
, EVT_RESULT_ID, func)
class
ResultEvent(wx.PyEvent):
"""Simple event to carry arbitrary result data."""
def
__init__(
self
, data):
"""Init Result Event."""
wx.PyEvent.__init__(
self
)
self
.SetEventType(EVT_RESULT_ID)
self
.data
=
data
########################################################################
class
TestThread(Thread):
"""Test Worker Thread Class."""
#----------------------------------------------------------------------
def
__init__(
self
, wxObject):
"""Init Worker Thread Class."""
Thread.__init__(
self
)
self
.wxObject
=
wxObject
self
.start()
# start the thread
#----------------------------------------------------------------------
def
run(
self
):
"""Run Worker Thread."""
# This is the code executing in the new thread.
for
i
in
range
(
6
):
time.sleep(
10
)
amtOfTime
=
(i
+
1
)
*
10
wx.PostEvent(
self
.wxObject, ResultEvent(amtOfTime))
time.sleep(
5
)
wx.PostEvent(
self
.wxObject, ResultEvent(
"Thread finished!"
))
########################################################################
class
MyForm(wx.Frame):
#----------------------------------------------------------------------
def
__init__(
self
):
wx.Frame.__init__(
self
,
None
, wx.ID_ANY,
"Tutorial"
)
# Add a panel so it looks the correct on all platforms
panel
=
wx.Panel(
self
, wx.ID_ANY)
self
.displayLbl
=
wx.StaticText(panel, label
=
"Amount of time since thread started goes here"
)
self
.btn
=
btn
=
wx.Button(panel, label
=
"Start Thread"
)
btn.Bind(wx.EVT_BUTTON,
self
.onButton)
sizer
=
wx.BoxSizer(wx.VERTICAL)
sizer.Add(
self
.displayLbl,
0
, wx.
ALL
|wx.CENTER,
5
)
sizer.Add(btn,
0
, wx.
ALL
|wx.CENTER,
5
)
panel.SetSizer(sizer)
# Set up event handler for any worker thread results
EVT_RESULT(
self
,
self
.updateDisplay)
#----------------------------------------------------------------------
def
onButton(
self
, event):
"""
Runs the thread
"""
TestThread(
self
)
self
.displayLbl.SetLabel(
"Thread started!"
)
btn
=
event.GetEventObject()
btn.Disable()
#----------------------------------------------------------------------
def
updateDisplay(
self
, msg):
"""
Receives data from thread and updates the display
"""
t
=
msg.data
if
isinstance
(t,
int
):
self
.displayLbl.SetLabel(
"Time since thread started: %s seconds"
%
t)
else
:
self
.displayLbl.SetLabel(
"%s"
%
t)
self
.btn.Enable()
#----------------------------------------------------------------------
# Run the program
if
__name__
=
=
"__main__"
:
app
=
wx.PySimpleApp()
frame
=
MyForm().Show()
app.MainLoop()
|
让我们先稍微放一放,对我来说,最困扰的事情是第一块:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
# Define notification event for thread completion
EVT_RESULT_ID
=
wx.NewId()
def
EVT_RESULT(win, func):
"""Define Result Event."""
win.Connect(
-
1
,
-
1
, EVT_RESULT_ID, func)
class
ResultEvent(wx.PyEvent):
"""Simple event to carry arbitrary result data."""
def
__init__(
self
, data):
"""Init Result Event."""
wx.PyEvent.__init__(
self
)
self
.SetEventType(EVT_RESULT_ID)
self
.data
=
data
|
EVT_RESULT_ID只是一个标识,它将线程与wx.PyEvent和“EVT_RESULT”函数关联起来,在wxPython代码中,我们将事件处理函数与EVT_RESULT进行捆绑,这就可以在线程中使用wx.PostEvent来将事件发送给自定义的ResultEvent了。
结束语
希望你已经明白在wxPython中基本的多线程技巧。还有其他多种多线程方法这里就不在涉及,如wx.Yield和Queues。幸好有wxPython wiki,它涵盖了这些话题,因此如果你有兴趣可以访问wiki的主页,查看这些方法的使用。