NiceGUI 进度条实战:用 `run.cpu_bound` 与 `ui.timer` 为重量级计算任务构建实时进度反馈
2026/9/14 17:12:10 网站建设 项目流程

NiceGUI 进度条实战:用run.cpu_boundui.timer为重量级计算任务构建实时进度反馈

【免费下载链接】niceguiCreate web-based user interfaces with Python. The nice way.项目地址: https://gitcode.com/GitHub_Trending/ni/nicegui

本文以 NiceGUI 官方示例 examples/progress 为主线,完整剖析"耗时计算 + 实时进度条"这一经典场景的落地方式:后台进程执行 CPU 密集任务,通过multiprocessing.Queue回传进度,前端由ui.timer轮询驱动ui.linear_progress平滑刷新。读完本文,你将掌握 NiceGUI 中进程池调用(run.cpu_bound)、跨进程进度通信、定时器轮询与进度条元素配置的组合用法,并能直接复用到文件处理、模型推理、数据清洗等任何重型计算场景。

一、示例概览:文档说了什么

原文档 README.md 内容极为精炼,核心只有一句话:

Demonstrate a progress bar for heavy computations.

即"演示如何为重量级计算显示进度条"。但麻雀虽小,五脏俱全,对应的 main.py 是一个约 40 行的自包含可运行程序,完整覆盖了 NiceGUI 处理耗时任务的四条关键链路:

  1. run.cpu_bound把 CPU 密集函数调度到独立进程执行,避免阻塞事件循环;
  2. multiprocessing.Manager().Queue()作为跨进程进度通道
  3. ui.timer周期性轮询队列并把进度写回进度条;
  4. ui.linear_progress渲染进度,配合props('instant-feedback')获得即时反馈。

二、完整代码与逐段精讲

下面是对 main.py 的完整复刻(保留了原示例的全部逻辑,仅补充注释):

#!/usr/bin/env python3 import time from multiprocessing import Manager, Queue from nicegui import run, ui def heavy_computation(q: Queue) -> str: """Run some heavy computation that updates the progress bar through the queue.""" n = 50 for i in range(n): # Perform some heavy computation time.sleep(0.1) # Update the progress bar through the queue q.put_nowait(i / n) return 'Done!' @ui.page('/') def main_page(): async def start_computation(): progressbar.visible = True result = await run.cpu_bound(heavy_computation, queue) ui.notify(result) progressbar.visible = False # Create a queue to communicate with the heavy computation process queue = Manager().Queue() # Update the progress bar on the main process ui.timer(0.1, callback=lambda: progressbar.set_value(queue.get() if not queue.empty() else progressbar.value)) # Create the UI ui.button('compute', on_click=start_computation) progressbar = ui.linear_progress(value=0).props('instant-feedback') progressbar.visible = False ui.run()

2.1 耗时任务函数:只负责算,不碰 UI

def heavy_computation(q: Queue) -> str: n = 50 for i in range(n): time.sleep(0.1) q.put_nowait(i / n) return 'Done!'

该函数模拟了 50 步、每步耗时 0.1 秒的"重型计算"(总耗时约 5 秒)。每一步完成后调用q.put_nowait(i / n)把当前完成比例(0.0~1.0)非阻塞地写入队列。

这里有一个 NiceGUI 进程池的硬性约束需要遵守(详见下文源码分析):传给run.cpu_bound的函数必须是模块级(module-level)函数,参数与返回值都必须能被pickle序列化。因此原示例刻意把计算逻辑从页面闭包中剥离为顶层函数,而不是在页面内定义嵌套函数或 lambda——这是示例代码最重要的设计意图之一。

2.2 页面与异步事件处理器

@ui.page('/') def main_page(): async def start_computation(): progressbar.visible = True result = await run.cpu_bound(heavy_computation, queue) ui.notify(result) progressbar.visible = False

start_computation是一个async 事件处理器,由按钮点击触发,核心动作只有三个:

  1. 显示进度条(progressbar.visible = True);
  2. await run.cpu_bound(...):把重活交给进程池,await挂起协程但不阻塞事件循环,其余客户端请求仍可正常响应;
  3. 任务完成后用ui.notify(result)弹出 "Done!" 通知,并隐藏进度条。

2.3 跨进程进度通道:Manager().Queue()

queue = Manager().Queue()

这是本示例的关键设计。run.cpu_bound独立进程中执行heavy_computation,而进度条属于主进程(服务端)的 UI 状态,两者无法直接共享内存。multiprocessing.Manager().Queue()是一个进程安全的代理队列:worker 进程通过put_nowait写入,主进程通过get读出,NiceGUI 由此实现了"后台进程产生进度、前端消费进度"的解耦。

说明:也可以改用multiprocessing.Queue()的全局实例,但Manager().Queue()在 spawn 启动方式下更稳妥,且无需额外的初始化器配合,示例的选择具备良好的可移植性。

2.4 前端轮询:ui.timer驱动进度条

ui.timer(0.1, callback=lambda: progressbar.set_value(queue.get() if not queue.empty() else progressbar.value))

ui.timer(0.1, callback)每 0.1 秒执行一次回调(具体实现见 nicegui/elements/timer.py,它是按客户端注册的定时器)。回调里:

  • 队列非空:取出最新进度queue.get()set_value
  • 队列为空:保持progressbar.value不变,避免把进度"回退"。

这样无论 worker 进程写得有多快,前端都以固定节奏平滑推进进度条。10 Hz 的轮询频率在 5 秒的 50 步任务下体验流畅,且开销极小。

2.5 进度条元素与按钮

ui.button('compute', on_click=start_computation) progressbar = ui.linear_progress(value=0).props('instant-feedback') progressbar.visible = False
  • ui.linear_progress(value=0):初始进度为 0;
  • .props('instant-feedback'):关闭 Quasar 的过渡动画,进度更新立即呈现——这正是"进度条实时跟随队列数据"的关键;
  • 初始visible = False:计算开始前不显示进度条,避免空进度条干扰界面。

三、源码纵深:run.cpu_bound的进程池原理

进度条背后真正的主角是 nicegui/run.py 中实现的run.cpu_bound。理解它的内部机制,才能解释示例代码为什么要写成"模块级函数 + 队列回传"。

3.1 两个内置执行器

在 run.py 中,NiceGUI 维护了两个全局执行器:

process_pool: ProcessPoolExecutor | None = None thread_pool = ThreadPoolExecutor()
  • run.io_bound(callback, ...):把 I/O 密集任务(网络请求、文件读写、无异步支持的数据库驱动)提交给线程池,见 run.py;
  • run.cpu_bound(callback, ...):把 CPU 密集任务提交给进程池,见 run.py。

官方文档 nicegui/llms.md 明确建议:优先使用run.io_bound/run.cpu_bound,而不是自行asyncio.to_thread()或手写run_in_executor,因为 NiceGUI 统一管理池的创建与关闭。

3.2 为什么 CPU 密集任务必须进独立进程

Python 的 GIL(全局解释器锁)决定了:在单进程内,CPU 密集代码几乎无法通过线程获得并行加速。若直接在事件循环中执行,整个应用会卡死;若塞进线程池,GIL 仍会让其他协程饿肚子。因此run.cpu_bound把函数连同参数pickle序列化后,交给ProcessPoolExecutor在独立进程中执行:

return await _run(process_pool, safe_callback, callback, *args, **kwargs)

见 run.py。这带来两个示例中必须遵守的约束(源码 docstring 与 llms.md 均有说明):

  • 函数必须是可 pickle 的:请使用静态方法或模块级自由函数,把数据作为简单参数传入、把结果作为返回值传出,不要在函数内触碰 UI 或类属性;
  • 传参和返回值同样要可 pickle:本示例传入的Manager().Queue()恰好满足这一点,跨进程通信因此成立。

此外,run.cpu_bound对子进程异常做了包装:子进程中抛出的异常会被safe_callback捕获并转换为可 picklable 的SubprocessException(见 run.py),主进程侧await处会重新抛出,保证报错信息(类型、消息、堆栈)完整透传。

3.3 进程池的启动方式与注意事项

run.py 提供了可配置的启动方式:

process_pool_start_method: Literal['spawn', 'fork', 'forkserver'] | None = None
  • None(默认):沿用平台默认。在 Linux/Docker 上 Python 3.13 之前默认是fork,NiceGUI 会打印一次警告,因为fork在线程化进程中不安全(详见 run.py 的说明);
  • 'spawn':推荐选项,worker 不继承父进程状态,跨平台行为一致。NiceGUI 4.0 将把默认值改为'spawn'
  • 必须在ui.run()之前设置,例如:
from nicegui import run run.process_pool_start_method = 'spawn'

见 llms.md。在 Windows 上(只有spawn可用)以及使用 spawn 时,请确保计算函数与ui.run()都位于if __name__ == '__main__':保护之下,避免子进程重复执行页面注册代码。

四、进度条元素速查:ui.linear_progressui.circular_progress

示例使用的ui.linear_progress定义在 nicegui/elements/progress.py,是对 QuasarQLinearProgress组件的封装。构造参数如下:

参数类型默认值说明
valuefloat0.0当前进度,取值范围 0.0~1.0
sizestr \| NoneNone进度条高度;None时自动取 "20px"(显示数值标签)或 "4px"(不显示)
show_valueboolTrue是否在进度条中央显示百分比数值标签
colorstr \| None'primary'颜色,支持 Quasar、Tailwind 或 CSS 颜色,传None表示不设颜色

show_value=True时,组件内部会在进度条中央放置一个绑定value的文本标签(见 progress.py),因此示例中配合.props('instant-feedback')使用,数值标签会与进度条同步跳动,形成直观的实时反馈。

同文件还提供了环形进度条ui.circular_progress(progress.py),参数为valuemin(默认 0.0)、max(默认 1.0)、size(默认'xl')、show_valuecolor,并内置track-color: grey-4的轨道底色。当 UI 空间紧张时,可把示例中的ui.linear_progress直接替换为ui.circular_progress(value=0),轮询回调set_value的用法完全一致。

五、用测试佐证:run.cpu_bound的行为边界

仓库测试 tests/test_run.py 对run.cpu_bound的边界行为做了系统性验证,可作为理解示例行为边界的依据:

  • test_delayed_hello:验证run.cpu_boundrun.io_bound都能在异步处理器中正常 await 并返回结果;
  • test_run_unpickable_exception_in_cpu_bound_callbacktest_run_cpu_bound_function_which_raises_problematic_exception:验证子进程内不可 pickle 的异常会被包装成SubprocessException安全传回主进程;
  • test_run_cpu_bound_survive_bad_function:一个函数出错后,进程池仍可继续执行后续正常任务(BrokenProcessPool会被自动重建,对应 run.py 的重建逻辑);
  • test_returns_none_when_app_is_stopping:应用关闭或协程被取消时,run.cpu_bound返回None而非结果(这是 4.0 之前的中期行为,4.0 将改为抛出CancelledError);
  • test_pool_uses_configured_start_methodtest_fork_heads_up_warning等:验证process_pool_start_method的配置生效与 fork 警告逻辑。

这些测试从侧面印证了示例代码的稳健性设计:进度回传通过队列独立于返回值进行,即使run.cpu_bound因取消/关闭返回None,进度条也不会因此卡死或异常。

六、运行方式与扩展思路

6.1 运行示例

cd examples/progress python main.py

浏览器访问http://localhost:8080,点击compute按钮即可看到进度条从 0 推进到 100%,随后弹出 "Done!" 通知(默认端口可在ui.run(port=...)中调整)。

6.2 从示例到生产的扩展建议

  • 进度粒度:把heavy_computation中的n换成实际任务步数(如文件数量、数据块数),i / n即天然的单位进度;
  • 取消机制:可为按钮增加禁用状态(button.disable()/button.enable()),并在页面卸载时清理队列;
  • 多任务并发:每个任务使用独立Manager().Queue(),或改为在队列中传递(job_id, progress)元组以区分并发任务;
  • 结合run.io_bound:若耗时主要来自 I/O(如批量网络请求),把run.cpu_bound替换为run.io_bound即可,前端进度条代码无需改动;
  • 环形进度:将ui.linear_progress换成ui.circular_progress,即可适配仪表盘类界面。

七、小结

这个不足 40 行的官方示例,浓缩了 NiceGUI 处理重型任务的核心范式:计算进程化(run.cpu_bound)+ 进度通道化(Manager().Queue())+ 前端轮询化(ui.timer)+ 渲染即时化(.props('instant-feedback')。掌握这四个环节,你就可以为任意耗时操作提供平滑、不阻塞的实时进度反馈——这正是生产级 Web 应用体验的关键一环。深入阅读 nicegui/run.py、nicegui/elements/progress.py 与 tests/test_run.py,可进一步理解 NiceGUI 进程池的完整设计与边界行为。

【免费下载链接】niceguiCreate web-based user interfaces with Python. The nice way.项目地址: https://gitcode.com/GitHub_Trending/ni/nicegui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询