Tcl_DoOneEvent 如果调用 tkwait / vwait 则阻塞

Tcl_DoOneEvent is blocked if tkwait / vwait is called

有一个从 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> 按钮被按下。我看到即使是标准的 Tk bgerror 也使用相同的方法(它使用 vwait)。

问题是当 Tcl 代码在 tkwait/vwait 行等待时,一旦调用 Tcl_DoOneEvent 就不会 return,否则它运行良好。是否可以在不完全重新设计 C++ 代码的情况下在该事件循环中修复它?因为那段代码相当古老和复杂,而且它的作者已经无法访问了。

小心!这是一个复杂的话题!

Tcl_DoOneEvent() 调用本质上是 vwaittkwaitupdate 的薄包装(传递不同的标志并设置不同的回调)。对其中任何一个的嵌套调用都会创建嵌套事件循环;除非你非常小心,否则你不会真的想要那些。事件循环仅在未处理任何活动事件回调时终止,并且如果这些事件回调创建了内部事件循环,则外部事件循环将不会做任何事情,直到内部事件循环完成。

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

1。继续通过

您可以将内部代码重写为连续传递样式 — 一大堆通过状态一步一步递交的过程 machine/workflow — 这样它实际上就不会调用 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 中的几乎所有内容都是协程感知的(我们内部术语中的“非递归”)(包括您通常不会想到的命令,例如 source),一旦您完成了,您就可以将 vwait 调用替换为来自 Tcllib coroutine package 的等价物,事情通常会“正常工作”。 (例如,vwait var 变为 coroutine::vwait var,而 after 123 变为 coroutine::after 123。)

唯一没有直接替换的是 tkwait windowtkwait visibility;你需要模拟那些等待 <Destroy><Visibility> 事件(后者不常见,因为它在某些平台上不受支持),你可以通过 binding 一个简单的回调来完成在那些只设置一个你可以 coroutine::vwait 的变量上(这基本上是 tkwait 在内部所做的一切)。

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