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

Unity Jobs多帧拆分执行失效问题求助

Unity Jobs/Burst 调度后未跨帧执行,单帧完成导致性能峰值

问题描述

我有一个需要密集计算的Unity脚本,使用Jobs和Burst实现后台多线程计算,核心需求是将工作拆分到多帧执行,避免单帧性能峰值或阻塞主线程。但实际效果不符合预期:调度的Job本该耗时较长跨多帧完成,却总是在调度的同一帧强制执行完毕。

当前有两个存在依赖关系的Job,第二个Job需要第一个Job的输出数据。代码逻辑为:

  1. 调度第一个Job后,下一帧通过IsCompleted()检查是否完成,完成后调用Complete(),准备数据并调度第二个Job;
  2. 同样在下一帧检查第二个Job的IsCompleted状态,完成后调用Complete()并使用结果。

UpdateGenerationState方法每帧在Update中调用,代码如下:

private void UpdateGenerationState()
{
    if (_generationState == GenerationState.Idle)
    {
        FindChunksToRegenerate();
    }
    
    int chunksCount = _chunksToRegenerate.Count;
    
    switch (_generationState)
    {
        case GenerationState.Idle:
            if (chunksCount > 0)
            {
                //Swap arrays: _currentChunks = _nextActiveChunks
                var temp = _currentChunks;
                _currentChunks = _nextActiveChunks;
                _nextActiveChunks = temp;
                
                ScheduleScalarFieldJob(chunksCount);
                _generationState = GenerationState.ScalarFieldGeneration;
            }
            break;
        
        case GenerationState.ScalarFieldGeneration:
            if (_scalarFieldHandle.IsCompleted)
            {
                _scalarFieldHandle.Complete();
                //Schedule job2
                ScheduleGenerateMeshesJob(chunksCount);
                _generationState = GenerationState.ChunksMeshesGeneration;
            }
            break;
        
        case GenerationState.ChunksMeshesGeneration:
            if (_generateMeshesHandle.IsCompleted)
            {
                _generateMeshesHandle.Complete();
                //Apply meshes
                CreateChunksMeshes(chunksCount);
                Dispose();
                
                _chunksToRegenerate.Clear();
                _generationState = GenerationState.Idle;
            }
            break;
    }
}

private void ScheduleScalarFieldJob(int chunksCount)
{
    _chunks = new NativeArray<Chunk>(_chunksToRegenerate.ToArray(), Allocator.Persistent);
    _scalarField = new NativeArray<ScalarPoint>(chunksCount * ChunkScalarSize, Allocator.Persistent);
    _cubeConfigurations = new NativeArray<int>(chunksCount * ChunkScalarSize, Allocator.Persistent);
    _trianglesCountPerChunk = new NativeArray<int>(chunksCount, Allocator.Persistent);

    var parameters = (
        chunks: _chunks,
        scalarField: _scalarField,
        cubeConfigurations: _cubeConfigurations,
        trianglesCountPerChunk: _trianglesCountPerChunk
    );
    
    CalculateScalarField job = new CalculateScalarField(
        parameters,
        triangulationTable: Universe.Instance.triangulationTable,
        scalarFieldPositions: Universe.Instance.scalarFieldPositions
    );
   
    //_scalarFieldHandle = job.ScheduleByRef(chunksCount, 1);
    _scalarFieldHandle = job.Schedule(chunksCount, 1);
    JobHandle.ScheduleBatchedJobs();
}

private void ScheduleGenerateMeshesJob(int chunksCount)
{
    _chunksTrianglesOffset = new NativeArray<int>(chunksCount, Allocator.Persistent);
    int trianglesCount = 0;
    for (int i = 0; i < chunksCount; i++)
    {
        _chunksTrianglesOffset[i] = trianglesCount;
        trianglesCount += _trianglesCountPerChunk[i];
    }
    
    _triangles = new NativeArray<float3>(trianglesCount, Allocator.Persistent);
    
    var parameters = (
        chunks: _chunks,
        scalarField: _scalarField,
        cubeConfigurations: _cubeConfigurations,
        trianglesCountPerChunk: _trianglesCountPerChunk,
        chunksTrianglesOffset: _chunksTrianglesOffset,
        triangles: _triangles
    );

    
    GenerateChunksMeshes job = new GenerateChunksMeshes(
        parameters,
        triangulationTable: Universe.Instance.triangulationTable,
        edgeToVertex: Universe.Instance.edgeToVertex,
        cornerPositions: Universe.Instance.cornerPositions,
        cubePositions: Universe.Instance.cubePositions
    );
    
    _generateMeshesHandle = job.Schedule(chunksCount, 1);
    JobHandle.ScheduleBatchedJobs();
}

Profiler显示Job占用了单帧的大量时间,并未延续到后续帧。

原因分析

  1. Batch Size设置过小:调用Schedule(chunksCount, 1)时,第二个参数是batch size(批处理大小),设置为1意味着每1个任务就分配一个工作单元。当chunksCount不大时,Job会被快速执行完毕,直接在当前帧完成。
  2. 无帧间执行节奏控制:Unity Jobs系统默认会全速完成所有调度任务,只要主线程没有等待操作,后台线程会立刻跑完任务,不会自动拆分到多帧。你的代码只是被动等待Job完成,没有限制每帧允许执行的工作量。
  3. ScheduleBatchedJobs()调用时机不当:单个Job调度后立即调用该方法,会强制Unity立刻调度所有待执行Job,没有给系统留缓冲时间,加速了Job的完成速度。

解决方案

1. 合理设置Batch Size

调整Schedule方法的batch size参数,根据计算量和目标帧率设置合适值。比如将chunksCount拆分为多个批次,让Job更易跨帧执行:

// 示例:动态计算批次大小,每帧最多处理20个chunk
int batchSize = Mathf.Clamp(chunksCount / 5, 10, 50);
_scalarFieldHandle = job.Schedule(chunksCount, batchSize);

2. 分帧调度任务

不要一次性调度所有任务,而是分多帧逐步处理。比如每帧只调度固定数量的chunk计算:

// 修改状态机,增加分帧逻辑
private int _processedChunks;

case GenerationState.ScalarFieldGeneration:
    if (_scalarFieldHandle.IsCompleted)
    {
        _scalarFieldHandle.Complete();
        _processedChunks = 0;
        _generationState = GenerationState.ChunksMeshesGeneration;
    }
    break;

case GenerationState.ChunksMeshesGeneration:
    if (_processedChunks < chunksCount)
    {
        // 每帧处理20个chunk
        int takeCount = Mathf.Min(20, chunksCount - _processedChunks);
        // 调度对应批次的Job
        var batchHandle = ScheduleBatchGenerateMeshesJob(_processedChunks, takeCount);
        _processedChunks += takeCount;
        // 可以等待下一帧再检查该批次是否完成
    }
    else
    {
        CreateChunksMeshes(chunksCount);
        Dispose();
        _chunksToRegenerate.Clear();
        _generationState = GenerationState.Idle;
    }
    break;

3. 利用Job依赖自动管理执行节奏

让第二个Job依赖第一个Job的Handle,Unity会自动处理执行顺序,主线程无需主动等待,直到需要数据时再调用Complete():

// 调度第二个Job时传入第一个Job的Handle作为依赖
_generateMeshesHandle = job.Schedule(chunksCount, batchSize, _scalarFieldHandle);

4. 移除不必要的ScheduleBatchedJobs()

该方法会强制Unity立即调度所有待执行Job,通常无需在单个Job调度后调用,Unity会在主线程合适时机自动批量调度,保留它会让Job更快完成,不利于跨帧拆分。


内容的提问来源于stack exchange,提问作者Light

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.11 16:05:09