多執行緒下的網格生成及效能分析

JimmyZou發表於2024-07-03

前言

概述

  • 透過多執行緒方式實現上千個物件的網格生成,並觀察執行效率。

  • 多執行緒透過Thread來進行,每個執行緒中執行GenerateMeshData方法,在方法中對不同種類的網格進行頂點和三角面序列的計算。首先設定簡單立方體,之後改為柏林噪聲下生成的複雜地形

主執行緒限制

Unity設計之初就是依靠單執行緒執行所有物件的生命週期的,所以有些地方無法支援多執行緒,此處大致進行舉例:

  • Unity API呼叫:許多Unity的功能和API只能在主執行緒上呼叫,例如例項化、銷燬、修改遊戲物件、修改元件屬性等。

  • 渲染相關操作:與渲染相關的操作,例如修改材質、設定渲染目標、更新紋理等,通常需要在主執行緒上執行。

  • 使用者介面操作:與使用者介面相關的操作,例如處理輸入事件、更新UI元素等,也需要在主執行緒上執行。

因為這篇文章中,需要對網格進行生成,所以會透過mesh.vertices和mesh.triangles進行賦值,然而mesh的獲取也是必須在主執行緒下進行的,所以每個執行緒中執行的GenerateMeshData方法實際上是計算所有頂點,存放到公共陣列中。最後到主執行緒中,再對每個網格進行點和三角形的賦值,並對所有的網格進行合批,透過一個MeshRenderer來顯示(見實現思路圖)

執行效果

實現過程

實現思路

完整程式碼

using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using UnityEngine;

public class MeshGeneratorMultiThreading : MonoBehaviour
{
    public int numThreads = 4;

    private List<Thread> threads;
    private List<Mesh> meshes;
    private MeshFilter meshFilter;
    private List<Vector3[]> verticesList;
    private List<int[]> trianglesList;

    private Stopwatch stopwatch;
    private string elapsedTime;
    private void Start()
    {

        stopwatch = new Stopwatch();
        stopwatch.Start();

        threads = new List<Thread>(numThreads);
        meshes = new List<Mesh>(numThreads);
        verticesList = new List<Vector3[]>(numThreads);
        trianglesList = new List<int[]>(numThreads);

        meshFilter = GetComponent<MeshFilter>();
        meshFilter.mesh = new Mesh();

        // 建立多個執行緒,開始計算生成網格資料
        for (int i = 0; i < numThreads; i++)
        {
            meshes.Add(new Mesh());

            int threadIndex = i;
            Thread thread = new Thread(() => { GenerateMeshData(threadIndex); });
            threads.Add(thread);
            thread.Start();
        }

        // 阻塞等待所有的網格資料計算完成
        foreach (Thread thread in threads)
        {
            thread.Join();
        }

        // 計算完成後應用於所有網格
        for (int i = 0; i < numThreads; i++)
        {
            meshes[i].vertices = verticesList[i];
            meshes[i].triangles = trianglesList[i];
        }
        // 合併
        CombineMeshes();

        stopwatch.Stop();
        elapsedTime = $"Elapsed time: {stopwatch.Elapsed.TotalSeconds} seconds";
    }

    private void GenerateMeshData(int threadIndex)
    {
        //生成立方體網格
        var (vs, ts) = GenerateCubeMeshData(threadIndex);

        // 將生成的網格資料新增到列表中
        lock (verticesList)
        {
            verticesList.Add(vs);
            trianglesList.Add(ts);
        }
    }

    private (Vector3[] vertices, int[] triangles) GenerateCubeMeshData(int threadIndex)
    {
        Vector3[] vertices = new Vector3[8];
        vertices[0] = new Vector3(-1, -1, -1);
        vertices[1] = new Vector3(1, -1, -1);
        vertices[2] = new Vector3(1, 1, -1);
        vertices[3] = new Vector3(-1, 1, -1);
        vertices[4] = new Vector3(-1, -1, 1);
        vertices[5] = new Vector3(1, -1, 1);
        vertices[6] = new Vector3(1, 1, 1);
        vertices[7] = new Vector3(-1, 1, 1);

        int sqrThreadNum = (int)Mathf.Sqrt(numThreads);
        for (var i = 0; i < vertices.Length; i++)
        {
            vertices[i] += new Vector3((threadIndex / sqrThreadNum) * 3, (threadIndex % sqrThreadNum) * 3, 0);
        }

        int[] triangles = new int[36]
        {
        0, 2, 1, 0, 3, 2, // Front face
        1, 2, 5, 5, 2, 6, // Right face
        4, 5, 6, 4, 6, 7, // Back face
        0, 7, 3, 0, 4, 7, // Left face
        0, 5, 4, 0, 1, 5, // Bottom face
        2, 3, 6, 6, 3, 7  // Top face
        };

        return (vertices, triangles);
    }


    private void CombineMeshes()
    {
        // 合併所有生成的網格
        CombineInstance[] combine = new CombineInstance[meshes.Count];

        for (int i = 0; i < meshes.Count; i++)
        {
            combine[i].mesh = meshes[i];
            combine[i].transform = Matrix4x4.identity;
        }
        meshFilter.mesh.CombineMeshes(combine, true, true);
    }


    private void OnGUI()
    {
        GUI.Label(new Rect(10, 10, 500, 20), elapsedTime);
    }
}

效能對比

透過StopWatch記錄執行時間,分為多種對比情況:

(1)一個執行緒只繪製一個立方體,總共繪製6400個立方

多執行緒下,花費1.3s; 單執行緒下花費0.03s。反而多執行緒花費的更多了,因為開了6400個執行緒,執行緒開啟的開銷遠遠大於每個執行緒計算所花費的開銷。

(2)一個執行緒只繪製一片地形(1000*1000 Unit),總共繪製100個地形

多執行緒下,花費3.4s; 單執行緒下花費8.9s。這裡可以看出差距,但是差距只有常數倍

這是正常的,原因分析:

理想情況下,N核CPU在多執行緒下的效率應該是 1/N,N本身就是常數,所以差距是常數倍也是正常的。
但是我的電腦是8核的,為什麼結果比8要小很多。因為通常情況下,執行緒數都是大於CPU核心數的,那一個核心就會(併發)處理多個執行緒,每個執行緒在切換的時候就要同時儲存載入上下文,這是主要耗時的地方。

注:由於Unity的一個Mesh最多隻支援65000個頂點,所以(2)未對網格進行設定、合批和渲染。

其他

MainThreadDispatcher(在子執行緒中將方法傳送到主執行緒執行)

概述

編寫過程中,寫了一些其他的指令碼,最終雖未用到,但是還是記錄下來。MainThreadDispatcher主要是用於在子執行緒中能夠方便呼叫主執行緒程式碼的(Unity API等)。

但是,都知道子執行緒是無法呼叫大多Unity API的,所以只能尋求別的方法。這裡就是將這些方法作為委託物件存放到一個佇列中,等待MainThreadDispatcher的主執行緒的Update生命週期時,對這些委託進行執行。

完整程式碼

using System.Collections.Generic;
using System.Threading;
using UnityEngine;

namespace JimDevPack.Common.MultiThread
{
    public class MainThreadDispatcher : MonoBehaviour
    {
        private static MainThreadDispatcher instance;
        private static readonly Queue<System.Action> actions = new Queue<System.Action>();

        // 執行開始時,就執行的方法
        [RuntimeInitializeOnLoadMethod]
        private static void Initialize()
        {
            if (instance == null)
            {
                GameObject dispatcherObject = new GameObject("MainThreadDispatcher");
                instance = dispatcherObject.AddComponent<MainThreadDispatcher>();
                DontDestroyOnLoad(dispatcherObject);
            }
        }

        void Update()
        {
            while (actions.Count > 0)
            {
                actions.Dequeue()?.Invoke();
            }
        }

        public static void RunOnMainThread(System.Action action)
        {
            if (action == null)
            {
                throw new System.ArgumentNullException(nameof(action));
            }

            actions.Enqueue(action);
        }
    }
}

Task和Thread使用場景

https://mp.weixin.qq.com/s?__biz=MzI0MTU0ODQwMQ==&mid=2247485189&idx=1&sn=4f53f980da4de559c3bf7903b998bb0a&chksm=e908aa1bde7f230d59a2dba625123069c140eeac155791bcd7a093f68f4135e82afd133f837e&scene=27

相關文章