起因
Perl-Tk
在windows下的表现着实让我头疼了很长一段时间,最无法忍受的一点是缺乏稳定的线程支持。比如,无法在一个界面上一边录制新闻,一边完成下一个录制时间的设定,整个程序被阻塞在那儿无法动弹。某些情况下可以使用非阻塞的本地socket来实现进程间的通信,但这么做很费劲,而且使得程序像一个奇怪的网络服务器。POE作为在单进程中实现并行的框架,提供了对Tk的支持,很好地解决了组件间通信的问题。
大致的介绍
#
!/usr/bin/perl
use
warnings;
use
strict;
use
Tk;
use
POE;
POE
::
Session
->
create(
inline_states
=>
{
_start
=>
\&
ui_start
,
ev_count
=>
\&
ui_count
,
ev_clear
=>
\&
ui_clear
,
}
);
$poe_kernel
->
run();
exit
0
;
sub
ui_start {
my
(
$kernel
,
$session
,
$heap
)
=
@_
[ KERNEL
,
SESSION
,
HEAP ];
$poe_main_window
->
Label(
-
text
=>
"
Counter
"
)
->
pack
;
$poe_main_window
->
Label(
-
textvariable
=>
\
$heap
->
{count} )
->
pack
;
$poe_main_window
->
Button(
-
text
=>
"
Clear
"
,
-
command
=>
$session
->
postback(
"
ev_clear
"
)
,
)
->
pack
;
$kernel
->
yield(
"
ev_count
"
);
}
sub
ui_count {
$_
[HEAP]
->
{count}
++
;
$_
[KERNEL]
->
delay(
"
ev_count
"
=>
.
5
);
#
$_[KERNEL]->yield("ev_count");
}
sub
ui_clear {
$_
[HEAP]
->
{count}
=
0
;
}
很容易可以看出来一个不一样的地方,比如说用$poe_kernel代替了一贯的POE::Kernel,但在实际运行中会发现$poe_kernel是对POE:;Kernel的引用,最终起作用的依然是POE::Kernel。好了,除了这一点外,其他还是与传统的POE程序差不多。
首先建立了一个包含了内置_start和两个自定义(ev_count,ev_clear)事件的session。之后在_start事件中,在已初始化了的$poe_main_window上建立一个内容为“Counter”的标签,内容为变量$_[HEAP]->{count}的标签和一个用来清零的按钮。至于ev_count和ev_clear事件不过是自增和清零$_[HEAP]->{count}而已,其中ev_count事件会递归回调自身以实现不断自增的过程。
如果一切顺利,运行之后的结果如下图所示:
单击Clear按钮,中间的数字将从零开始继续飞速自增。如果想让它自增得慢一点,或者说可以看得清,可以在ev_count事件中delay自身回调一小段时间。需要注意的一点是不要指望用sleep,因为它会让整个POE崩溃。
一些细节
下面来讲一下隐藏在这一切背后的一些小细节。事实上,这些细节危险重重,任何细小的错误哪怕是位置的改变都可能导致程序的失败,而原文中的某些注释也比较晦涩或者是存在误导,需要详细说明一下。
$poe_main_window
在strict环境中出现,这让我很吃惊。一般情况下,这样的非my变量是无法通过检测的。唯一的解释是use POE在背后干了很多事情,多得连POE::Kernel都增加了叫一个$poe_kernel的引用,当然这是为了保证POE::Kernel不被自动垃圾回收。在这里不得不说的是,use Tk必须出现在use POE之前,否则use POE不会初始化生成$poe_main_window。所以可以这么认为,use POE会通过检测其之前的模块调用来决定之后的框架行为,这点在其他模块的应用中不是很多见,至少在我是第一次看见。
原文中有这么一句话“Widgets we need to work with later, such as the counter display,must be stored somewhere.The heap is a convenient place for them.”意思是说“要在之后用到的组件,比如说显示自增计数的标签,必须存储在某个地方,而堆栈就是一个方便的场所。”这也让我感到很困惑,至少在这个例子中,之后没有显式地用到这个计数显示标签。如果非要说用到,那么就是ev_count事件中的$_[HEAP]->{counter}以textvariable的身份影响到其宿主,但事实上这非常隐晦,而且为此保存一个引用似乎也毫无意义。后来的实验证明了我的猜测,在注释掉这个引用之后程序依然可以正常运行,也就是说原文这句话并非针对于本例子或者说毫不相干,其中的“such as”极易引人误入歧途。它真正想说的是,为了可以在某个事件中改变组件的行为,需要一个全局可见的引用,而$_[HEAP]正是恰当的选择,其实用一个全局哈希表就可以得到相同的效果,只是这么多就显得多余了。
最后的一点是有关于POE本身的机制。在ev_count中为了实现不断自增,在其事件处理句柄中有一句自身回调,而在ev_clear中仅仅是将计数清零了而已。一开始我有些担心自增行为会中断,因此在ev_clear中增加了一句触发ev_count事件的命令,但这么做是多余的。首先POE中的每一个事件句柄都是并行的,因此ev_clear不会中断ev_count的行为;第二,作为两个事件之间关联的全局变量$_[HEAP]->{counter},POE为其提供了锁保护,不必担心其中一个事件的行为导致另外一个崩溃。
几句话
因为不是正儿八经地写论文,所以遣词造句就显得随性了一些。倒不是说我可以不认真了,相反地,对于一些细节倒是表现得更自然。
这个例子非常简单,简单得几乎没有实用价值,但是说明了问题。我认为一个好的tutorial就应该这样,简单而能说明问题。简单就是说容易让人看懂,说明问题则是把注意事项讲清楚了,之后全凭各人兴趣和悟性了。POE Cookbook是一本非官方的技术文集,没有出版物,甚至说如果你写了一个好的例子,也可以被收录其中。但是每个例子都很通俗易懂,从最简单的入手,由浅入深,最后形成一个系统。反观国内的一些技术出版物,往往枯燥理论先讲一大堆,吓跑一批人;再举一两个可以堪称作者代表作的复杂例子,又闷掉一批人,结果是弄得是曲高和寡。