首页 > 解决方案 > 如果调用 tkwait / vwait,则 Tcl_DoOneEvent 被阻塞

问题描述

有一个从 Tcl/Tk 调用的外部 C++ 函数并在相当长的时间内完成了一些事情。Tcl 调用者必须得到该函数的结果,所以它一直等到它完成。为了避免阻塞 GUI,该 C++ 函数在其主体中实现了某种事件循环:

while (m_curSyncProc.isRunning()) {
    const clock_t tm = clock();
    while (Tcl_DoOneEvent(TCL_ALL_EVENTS | TCL_DONT_WAIT) > 0) {}  // <- stuck here in case of tkwait/vwait
    // Pause for 10 ms to avoid 100% CPU usage
    if (double(clock() - tm) / CLOCKS_PER_SEC < 0.005) {
        nanosleep(10000);
    }
}

tkwait除非/vwait在 Tcl 代码中起作用,否则一切都很好。

例如,对于对话框,tkwait variable someVariable用于等待Ok/Close/<whatever>按钮被按下。我看到即使是标准 Tkbgerror也使用相同的方法(它使用 vwait)。

问题是,当 Tcl 代码在 tkwait/vwait 行中等待时,一旦调用Tcl_DoOneEvent就不会返回,否则它运行良好。是否可以在不完全重新设计 C++ 代码的情况下在该事件循环中修复它?因为该代码相当陈旧且复杂,并且其作者不再可访问。

标签: tcl

解决方案


谨防!这是一个复杂的话题!

Tcl_DoOneEvent()调用本质上是什么vwaittkwait并且update是围绕的薄包装器(传递不同的标志并设置不同的回调)。对它们中的任何一个的嵌套调用都会创建嵌套事件循环;除非您非常小心,否则您并不真正想要那些。事件循环仅在它不处理任何活动的事件回调时终止,并且如果这些事件回调创建内部事件循环,则外部事件循环将在内部事件完成之前根本不会做任何事情。

当您控制外部事件循环时(以一种非常低效的方式,但是很好),您真的希望内部事件循环根本不运行。有三种可能的方法来处理这个问题;我怀疑第三个(协程)最适合您,而第一个是您真正想要避免的,但这绝对是您的决定。

1. 续传

您可以将内部代码重写为持续传递样式——一大堆程序通过状态机/工作流一步一步地传递——这样它就不会真正调用vwait(和朋友)。该家族中唯一倾向于隐约安全的是update idletasks(实际上只是Tcl_DoOneEvent(TCL_IDLE_EVENTS | TCL_DONT_WAIT))处理 Tk 内部生成的更改。

在 Tcl 8.5 之前,此选项是您的主要选择,而且工作量很大。

2. 线程

您可以移动到多线程应用程序。这可能很容易……也可能非常困难;细节取决于你在整个申请过程中所做的检查。

如果走这条路,请记住 Tcl 解释器和 Tcl 值完全是线程绑定的;他们在内部使用特定于线程的数据,这样他们就可以避免大的全局锁。这意味着 Tcl 中的线程设置起来相对昂贵,但之后实际上可以非常有效地使用多个 CPU;线程池是一种非常常见的方法。

3. 协程

从 8.6 开始,您可以将内部代码放在协程中。默认情况下,8.6 中的几乎所有内容都是协同程序感知的(在我们的内部术语中为“非递归”)(包括您通常不会想到的命令sourcevwait例如从 Tcllib协程包中,事情通常会“正常工作”。(例如,vwait var变成coroutine::vwait varafter 123变成coroutine::after 123。)

唯一没有直接替换的东西是tkwait windowand tkwait visibility; 你需要模拟那些等待一个<Destroy><Visibility>事件(后者不常见,因为它在某些平台上不受支持),你可以通过bind对那些只设置一个你可以使用的变量coroutine::vwait(本质上是无论如何,所有tkwait内部操作)。

在某些情况下,协程可能会变得混乱,例如当您的 C 代码不支持协程时。这些在 Tcl 中发挥作用的主要地方是trace回调、解释器间调用和通道的脚本实现;问题在于,这些位于后面的内部 API 已经相当复杂(尤其是通道),并且没有人愿意涉足并启用非递归实现。


推荐阅读