Unity中Task.Run后台创建任务仍触发主线程调用错误求助
问题解决:Unity后台线程任务引发主线程API错误及CPU峰值优化
问题背景
Unity版本:2022.1.18f1
为避免主线程调用CreateTaskList()时的严重CPU峰值(Profiler显示主线程出现明显性能尖峰),采用Task.Run在后台线程创建任务列表,每5秒通过InvokeRepeating触发更新。但在主线程Update()中调用Task.WhenAll等待任务完成时,触发错误:
UnityEngine.UnityException: RandomRangeInt只能从主线程调用。场景加载时构造函数和字段初始化器将在加载线程执行,请勿在构造函数或字段初始化器中使用此函数,应将初始化代码移至Awake或Start函数。
任务代码仅包含WWWForm和UnityWebRequest,未直接使用RandomRange,尝试PimDeWitte的MainThreadDispatcher后问题依旧。
问题根源
- UnityWebRequest主线程限制:UnityWebRequest的初始化操作必须在主线程执行,即使未直接调用RandomRange,
ChainFunctions内部创建UnityWebRequest或相关对象时,可能间接触发了依赖主线程的Unity内部API(如某些初始化逻辑用到Random)。 - 任务创建时机错误:在后台线程调用
ChainFunctions.OwnedTokenIDs()等方法时,这些方法可能立即执行了需要主线程的初始化代码,而非仅创建待执行的任务。 - 线程安全隐患:
chainDataTaskList和tasksReadyToConsume未做线程同步,多线程访问可能导致逻辑异常。
解决方案
1. 确保Unity API仅在主线程初始化
重构ChainFunctions,将UnityWebRequest的创建移到主线程,仅把请求发送后的等待、响应处理逻辑放在后台线程。示例:
// 重构后的ChainFunctions示例 public static async Task<List<int>> OwnedTokenIDs(string chain, string network, string contract, string account) { UnityWebRequest request = null; // 主线程初始化请求对象 await MainThreadDispatcher.Instance.Enqueue(() => { WWWForm form = new WWWForm(); // 填充请求参数 form.AddField("chain", chain); form.AddField("account", account); request = UnityWebRequest.Post("your-api-url", form); }); // 后台线程等待请求完成 await request.SendWebRequest().AsTask(); // 后台线程处理响应(无Unity API依赖) if (request.result != UnityWebRequest.Result.Success) { throw new Exception(request.error); } var result = JsonUtility.FromJson<List<int>>(request.downloadHandler.text); request.Dispose(); return result; }
2. 调整任务创建逻辑,分离主线程与后台工作
不再在后台线程创建任务列表,而是在主线程创建任务,仅将耗时计算逻辑用Task.Run包裹在ChainFunctions内部:
async Task UpdateData() { if (getDataLocked) return; getDataLocked = true; // 主线程创建任务列表(仅初始化请求,不执行耗时操作) chainDataTaskList = new List<Task>() { ChainFunctions.OwnedTokenIDs(s_chain, s_network, s_contract, s_account), ChainFunctions.GetStatBigInt("getCostForAddress", new string[] { s_account }), ChainFunctions.GetStatNoParams("totalSupply"), ChainFunctions.GetStatNoParams("maxSupply"), ChainFunctions.GetBoolNoParams("paused") }; tasksReadyToConsume = true; Debug.Log("Task List Creation Finished"); }
3. 修复线程同步问题
对共享变量加锁,避免多线程竞争:
private readonly object _lockObj = new object(); List<Task> chainDataTaskList; bool tasksReadyToConsume; private async void Update() { lock(_lockObj) { if(!tasksReadyToConsume || chainDataTaskList?.Count != 5) return; } await CompleteTasks(); lock(_lockObj) { tasksReadyToConsume = false; } Debug.Log("Tasks consumed"); } void CreateTaskList() { var newTasks = new List<Task>() { ChainFunctions.OwnedTokenIDs(s_chain, s_network, s_contract, s_account), ChainFunctions.GetStatBigInt("getCostForAddress", new string[] { s_account }), ChainFunctions.GetStatNoParams("totalSupply"), ChainFunctions.GetStatNoParams("maxSupply"), ChainFunctions.GetBoolNoParams("paused") }; lock(_lockObj) { chainDataTaskList = newTasks; tasksReadyToConsume = true; } Debug.Log("Task List Creation Finished"); }
4. 替代方案:Unity协程结合异步请求
如果Task的线程模型带来适配问题,可改用Unity协程处理异步请求,同时将耗时计算移到后台:
IEnumerator UpdateDataCoroutine() { if (getDataLocked) yield break; getDataLocked = true; var requests = new List<UnityWebRequest>(); // 主线程创建所有请求 requests.Add(CreateOwnedTokenIDsRequest()); requests.Add(CreateGetCostRequest()); requests.Add(CreateTotalSupplyRequest()); requests.Add(CreateMaxSupplyRequest()); requests.Add(CreatePausedRequest()); // 发送所有请求 foreach(var req in requests) { req.SendWebRequest(); } // 等待所有请求完成并处理 foreach(var req in requests) { yield return req; if(req.result == UnityWebRequest.Result.Success) { // 后台线程解析响应 await Task.Run(() => ParseResponse(req)); } req.Dispose(); } getDataLocked = false; }
关键注意事项
- 所有Unity API(包括UnityWebRequest创建、GameObject操作、Random类等)必须在主线程调用,仅纯计算、数据解析等无Unity依赖的逻辑可放后台线程。
- 多线程访问共享变量时必须加锁,避免竞态条件。
- 使用Unity的
AsTask()扩展(需引入UnityEngine.Networking和System.Threading.Tasks),可将AsyncOperation转换为Task,更方便结合异步编程。
内容的提问来源于stack exchange,提问作者Tony Steel
相关产品推荐
相关产品推荐

