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 个回答 Voted 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 个选项: Singleton 是经典之作,但转向它寻求解决方案已不再有趣 :) 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"); } } } 结果 我们有一种非常容易编写的方法来创建任务,甚至是带有异常处理的任务链。您还可以在此处附加取消令牌,处理这些情况,处理窗口关闭情况等。 即使没有任何微妙之处,答案也很麻烦,在此基础上,您已经可以从上面附加各种小工具 - 会有一个愿望:) 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 (); } 结论:
您可以,但有一个例外 - Unity 禁止在其主线程之外调用其大部分 API。
顺便说一下,可以在子线程中调用 Debug 类及其方法。
“运行时”中有什么,编辑器中有什么,Unity 有自己的 SynchronizationContext 实现 - UnitySynchronizationContext,除此之外,我们原则上不需要任何东西。
一个微不足道的任务被简单地解决了:
反正一切都很清楚,问题是在那之后返回主线程,此外,将一些东西转移到某个窗口。
好吧,我们没有很多选择,而确切地说是 1 -
EditorWindow.GetWindow<T>它要么创建一个窗口,要么恢复一个保存的窗口,或者,充其量,专注于一个打开的窗口。对于后台任务,我希望窗口在那里更新一些东西,但用户不会被猛拉,也不会从活动窗口中获得焦点。在这里,我们有 2 个选项:
GetWindow<T>("Title", focus: false)- 你将不得不重新传输窗口的标题,但你能做什么。让我们在第二个选项上停下来,得到这样的东西:
那么一切都是微不足道的:
在我们的窗口中,我们有一些“进步”的衡量标准:
我们在任务中做重要的事情——我们睡觉:
Debug.Log()仅用于日志 - 更容易调试。使用上下文,我们获得了窗口的链接并在其上调用必要的方法。任务结束的神秘方法是释放资源和标志:
调用此类任务或取消它们并没有什么特别之处:
我们得到结果:
另外,不要忘记任务链。例如,如果您需要执行多个顺序任务,但每个任务的结果取决于前一个任务,您需要检查所有阶段结果的可靠性。好吧,或者某些阶段很容易引发异常。
编写异常处理程序:
我们创建一个链:
这里指定当前上下文的调度器非常重要,否则延续将在与任务相同的地方执行 - 在主 Unity 线程之外。
从这一切我们得到一个不是很漂亮,但阻塞了整个 UI Unity 对话框:
完整代码
为了剧透,代码被放在一个容器中
结果
我们有一种非常容易编写的方法来创建任务,甚至是带有异常处理的任务链。您还可以在此处附加取消令牌,处理这些情况,处理窗口关闭情况等。
即使没有任何微妙之处,答案也很麻烦,在此基础上,您已经可以从上面附加各种小工具 - 会有一个愿望:)
要在任务中调用统一 API,我使用Dispatcher或在此答案中重做类似 DumbTaskExample 的方法:
不要忘记添加到 OnGUI():
结论: