Skip to content

线程调度器 ​

线程调度器模块提供了一个统一的 API,用于在不同的 DCC(数字内容创作)应用程序中在主线程/UI 线程上执行代码。许多 DCC 应用程序要求某些操作必须在主线程上执行以确保线程安全。

概述 ​

不同的 DCC 应用程序有不同的主线程执行 API:

DCC 应用程序延迟执行阻塞执行
Mayamaya.utils.executeDeferred()maya.utils.executeInMainThreadWithResult()
Houdinihdefereval.executeDeferred()hdefereval.executeInMainThread()
Blenderbpy.app.timers.register()基于队列的方式
Nukenuke.executeInMainThread()nuke.executeInMainThreadWithResult()
3ds MaxMaxPlus.Core.EvalOnMainThread()带返回值的包装器
Unreal Engineregister_slate_post_tick_callback()基于事件的方式
Qt 应用程序QTimer.singleShot()基于事件的方式

线程调度器抽象了这些差异,提供了一个一致的 API,能够自动检测并使用适当的后端。

核心特性 ​

  • 懒加载:DCC 后端仅在需要时才加载,避免导入错误
  • 字符串注册:通过模块路径注册后端以实现懒加载
  • 环境变量覆盖:通过 AURORAVIEW_DISPATCHER 强制指定后端
  • 优先级选择:优先级高的后端优先尝试
  • 自动检测:自动检测当前 DCC 环境

快速开始 ​

基本用法 ​

python
from auroraview.utils import run_on_main_thread, run_on_main_thread_sync

# 即发即忘执行(非阻塞)
def create_cube():
    import maya.cmds as cmds
    cmds.polyCube()

run_on_main_thread(create_cube)

# 阻塞执行并获取返回值
def get_selection():
    import maya.cmds as cmds
    return cmds.ls(selection=True)

selected = run_on_main_thread_sync(get_selection)
print(f"已选择: {selected}")

使用装饰器 ​

python
from auroraview.utils import ensure_main_thread, defer_to_main_thread

# 确保函数始终在主线程上运行
@ensure_main_thread
def update_viewport():
    import maya.cmds as cmds
    cmds.refresh()

# 可以从任何线程安全调用
update_viewport()

# 即发即忘装饰器
@defer_to_main_thread
def log_message(msg):
    print(f"[主线程] {msg}")

# 立即返回,稍后在主线程上执行
log_message("来自后台线程的问候!")

检查当前线程 ​

python
from auroraview.utils import is_main_thread

if is_main_thread():
    # 可以直接安全调用 DCC API
    do_dcc_operation()
else:
    # 需要调度到主线程
    run_on_main_thread(do_dcc_operation)

API 参考 ​

函数 ​

run_on_main_thread(func, *args, **kwargs) -> None ​

在主线程上执行函数,不等待结果(即发即忘)。

参数:

  • func:要执行的函数
  • *args:位置参数
  • **kwargs:关键字参数

示例:

python
def create_sphere(radius):
    import maya.cmds as cmds
    cmds.polySphere(radius=radius)

run_on_main_thread(create_sphere, 2.0)

run_on_main_thread_sync(func, *args, **kwargs) -> T ​

在主线程上执行函数并等待结果(阻塞)。

参数:

  • func:要执行的函数
  • *args:位置参数
  • **kwargs:关键字参数

返回: 函数的返回值

异常: 重新抛出函数中的任何异常

示例:

python
def get_scene_name():
    import maya.cmds as cmds
    return cmds.file(q=True, sceneName=True)

scene = run_on_main_thread_sync(get_scene_name)

is_main_thread() -> bool ​

检查当前线程是否是主线程/UI 线程。

返回: 如果在主线程上返回 True,否则返回 False

ensure_main_thread(func) -> Callable ​

确保函数在主线程上运行的装饰器。如果从后台线程调用,函数将被调度到主线程,调用会阻塞直到完成。

示例:

python
@ensure_main_thread
def safe_ui_update():
    # 这始终在主线程上运行
    update_ui_elements()

defer_to_main_thread(func) -> Callable ​

将函数执行延迟到主线程的装饰器(即发即忘)。被装饰的函数立即返回 None。

示例:

python
@defer_to_main_thread
def async_log(message):
    print(message)

async_log("这稍后打印")  # 立即返回

后端管理 ​

get_dispatcher_backend() -> ThreadDispatcherBackend ​

获取当前活动的后端。

list_dispatcher_backends() -> List[Tuple[int, str, bool]] ​

列出所有已注册的后端及其优先级和可用性。

返回: (priority, name, is_available) 元组列表

示例:

python
for priority, name, available in list_dispatcher_backends():
    status = "+" if available else "-"
    print(f"{status} {name} (优先级={priority})")

register_dispatcher_backend(backend, priority=0, *, name="") ​

注册自定义后端。支持类注册和字符串注册两种方式。

参数:

  • backend:ThreadDispatcherBackend 的子类,或 "module:ClassName" 格式的字符串路径
  • priority:值越高越优先尝试(默认:0)
  • name:后端的可选显示名称

示例 - 类注册:

python
register_dispatcher_backend(MyDCCBackend, priority=250)

示例 - 字符串注册(懒加载):

python
# 仅在调用 get_dispatcher_backend() 时才加载
register_dispatcher_backend(
    "my_package.dispatchers:MyDCCBackend",
    priority=250,
    name="MyDCC"
)

unregister_dispatcher_backend(backend) -> bool ​

注销之前注册的后端。

参数:

  • backend:要注销的后端类或字符串路径

返回: 如果找到并移除返回 True,否则返回 False

clear_dispatcher_backends() -> None ​

清除所有已注册的后端并重置为初始状态。主要用于测试。

环境变量覆盖 ​

您可以使用 AURORAVIEW_DISPATCHER 环境变量强制指定后端:

bash
# 强制使用 Qt 后端
export AURORAVIEW_DISPATCHER=qt

# 强制使用 fallback 后端
export AURORAVIEW_DISPATCHER=fallback

有效值(不区分大小写):maya、houdini、nuke、blender、max、unreal、qt、fallback

自定义后端 ​

类注册 ​

python
from auroraview.utils.thread_dispatcher import (
    ThreadDispatcherBackend,
    register_dispatcher_backend
)

class MyDCCBackend(ThreadDispatcherBackend):
    """MyDCC 应用程序的自定义后端。"""
    
    def is_available(self) -> bool:
        try:
            import mydcc
            return True
        except ImportError:
            return False
    
    def run_deferred(self, func, *args, **kwargs):
        import mydcc
        mydcc.execute_deferred(lambda: func(*args, **kwargs))
    
    def run_sync(self, func, *args, **kwargs):
        import mydcc
        return mydcc.execute_in_main_thread(lambda: func(*args, **kwargs))
    
    def is_main_thread(self) -> bool:
        import mydcc
        return mydcc.is_main_thread()

# 以高优先级注册
register_dispatcher_backend(MyDCCBackend, priority=250, name="MyDCC")

字符串注册(懒加载) ​

对于外部包或避免 DCC 不可用时的导入错误:

python
from auroraview.utils import register_dispatcher_backend

# 通过模块路径注册 - 仅在需要时才加载
register_dispatcher_backend(
    "my_package.dispatchers:MyDCCBackend",
    priority=250,
    name="MyDCC"
)

这在以下情况特别有用:

  • 您的后端依赖于可能未安装的 DCC 特定模块
  • 您想分发一个跨多个 DCC 工作的插件
  • 您需要延迟导入直到 DCC 环境完全初始化

内置后端 ​

以下后端默认已注册(按优先级排序):

后端优先级描述
Maya200使用 maya.utils 模块
Houdini190使用 hdefereval 模块
Nuke180使用 nuke 模块
Blender170使用 bpy.app.timers
3ds Max160使用 MaxPlus 或 pymxs
Unreal150使用 slate tick 回调
Qt100使用 QTimer.singleShot()
Fallback0直接执行(会记录警告)

使用场景 ​

后台处理与 UI 更新 ​

python
import threading
from auroraview.utils import run_on_main_thread

def process_data(data):
    """在后台线程中处理数据。"""
    result = heavy_computation(data)
    
    # 在主线程上更新 UI
    run_on_main_thread(update_progress_bar, 100)
    run_on_main_thread(show_result, result)

# 启动后台线程
thread = threading.Thread(target=process_data, args=(my_data,))
thread.start()

线程安全的事件处理器 ​

python
from auroraview import WebView
from auroraview.utils import ensure_main_thread

webview = WebView()

@webview.on("export_scene")
@ensure_main_thread
def handle_export(data):
    """处理导出事件 - 始终在主线程上运行。"""
    import maya.cmds as cmds
    cmds.file(data['path'], exportSelected=True, type='mayaAscii')

带返回值的异步操作 ​

python
from concurrent.futures import ThreadPoolExecutor
from auroraview.utils import run_on_main_thread_sync

def get_scene_info():
    """从主线程获取场景信息。"""
    return run_on_main_thread_sync(lambda: {
        'name': cmds.file(q=True, sceneName=True),
        'objects': len(cmds.ls(dag=True)),
    })

with ThreadPoolExecutor() as executor:
    future = executor.submit(get_scene_info)
    info = future.result()

最佳实践 ​

  1. 最小化主线程工作:保持主线程操作简短,避免 UI 冻结。

  2. 批量操作:将多个 DCC 操作组合到单个主线程调用中。

  3. 对即发即忘使用延迟执行:当不需要返回值时使用 run_on_main_thread。

  4. 处理异常:使用 run_sync 时用 try/except 包装主线程操作。

  5. 调度前检查线程:使用 is_main_thread() 避免不必要的调度。

python
from auroraview.utils import is_main_thread, run_on_main_thread_sync

def safe_operation():
    if is_main_thread():
        return do_operation()
    else:
        return run_on_main_thread_sync(do_operation)
  1. 对插件使用字符串注册:分发插件时,使用字符串注册以避免导入错误。

故障排除 ​

函数未执行 ​

原因:DCC 应用程序的事件循环未运行。

解决方案:确保应用程序处于交互状态,而不是在启动或关闭期间。

使用 run_sync 时死锁 ​

原因:在主线程被阻塞时从主线程调用 run_on_main_thread_sync。

解决方案:调用前检查 is_main_thread(),或改用 run_on_main_thread(非阻塞)。

选择了错误的后端 ​

原因:检测到多个 DCC 环境。

解决方案:以更高优先级注册自定义后端,或设置 AURORAVIEW_DISPATCHER 环境变量。

DCC 模块导入错误 ​

原因:DCC 特定模块在当前环境中不可用。

解决方案:使用字符串注册进行懒加载,或在导入前检查可用性。

另请参阅 ​

WebView 的 DCC 线程安全 ​

当将 AuroraView WebView 集成到 DCC 应用程序时,您需要处理 WebView 线程和 DCC 主线程之间的线程安全。AuroraView 为此提供了专门的工具。

挑战 ​

  • WebView 线程:WebView2 在其自己的 STA 线程上运行
  • DCC 主线程:DCC API(Maya cmds、Blender bpy 等)必须从主线程调用
  • 事件处理器:@webview.on() 处理器可能从 WebView 线程调用

使用 @dcc_thread_safe 装饰器 ​

@dcc_thread_safe 装饰器自动将函数执行调度到 DCC 主线程:

python
from auroraview import WebView
from auroraview.utils import dcc_thread_safe

webview = WebView(parent=dcc_hwnd)

@webview.on("create_object")
@dcc_thread_safe  # 确保在 DCC 主线程上运行
def handle_create(data):
    import maya.cmds as cmds
    return cmds.polyCube()[0]

使用 dcc_mode ​

在 WebView 上启用 dcc_mode 以自动包装所有回调:

python
# 所有回调自动在 DCC 主线程上运行
webview = WebView(parent=dcc_hwnd, dcc_mode=True)

@webview.on("create_object")
def handle_create(data):  # 不需要装饰器!
    import maya.cmds as cmds
    return cmds.polyCube()[0]

线程安全包装器 ​

使用 thread_safe() 进行跨线程 WebView 操作:

python
webview = WebView(parent=dcc_hwnd)

# 获取线程安全包装器
safe = webview.thread_safe()

# 可以从任何线程调用:
safe.eval_js("updateStatus('ready')")
safe.emit("data_loaded", {"count": 100})
safe.load_url("https://example.com")

使用 @dcc_thread_safe_async 进行即发即忘 ​

对于不需要返回值的操作:

python
from auroraview.utils import dcc_thread_safe_async

@dcc_thread_safe_async
def update_viewport():
    import maya.cmds as cmds
    cmds.refresh()

# 立即返回,稍后在主线程上执行
update_viewport()

超时保护 ​

使用 run_on_main_thread_sync_with_timeout ​

对于可能挂起或耗时过长的操作,使用带超时保护的版本:

python
from auroraview.utils import (
    run_on_main_thread_sync_with_timeout,
    ThreadDispatchTimeoutError
)

def slow_operation():
    import maya.cmds as cmds
    return cmds.ls(dag=True)  # 大型场景可能很慢

try:
    # 如果超过 10 秒将抛出 ThreadDispatchTimeoutError
    result = run_on_main_thread_sync_with_timeout(
        slow_operation,
        timeout=10.0
    )
except ThreadDispatchTimeoutError as e:
    print(f"操作超时: {e}")
    # 优雅地处理超时

线程安全异常 ​

AuroraView 为线程安全问题提供了专门的异常类:

python
from auroraview.utils import (
    ThreadSafetyError,          # 所有线程安全错误的基类
    ThreadDispatchTimeoutError, # 主线程调度超时
    DeadlockDetectedError,      # 检测到潜在死锁
    ShutdownInProgressError,    # 关闭期间的操作
)

try:
    result = run_on_main_thread_sync_with_timeout(operation, timeout=5.0)
except ThreadDispatchTimeoutError:
    # 处理超时 - 可能表示死锁或操作过慢
    pass
except ThreadSafetyError as e:
    # 捕获任何线程安全相关错误
    logger.error(f"线程安全错误: {e}")

死锁预防 ​

常见死锁模式 ​

模式 1:跨线程同步调用 ​

python
# 错误:这可能导致死锁!
@webview.on("get_data")
def handle_get_data(data):
    # WebView 线程等待主线程
    result = run_on_main_thread_sync(get_scene_data)
    # 同时,主线程可能在等待 WebView...
    return result

# 正确:使用超时保护
@webview.on("get_data")
def handle_get_data(data):
    try:
        result = run_on_main_thread_sync_with_timeout(
            get_scene_data,
            timeout=5.0
        )
        return result
    except ThreadDispatchTimeoutError:
        return {"error": "timeout"}

模式 2:嵌套回调 ​

python
# 错误:在锁内调用回调可能导致死锁
def process_data():
    with data_lock:
        user_callback()  # 如果回调需要 data_lock -> 死锁

# 正确:在回调前释放锁
def process_data():
    with data_lock:
        result = compute_result()
    user_callback(result)  # 在锁外调用

死锁预防最佳实践 ​

  1. 始终使用超时 - 对同步跨线程调用使用超时保护
  2. 避免嵌套锁 - 按一致的顺序获取锁
  3. 不要在锁内调用用户代码 - 先释放锁
  4. 尽可能使用即发即忘 - 使用 run_on_main_thread
  5. 调度前检查线程 - 避免不必要的跨线程调用

另请参阅 ​

Released under the MIT License.