高级 FLTk-rs

Advanced FLTk

本章将介绍高级的编程和设计,来帮助您充分利用 FLTK 。

Multithreading

FLTK 可实现多线程的 GUI 应用程序,但与一般的多线程编程一样,必须牢记一些概念和注意事项。

其中的关键是:对于 FLTK 支持的许多目标平台来说,只有进程 main() 的线程被允许处理系统事件、创建或销毁窗口以及打开或关闭窗口。 进一步说,就是只有进程 main() 的线程可以安全地 write to the display。

为了所有目标平台支持这点,

  • 所有 FLTK draw() 方法都得在 main() 线程执行。
  • 一个 worker 线程(非 mian() 线程)可以更新现有小部件的状态,但它不能直接进行任何渲染,也不能创建或销毁窗口。(注意:存在一种特殊情况 GlWindow,只要采取适当的预防措施,它就可以从 worker 线程安全地渲染到现有的 GL 上下文。)

创建可移植线程

利用 Rust 的可移植性,能够轻松创建可移植线程。

FLTK multithread locking - fltk::app::lock() and fltk::app::unlock()

在多线程程序中,widget 的绘制(在 main() 线程)与 widget 在 worker 线程中更新不能同步发生,因此当一个 widget 被修改时不能安全地进行绘制(并且在绘制过程中也不应修改任何 widget)。

FLTK 内部使用锁机制支持多线程应用程序。这样允许一个 worker 线程锁定渲染上下文,阻止发生任何绘制,使得能更改其 widget 的值。

注意:
反之亦然; 当 worker 线程持有锁时, main() 线程可能无法处理任何绘制请求,也无法为任何事件提供服务。 因此,持有 FLTK 锁的 worker 线程必须设法在尽可能短的时间内完成此操作,否则可能会损害应用程序的运行。

锁的使用大致如下:

使用 FLTK 库, 每当 main() 线程处理事件或重绘显示时,它都会持有锁。 它自动获取(锁定)和释放(解锁)FLTK 锁,无需“用户干预”。 事实上,一个在 main() 线程上下文中运行的函数理想情况下,不应显式获取/释放 FLTK 锁。(但请注意,锁调用是递归的,因此从已经持有锁的线程(包括 main()线程)调用 lock(),是良性的。 唯一的限制是每次调用 lock() 都必须相应调用 unlock() 来平衡,以确保保持锁计数器。)

这 main()线程必须在显示任何窗口之前调用 lock() 一次 ,以启用内部锁(默认情况下它是“关闭”的,因为它在单线程应用程序中没有用),但此后 main() 线程锁由 FLTK 库内部管理。

当 worker 线程想要更​​改 widget 的值时,可以使用 Fl::lock() 获取锁,更新小部件,然后使用 Fl::unlock() 释放锁。获取锁可确保 worker 线程可以更新 widget,而不会有任何风险(风险指 widget 更新时,main() 线程尝试重绘 widget)。

注意,获取锁是一个阻塞动作; worker 线程将在获取锁所需的时间内暂停。 如果 main() 线程正在进行一些复杂的绘制操作,这可能会长时间阻塞 worker 线程,实际上从而串行化本应并行的操作。 (对于不太熟悉多线程编程问题的程序员来说,这常常会让人感到惊讶;有关管理此问题的策略,请参阅稍后对“无锁编程”的讨论。)

为了将锁定机合并到库中,FLTK 在配置过程中必须使用 --enable-threads 进行编译。 基于 IDE 的 FLTK 版本会自动编译,并在可能的情况下合并锁定机制。 从 1.3 版本开始, configure 构建 FLTK 库的脚本默认设置 --enable-threads。

Simple multithreaded examples using Fl::lock

在 main(),在 run() 或 wait() 之前调用 lock 一次去启用锁并为程序支持多线程运行时。此时所有回调和派生函数,例如 handle() 和 draw()将被正确锁定。

这可能看起来像这样:

fn main() {
  /* Create your windows and widgets here */
 
  fltk::app::lock(); /* "start" the FLTK lock mechanism */
 
  /* show your window */
  main_win.show();

  /* start your worker threads */
  ... start threads ...
 
  /* Run the FLTK main loop */
  fltk::app::run().unwrap();
 
  /* terminate any pending worker threads */
  ... stop threads ...
}

您可以根据需要启动任意多个线程。 从线程内部(除了 main()线程)FLTK 调用必须包含对 lock() 和 unlock() 的调用:

fn my_thread() {
  while (thread_still_running) {
    /* do thread work */
    ...
    /* compute new values for widgets */
    ...
 
    fltk::app::lock();      // acquire the lock
    my_widget.update(values);
    fltk::app::unlock();    // release the lock; allow other threads to access FLTK again
    fltk::app::awake();     // use Fl::awake() to signal main thread to refresh the GUI
  }
}

注意
要从 worker 线程触发 GUI 刷新, worker 线程的代码应调用 fltk::app::awake()

使用 fltk::app::awake()

您可以在 worker 线程使用 fltk::app::awake() 通知 main() 线程进行绘制。

您的 worker 线程可以使用 Fl::awake(void* message) 将消息发送到 main()线程 :
~~ ~~void *msg; // "msg" is a pointer to your message~~ ~·Fl::awake(msg); // send "msg" to main thread~~ ~~
消息可以是您喜欢的任何内容。main() 可以通过调用 Fl::thread_message() 来检索消息。

注意
fltk-rs 新版本不支持 fl::app::awake_msg()(即对应 C++ FLTK 的 Fl::awake(void* message)),只支持无消息版本的 awake。

使用 fltk::app::awake_callback() 回调消息

您还可以要求 main() 线程使用 fltk::app::awake_callback 调用代表 worker 线程函数。

当 main() 下次处理待处理事件时, main() 线程将“尽快”执行这个回调。 worker 线程可以使用它来执行 worker 线程中禁止的操作(例如显示或隐藏窗口)。

fn do_something_cb(void *userdata) {
  // Will run in the context of the main thread
  ... do_stuff ...
}
 
// running in worker thread
void *data;                       // "data" is a pointer to your user data
fltk::app::awake_callback(do_something_cb, data); // call to execute cb in main thread

注意
在 worker 线程注册请求后的某个短暂但不确定的时间,main() 线程将与 worker 线程不同步地执行 Fl_Awake_Handler 的 do_something_cb 回调。 当它执行 Fl_Awake_Handler 回调时, main() 线程将使用*userdata的内容在执行时, 是 不一定 内容 *userdata当 worker 线程发布回调请求时。 因此, worker 线程应该不要改变 *userdata 内容,一旦它请求了回调,因为 worker 线程不知道什么时候 main() 线程将消耗该数据。 这通常很有用,userdata 指向一个结构体,其中一个成员, main()线程可以修改以表明它已经消耗了数据,从而允许 worker 线程重用或更新 userdata.

警告
用于将 fltk::app::awake() 和 fltk::app::awake_callback() 事件传递给 main()在某些平台上,线程可能会以意想不到的方式进行交互。 因此,为了可靠运行,建议程序要么使用 fltk::app::awake() 要么使用 fltk::app::awake_callback() ,但切勿混合使用它们。在任何一种情况下,调用不带参数的 fltk::app::awake() 都是安全的。
如果您必须在使用 fltk::app::awake() 和 fltk::app::awake_callback() 机制之间进行选择并且不知道选择哪个,那么请首先尝试 fltk::app::awake_callback() 方法,因为它通常更强大。

FLTK multithreaded "lockless programming"

上面展示了使用 FLTK 锁的简单多线程示例,适用于许多需要多线程的情况。 然而,当该模型扩展到更复杂的程序时,它常常会产生开发者难以预料到的结果。

一个典型的案例可能是这样的: 一个开发者创建了一个程序来处理巨大的数据集。该程序有一个 main() 线程和 7 个 worker 线程,目标是在 8 核计算机上运行。当它运行时,程序将数据分配给 7 个 worker 线程,当它们处理自己的数据份额时,每个线程都会用结果更新其 GUI 部分,并在执行过程中锁定和解锁。

但当这个程序运行时,它比预期慢得多,开发人员发现尽管程序中有 8 个线程,但 8 个 CPU 核心似乎只有一个被利用。 发生了什么?

程序中的线程都按预期运行,但它们最终被串行化(即无法并行运行),因为它们都依赖于单个 FLTK 锁。获取(和释放)该锁会产生相关成本,并且是一个阻塞动作,如果该锁已被任何其他 worker 线程或 main()线持有。

如果 worker 线程“过于频繁”地获取锁,那么该锁将 始终 保留 在某处 ,并且任何其他线程的每次尝试(甚至 main())锁定会导致其他线程(包括 main())来阻塞。main() 线程阻塞还导致阻塞事件处理、显示刷新...

结果,在任何给定时间只有一个线程运行,并且多线程程序实际上减少为(复杂且效率稍低)单线程程序。

一个“解决方案”是让 worker 线程“不那么频繁地”锁定,这样它们就不会互相阻塞或阻塞 main()线。 但是,判断什么导致了“过于频繁”锁定,从而导致阻塞,是一个非常棘手的问题。一台具配置显卡和 CPU 的机器上运行良好的内容在另一台目标机器上可能表现得非常不同。

这个主题也有一些“有趣的”变化:例如,如上所述的“有缺陷的”多线程程序有可能在单核机器上运行(所有线程本质上都是串行化的,因此不太可能相互阻塞),但当线程确实相互干扰时,在多核计算机上会以意想不到的方式停滞甚至死锁。 (我已经看到了这一点——它确实发生了。)

“更好”的解决方案是尽可能避免使用 FLTK 锁。 相反,代码的设计应使 worker 线程本身不会更新 GUI,因此永远不需要获取 FLTK 锁。这就是 FLTK 多线程“无锁编程”。

在实践中,有多种方法可以实现“无锁编程”(或至少近似),但最直接的方法是 worker 线程使用 fltk::app::awake_callback 方法,以便 GUI 更新都可以在上下文中运行 main()线程,减轻 worker 线程锁定的需要。然后, worker 线程就有责任来管理 userdata 以便将其安全地运送到 main() 线程,但有很多方法可以完成。

注意
严格来说,使用 fltk::app::awake 并不是完全“无锁”,因为唤醒处理程序机制在内部包含资源锁定以保护待处理唤醒消息的队列。这些资源锁是暂时持有的,通常不会触发此处描述的病态阻塞问题。

然而,除了使用 fltk::app::awake 之外,还有许多其他方式可以实现“无锁”设计,包括消息传递、各种形式的 IPC 等。

如果您需要高性能多线程编程,那么请花一些时间研究这些选项并了解每个选项的优点和缺点;在这,我们不能触及这个巨大主题的表面!

当然,偶尔地使用 worker 线程中的 FLTK 锁不会造成任何损害; “频繁”锁定(无论是什么)会导致失败行为。

在处理批量数据时(或者实际上,在所有情况下!)以较低的频率更新 GUI 始终是最好的,每秒几帧的更新速度可能足以在长时间计算期间提供反馈。 在上限下,任何比显示器的帧率更快的内容和更新都不会显示;为什么要浪费你永远不会显示的CPU计算像素呢?

FLTK multithreaded Constraints

FLTK 支持多个平台,其中一些仅允许 main()线程处理系统事件并打开或关闭窗口。安全的做法是在所有操作系统上遵守以下线程规则:

  • 不在 worker 线程 show() 或者 hide() 任何基于 Fl_Window 的小部件。 这包括任何窗口、对话框、文件选择器、子窗口或使用 Fl_Gl_Window 的小部件。请注意,此约束也适用于具有 tooltip 的非窗口小部件,因为 tooltip 包含 Fl_Window 对象。 安全且可移植的方法是 永远 不要在 worker 线程上下文中调用 show()或者 hide()的任何小部件。 相反,您可以使用Fl_Awake_Handler 的变体 Fl::awake() 来请求 main()线程代表 worker 线程创建、销毁、显示或隐藏小部件。

  • 不要调用 Fl::run() 、 Fl::wait() 、 Fl::flush() 、 Fl::check() 或任何处理来自 worker 线程的系统消息的相关方法

  • 不要在同一程序中 混合使用Fl::awake(Fl_Awake_Handler cb, void* userdata) 和 Fl::awake(void* message) 调用,因为它们在某些平台上可能会发生不可预测的交互; 选择一种或其他风格的 Fl::awake() 机制并使用它。 (但是,混合调用 Fl::awake()应该是安全的。)

  • 不要从 worker 线程启动或取消计时器

  • 不要从 worker 线程更改窗口装饰或标题

  • 这 make_current() 该方法可能不适用于常规窗口,但应该始终适用于 Fl_Gl_Window, 以 允许在具有多个管道的显卡上进行高速渲染。 管理对 GL 管道的线程安全访问留作读者的练习! (并且可能是特定目标......)

另请参见:

  • fltk::app::lock()
  • fltk::app::unlock()
  • fltk::app::awake()
  • fltk::app::awake_callback()
  • fltk::app::awake()
  • fltk::app::thread_message()
posted @ 2024-01-29 21:37  rfrf  阅读(233)  评论(0编辑  收藏  举报