You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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后问题依旧。

问题根源

  1. UnityWebRequest主线程限制:UnityWebRequest的初始化操作必须在主线程执行,即使未直接调用RandomRange,ChainFunctions内部创建UnityWebRequest或相关对象时,可能间接触发了依赖主线程的Unity内部API(如某些初始化逻辑用到Random)。
  2. 任务创建时机错误:在后台线程调用ChainFunctions.OwnedTokenIDs()等方法时,这些方法可能立即执行了需要主线程的初始化代码,而非仅创建待执行的任务。
  3. 线程安全隐患: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 08:01:30