Unity中C#静态ActionManager类异常行为求助
问题描述
在Unity项目中实现了静态类ActionManager,用于按顺序执行PlayableAction数组中的动作:部分动作需等待完成后再执行下一个,部分无需等待直接执行下一个。但出现偶发异常:当waitForCompletion为true的动作执行时,if和else分支的日志均会打印,Dequeue仅调用一次,且该问题仅在动作的Act()方法包含协程时出现。
相关代码片段
if (actionQueue.Count > 0) { isActionPlaying = true; Debug.Log("Dequeue"); _currentAction = actionQueue.Dequeue(); if (_currentAction.waitForCompletion) { _currentAction.onActionCompleteEvent.AddListener(HandleActionCompleted); Logs.Print("Playing Action " + _currentAction.Name + " waiting for completion!. For the Query: " + _currentAction.Query); _currentAction.Act(); Logs.Print("Act called on current action " + _currentAction.Name); } else { Logs.Print("Playing Action " + _currentAction.Name + " NOT waiting for completion!"); _currentAction.Act(); HandleActionCompleted(); Logs.Print("Act called on current action " + _currentAction.Name + " NOT waiting for completion!. For the Query: " + _currentAction.Query); } }
完整类代码
using System.Collections; using System.Collections.Generic; using System.Runtime.InteropServices; using UnityEngine; using UnityEngine.Events; using VRseBuilder.Core.Utility; namespace VRseBuilder.Core.NoCode.Utility { public static class ActionManager { private static Queue<PlayableAction> actionQueue = new Queue<PlayableAction>(); private static bool isActionPlaying = false; private static PlayableAction _currentAction = null; public static UnityEvent onPlayableActionSequenceCompleted = new UnityEvent(); // Method to add actions to the queue and start the sequence if not already started public static void PlayActionsInSequence(PlayableAction[] actions) { foreach (var action in actions) { actionQueue.Enqueue(action); Logs.Print("Adding to the current queue is:" + action.Name); } Logs.Print("The current length of action queue after clearing is :" + actionQueue.Count); if (!isActionPlaying) { PlayNextAction(); } } // Plays the next action in the queue private static void PlayNextAction() { if (actionQueue.Count > 0) { isActionPlaying = true; _currentAction = actionQueue.Dequeue(); if (_currentAction.waitForCompletion) { _currentAction.onActionCompleteEvent.AddListener(HandleActionCompleted); Logs.Print("Playing Action " + _currentAction.Name + " waiting for completion!. For the Query: " + _currentAction.Query); _currentAction.Act(); Logs.Print("Act called on current action " + _currentAction.Name); } else { Logs.Print("Playing Action " + _currentAction.Name + " NOT waiting for completion!"); _currentAction.Act(); HandleActionCompleted(); Logs.Print("Act called on current action " + _currentAction.Name + " NOT waiting for completion!. For the Query: " + _currentAction.Query); } } else { isActionPlaying = false; } } // Handles the completion of an action private static void HandleActionCompleted() { Logs.Print("Entered Handle Action Completed the count for actionQueue is" + actionQueue.Count); _currentAction.onActionCompleteEvent.RemoveListener(HandleActionCompleted); isActionPlaying = false; if (actionQueue.Count <= 0) { Logs.Print("All the actions in the queue played!"); onPlayableActionSequenceCompleted?.Invoke(); return; } if (actionQueue.Peek() != null) { PlayNextAction();// Unsubscribe to prevent memory leaks } // Play the next action in the sequence } // Adds an array of PlayableActions to the queue and starts the sequence if not already playing public static void AddActionsAndPlay(PlayableAction[] actions) { foreach (var action in actions) { actionQueue.Enqueue(action); } if (!isActionPlaying) { PlayNextAction(); } } // CLears and then adds an array of PlayableActions to the queue and starts the sequence if not already playing public static void ClearPreviousAndPlayActionSet(PlayableAction[] actions) { if (_currentAction != null && isActionPlaying) { _currentAction.SkipAction(); _currentAction.onActionCompleteEvent.RemoveListener(HandleActionCompleted); } //_currentAction.SkipAction(); actionQueue.Clear(); isActionPlaying = false; PlayActionsInSequence(actions); } // Adds a single PlayableAction to the queue public static void AddAction(PlayableAction action) { actionQueue.Enqueue(action); if (!isActionPlaying) { PlayNextAction(); } } // Skips the current action without automatically proceeding to the next action in the queue public static void SkipCurrentAction(bool proceedToNext = true) { Debug.Log("Skipping Current Action"); if (isActionPlaying) { // Assuming currentAction represents the currently playing action _currentAction?.SkipAction(); _currentAction.onActionCompleteEvent.RemoveListener(HandleActionCompleted); // Unsubscribe isActionPlaying = false; // Stop the playback sequence if (proceedToNext) { PlayNextAction(); // Proceed to next action only if specified } } } public static void PlayActionImmediately(PlayableAction action) { // Clear the queue of any pending actions actionQueue.Clear(); // Unsubscribe from the current action's completion event and stop it if it's playing if (isActionPlaying) { // Assuming there's a way to get the currently playing action // This could be tracked by updating a currentAction variable in the PlayNextAction method // For simplicity in this example, we'll just skip the current action SkipCurrentAction(); // Note: Adjust SkipCurrentAction to not automatically play the next action in the queue } // Reset the playing flag as we're manually handling the next action isActionPlaying = false; // Add the new action to the queue and play it AddAction(action); } } }
问题原因分析
静态类竞态条件:
ActionManager是静态类,isActionPlaying、_currentAction等字段无线程同步保护。当协程的完成回调触发时,可能和主线程的PlayNextAction逻辑冲突:主线程刚进入waitForCompletion分支,回调就将isActionPlaying设为false,此时若有其他操作触发PlayNextAction,会导致同一_currentAction被重复判断,双分支日志打印。协程回调时序问题:若
Act()中的协程在同一帧内就触发onActionCompleteEvent,会导致HandleActionCompleted在PlayNextAction的waitForCompletion分支代码执行完前被调用,进而再次触发PlayNextAction,但队列中该动作已被Dequeue取出,所以只打印一次Dequeue日志。事件订阅时序混乱:竞态下可能出现事件重复订阅或订阅未完成就取消的情况,导致
HandleActionCompleted被多次调用,进一步加剧逻辑混乱。
修复方案
添加线程同步锁:对静态字段的访问和修改加锁,确保同一时间只有一个线程执行关键逻辑:
private static readonly object _lockObj = new object(); // 在PlayNextAction、HandleActionCompleted等方法的关键代码块外层加锁 lock(_lockObj) { // 原逻辑代码,如判断isActionPlaying、操作队列、修改_currentAction等 }强制主线程执行事件回调:Unity的
UnityEvent默认在主线程触发,但如果协程回调来自其他线程,需通过主线程调度器(如自行实现的UnityMainThreadDispatcher)触发onActionCompleteEvent,避免跨线程操作导致的状态异常。优化状态校验逻辑:在
HandleActionCompleted中先判断_currentAction是否为空再取消订阅,避免空引用异常;在PlayNextAction中获取动作后再次确认waitForCompletion状态,防止竞态下状态被篡改。标记动作执行状态:给
PlayableAction添加IsPlaying字段,在PlayNextAction执行前校验,避免同一动作被重复处理。
内容的提问来源于stack exchange,提问作者Epiczzor

