RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 1088120
Accepted
user302909
user302909
Asked:2020-02-27 18:42:37 +0000 UTC2020-02-27 18:42:37 +0000 UTC 2020-02-27 18:42:37 +0000 UTC

Unity 编辑器中的多线程

  • 772

我想在编辑器中并行做一些事情,但同时我不想在计算过程中挂起编辑器的 UI。是否可以使用任务或类似的东西?或者至少手动管理流程?

c#
  • 2 2 个回答
  • 10 Views

2 个回答

  • Voted
  1. Best Answer
    user302909
    2020-02-27T18:42:37Z2020-02-27T18:42:37Z

    您可以,但有一个例外 - Unity 禁止在其主线程之外调用其大部分 API。

    顺便说一下,可以在子线程中调用 Debug 类及其方法。

    “运行时”中有什么,编辑器中有什么,Unity 有自己的 SynchronizationContext 实现 - UnitySynchronizationContext,除此之外,我们原则上不需要任何东西。


    一个微不足道的任务被简单地解决了:

    CancellationTokenSource _cts;
    
    var token = _cts.Token;
    var context = SynchronizationContext.Current;
    Task.Run(() => DoWork(context, ...), token);
    

    反正一切都很清楚,问题是在那之后返回主线程,此外,将一些东西转移到某个窗口。

    好吧,我们没有很多选择,而确切地说是 1 -EditorWindow.GetWindow<T>它要么创建一个窗口,要么恢复一个保存的窗口,或者,充其量,专注于一个打开的窗口。

    对于后台任务,我希望窗口在那里更新一些东西,但用户不会被猛拉,也不会从活动窗口中获得焦点。在这里,我们有 2 个选项:

    1. Singleton 是经典之作,但转向它寻求解决方案已不再有趣 :)
    2. GetWindow<T>("Title", focus: false)- 你将不得不重新传输窗口的标题,但你能做什么。

    让我们在第二个选项上停下来,得到这样的东西:

    private static CustomWindow GetCustomWindow(bool focus) {
        return GetWindow<CustomWindow>("Task example", focus);
    }
    

    那么一切都是微不足道的:

    在我们的窗口中,我们有一些“进步”的衡量标准:

    private float _progress = 0f;
    private float Progress {
        set {
            _progress = value;
            // этот вызов перерисовки обязателен - без него не обновится визуал
            Repaint();
        }
    }
    

    我们在任务中做重要的事情——我们睡觉:

    private static void DumbTaskExample(int subTaskCount, int sleepTime, SynchronizationContext context, CancellationToken token) {
        Debug.Log($"Task started at {Thread.CurrentThread.ManagedThreadId}");
        for (int i = 0; i < subTaskCount; i++) {
            // Cancellation
            token.ThrowIfCancellationRequested();
            
            // Work
            Thread.Sleep(sleepTime);
            
            // Notification
            context.Post(_ => GetCustomWindow(false).Progress = (float)i/subTaskCount, null);
        }
        Debug.Log($"Task done at {Thread.CurrentThread.ManagedThreadId}");
        context.Post(_ =>  GetCustomWindow(true).OnTaskFinishedOrCanceled(), null);
    }
    

    Debug.Log()仅用于日志 - 更容易调试。使用上下文,我们获得了窗口的链接并在其上调用必要的方法。

    任务结束的神秘方法是释放资源和标志:

    private void OnTaskFinishedOrCanceled() {
        _taskRunning = false;
        _cts.Dispose();
        _cts = null;
    }
    

    调用此类任务或取消它们并没有什么特别之处:

    private void OnGUI() {
        ProgressBar();
        if (_taskRunning) {
            if (GUILayout.Button("Cancel task")) {
                _cts.Cancel();
                OnTaskFinishedOrCanceled();
            }
        }
        else {
            _sleepTaskCount = EditorGUILayout.IntField("Sleeping tasks count:", _sleepTaskCount);
            _sleepTaskTime = EditorGUILayout.IntField("Sleeping time (ms):", _sleepTaskTime);
            if (GUILayout.Button("Start single task") && _sleepTaskCount > 0) {
                _progress = 0f;
                _taskRunning = true;
                
                _cts = new CancellationTokenSource();
                var token = _cts.Token;
                var context = SynchronizationContext.Current;
                Task.Run(() => DumbTaskExample(_sleepTaskCount, _sleepTaskTime, context, token), token);
            }
        
        }
    }
    
    
    private void ProgressBar() {
        var size = position.size;
        var fullRect = GUILayoutUtility.GetRect(size.x, 30);
        var completedRect = new Rect(fullRect.x, fullRect.y, fullRect.width * _progress, fullRect.height);
        
        EditorGUI.DrawRect(fullRect, Color.black);
        EditorGUI.DrawRect(completedRect, Color.Lerp(Color.red, Color.green, _progress));
        
        EditorGUI.LabelField(fullRect, $"{_progress * 100}%", EditorStyles.centeredGreyMiniLabel);
    }
    

    我们得到结果:

    在此处输入图像描述


    另外,不要忘记任务链。例如,如果您需要执行多个顺序任务,但每个任务的结果取决于前一个任务,您需要检查所有阶段结果的可靠性。好吧,或者某些阶段很容易引发异常。

    编写异常处理程序:

    private static void HandleTaskException(Task task) {
        Exception ex = task.Exception;
        while (ex is AggregateException && ex.InnerException != null)
            ex = ex.InnerException;
        EditorUtility.DisplayDialog("Task chain terminated", $"Exception: {ex.Message}", "Ok");
    }
    

    我们创建一个链:

    Task.Run(() => DumbTaskExample(_sleepTaskCount, _sleepTaskTime, context, token), token)
        .ContinueWith(
            t => { HandleTaskException(t); OnTaskFinishedOrCanceled(); }, 
            token, 
            TaskContinuationOptions.OnlyOnFaulted, 
            TaskScheduler.FromCurrentSynchronizationContext()
        );
    

    这里指定当前上下文的调度器非常重要,否则延续将在与任务相同的地方执行 - 在主 Unity 线程之外。

    从这一切我们得到一个不是很漂亮,但阻塞了整个 UI Unity 对话框:

    在此处输入图像描述


    完整代码

    为了剧透,代码被放在一个容器中

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using UnityEditor;
    using UnityEngine;
    
    public class CustomWindow : EditorWindow {
        [MenuItem("Window/Task example")]
        private static void ShowWindow() {
            GetCustomWindow(true).Show();
        }
        
        private static CustomWindow GetCustomWindow(bool focus) {
            return GetWindow<CustomWindow>("Task example", focus);
        }
        
        private float _progress = 0f;
        private float Progress {
            set {
                _progress = value;
                Repaint();
            }
        }
        
        private bool _taskRunning;
    
        private int _sleepTaskCount = 3;
        private int _sleepTaskTime = 1000;
    
        private CancellationTokenSource _cts;
    
        private void OnGUI() {
            ProgressBar();
            if (_taskRunning) {
                if (GUILayout.Button("Cancel task")) {
                    _cts.Cancel();
                    OnTaskFinishedOrCanceled();
                }
            }
            else {
                _sleepTaskCount = EditorGUILayout.IntField("Sleeping sub-tasks count:", _sleepTaskCount);
                _sleepTaskTime = EditorGUILayout.IntField("sleeping time (ms):", _sleepTaskTime);
                if (GUILayout.Button("Start single task") && _sleepTaskCount > 0) {
                    _progress = 0f;
                    _taskRunning = true;
                    
                    _cts = new CancellationTokenSource();
                    var token = _cts.Token;
                    var context = SynchronizationContext.Current;
                    Task.Run(() => DumbTaskExample(_sleepTaskCount, _sleepTaskTime, context, token), token);
                }
                else if (GUILayout.Button("Start chained task") && _sleepTaskCount > 0) {
                    _progress = 0f;
                    _taskRunning = true;
                    
                    _cts = new CancellationTokenSource();
                    var token = _cts.Token;
                    var context = SynchronizationContext.Current;
                    Task.Run(() => DumbTaskExample(_sleepTaskCount, _sleepTaskTime, context, token), token)
                        .ContinueWith(
                            t => { HandleTaskException(t); OnTaskFinishedOrCanceled(); }, 
                            token, 
                            TaskContinuationOptions.OnlyOnFaulted, 
                            TaskScheduler.FromCurrentSynchronizationContext()
                        );
                }
            }
        }
    
        private void ProgressBar() {
            var size = position.size;
            var fullRect = GUILayoutUtility.GetRect(size.x, 30);
            var completedRect = new Rect(fullRect.x, fullRect.y, fullRect.width * _progress, fullRect.height);
            
            EditorGUI.DrawRect(fullRect, Color.black);
            EditorGUI.DrawRect(completedRect, Color.Lerp(Color.red, Color.green, _progress));
            
            EditorGUI.LabelField(fullRect, $"{_progress * 100}%", EditorStyles.centeredGreyMiniLabel);
        }
        
        private void OnTaskFinishedOrCanceled() {
            _taskRunning = false;
            _cts.Dispose();
            _cts = null;
        }
    
        private static void DumbTaskExample(int subTaskCount, int sleepTime, SynchronizationContext context, CancellationToken token) {
            Debug.Log($"Task started at {Thread.CurrentThread.ManagedThreadId}");
            for (int i = 0; i < subTaskCount; i++) {
                // Cancellation
                token.ThrowIfCancellationRequested();
                
                // Work
                Thread.Sleep(sleepTime);
                
                // Notification
                context.Post(_ => GetCustomWindow(false).Progress = (float)i/subTaskCount, null);
            }
            Debug.Log($"Task done at {Thread.CurrentThread.ManagedThreadId}");
            context.Post(_ =>  GetCustomWindow(true).OnTaskFinishedOrCanceled(), null);
        }
    
        private static void HandleTaskException(Task task) {
            if (task.IsFaulted) {
                Exception ex = task.Exception;
                while (ex is AggregateException && ex.InnerException != null)
                    ex = ex.InnerException;
                EditorUtility.DisplayDialog("Task chain terminated", $"Exception: {ex.Message}", "Ok");
            }
        }
    }


    结果

    我们有一种非常容易编写的方法来创建任务,甚至是带有异常处理的任务链。您还可以在此处附加取消令牌,处理这些情况,处理窗口关闭情况等。

    即使没有任何微妙之处,答案也很麻烦,在此基础上,您已经可以从上面附加各种小工具 - 会有一个愿望:)

    • 8
  2. Ivan Triumphov
    2020-03-02T19:22:17Z2020-03-02T19:22:17Z

    要在任务中调用统一 API,我使用Dispatcher或在此答案中重做类似 DumbTaskExample 的方法:

    async private static void DumbTaskExample (int subTaskCount, int sleepTime, SynchronizationContext context, CancellationToken token) {
        Debug.Log ($"Task started at {Thread.CurrentThread.ManagedThreadId}");
        for (int i = 0; i < subTaskCount; i++) {
            // Cancellation
            token.ThrowIfCancellationRequested ();
    
            // Work
            Thread.Sleep (sleepTime);
            string path = "none";
            Debug.Log ("path=" + path);
            path = await Task.Run (() => {
                bool val = false;
                Dispatcher.Instance.Invoke (() => {
                    Texture2D tempTex2D = Resources.Load<Texture2D> ("Texture/Rock (Moss)");
                    path = AssetDatabase.GetAssetPath (tempTex2D);
                    Debug.Log ("Dispatcher.path=" + path);
                    val = true;
                });
                while (val == false) {
                    Debug.Log ("val");
                }
                return path;
            });
            Debug.Log ("path=" + path);
    
            // Notification
            context.Post (_ => GetCustomWindow (false).Progress = (float) i / subTaskCount, null);
    
            //throw new Exception("Dumb exception");
        }
        Debug.Log ($"Task done at {Thread.CurrentThread.ManagedThreadId}");
        context.Post (_ => GetCustomWindow (true).OnTaskFinishedOrCanceled (), null);
    }
    

    不要忘记添加到 OnGUI():

     private void OnGUI() {
      ...
      Dispatcher.Instance.InvokePending ();
     }
    

    结论:

    在此处输入图像描述

    • 0

相关问题

  • 如何知道类中的方法是否属于接口?

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    如何从列表中打印最大元素(str 类型)的长度?

    • 2 个回答
  • Marko Smith

    如何在 PyQT5 中清除 QFrame 的内容

    • 1 个回答
  • Marko Smith

    如何将具有特定字符的字符串拆分为两个不同的列表?

    • 2 个回答
  • Marko Smith

    导航栏活动元素

    • 1 个回答
  • Marko Smith

    是否可以将文本放入数组中?[关闭]

    • 1 个回答
  • Marko Smith

    如何一次用多个分隔符拆分字符串?

    • 1 个回答
  • Marko Smith

    如何通过 ClassPath 创建 InputStream?

    • 2 个回答
  • Marko Smith

    在一个查询中连接多个表

    • 1 个回答
  • Marko Smith

    对列表列表中的所有值求和

    • 3 个回答
  • Marko Smith

    如何对齐 string.Format 中的列?

    • 1 个回答
  • Martin Hope
    Alexandr_TT 2020年新年大赛! 2020-12-20 18:20:21 +0000 UTC
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5