จะไม่ตรึงเธรดหลักใน Unity ได้อย่างไร


33

ฉันมีอัลกอริธึมการสร้างระดับที่หนักมาก เช่นนี้การเรียกมันจะส่งผลให้หน้าจอเกมค้างอยู่เสมอ ฉันจะวางฟังก์ชันบนเธรดที่สองในขณะที่เกมยังคงทำการเรนเดอร์หน้าจอการโหลดเพื่อระบุว่าเกมนั้นไม่หยุดค้างอยู่?


1
คุณสามารถใช้ระบบเธรดเพื่อรันงานอื่นในเบื้องหลัง ... แต่พวกเขาอาจไม่เรียกอะไรเลยใน Unity API เนื่องจากสิ่งนั้นไม่ใช่ threadsafe เพียงแค่แสดงความคิดเห็นเพราะฉันยังไม่ได้ทำสิ่งนี้และไม่สามารถให้รหัสตัวอย่างแก่คุณได้อย่างรวดเร็ว
Almo

การใช้คำListสั่งเพื่อจัดเก็บฟังก์ชันที่คุณต้องการโทรในเธรดหลัก ล็อคและคัดลอกActionรายการในUpdateฟังก์ชั่นไปยังรายการชั่วคราวล้างรายการเดิมจากนั้นดำเนินการActionรหัสในListหัวข้อหลัก ดู UnityThread จากโพสต์อื่นของฉันเกี่ยวกับวิธีการทำเช่นนี้ ตัวอย่างเช่นเมื่อต้องการเรียกใช้ฟังก์ชันบนเธรดหลัก UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
โปรแกรมเมอร์

คำตอบ:


48

อัปเดต:ในปี 2018 Unity กำลังเปิดตัวระบบงาน C #เพื่อลดการทำงานและใช้ประโยชน์จากคอร์ CPU หลายตัว

คำตอบด้านล่างนี้มาก่อนระบบนี้ มันจะยังคงใช้งานได้ แต่อาจมีตัวเลือกที่ดีกว่าใน Unity สมัยใหม่ขึ้นอยู่กับความต้องการของคุณ โดยเฉพาะอย่างยิ่งระบบงานจะปรากฏขึ้นเพื่อแก้ไขข้อ จำกัด บางอย่างเกี่ยวกับสิ่งที่หัวข้อที่สร้างขึ้นด้วยตนเองสามารถเข้าถึงได้อย่างปลอดภัยอธิบายไว้ด้านล่าง นักพัฒนาที่ทดลองใช้รายงานตัวอย่างแสดงเรย์แคสต์และสร้างตาข่ายแบบขนานตัวอย่างเช่น

ฉันขอเชิญผู้ใช้ที่มีประสบการณ์ในการใช้ระบบงานนี้เพื่อเพิ่มคำตอบของตัวเองที่สะท้อนสถานะปัจจุบันของเครื่องยนต์


ฉันใช้เธรดสำหรับงานหนาใน Unity ในอดีต (โดยปกติแล้วการประมวลผลภาพและเรขาคณิต) และก็ไม่แตกต่างอย่างมากจากการใช้เธรดในแอปพลิเคชัน C # อื่น ๆ โดยมีสองประการ:

  1. เนื่องจาก Unity ใช้ชุดย่อยของ. NET ที่ค่อนข้างเก่ากว่ามีคุณลักษณะการทำเธรดและไลบรารีที่ใหม่กว่าที่เราไม่สามารถใช้งานได้ แต่พื้นฐานทั้งหมดอยู่ที่นั่น

  2. เนื่องจาก Almo บันทึกไว้ในความคิดเห็นข้างต้น Unity หลายประเภทไม่ได้เป็น threadsafe และจะโยนข้อยกเว้นหากคุณพยายามสร้างใช้หรือเปรียบเทียบกับพวกมันนอกเธรดหลัก สิ่งที่ควรทราบ:

    • กรณีหนึ่งที่พบบ่อยคือการตรวจสอบเพื่อดูว่าการอ้างอิง GameObject หรือ Monobehaviour เป็นโมฆะก่อนที่จะพยายามเข้าถึงสมาชิก myUnityObject == nullเรียกใช้โอเปอเรเตอร์ที่โอเวอร์โหลดสำหรับสิ่งใดก็ตามที่สืบทอดมาจาก UnityEngineObject แต่ใช้System.Object.ReferenceEquals()งานได้ในระดับหนึ่ง - เพียงจำไว้ว่าการทำลาย () ed GameObject จะเปรียบเทียบเท่ากับโมฆะโดยใช้โอเวอร์โหลด แต่ยังไม่ได้อ้างอิงกับคิว

    • การอ่านพารามิเตอร์จากประเภท Unity มักจะปลอดภัยในเธรดอื่น (ซึ่งจะไม่เกิดข้อยกเว้นทันทีหากคุณระมัดระวังในการตรวจสอบค่า null ดังกล่าวข้างต้น) แต่ให้สังเกตคำเตือนของ Philipp ที่นี่ว่าเธรดหลักอาจแก้ไขสถานะ ในขณะที่คุณกำลังอ่าน คุณจะต้องได้รับการลงโทษทางวินัยเกี่ยวกับผู้ที่ได้รับอนุญาตให้แก้ไขอะไรและเมื่อใดเพื่อหลีกเลี่ยงการอ่านสถานะที่ไม่สอดคล้องกันบางอย่างซึ่งอาจนำไปสู่ข้อบกพร่องที่ยากต่อการติดตามอย่างมากเนื่องจากขึ้นอยู่กับการกำหนดเวลา ทำซ้ำตามความประสงค์

    • สมาชิกสุ่มและเวลาคงไม่พร้อมใช้งาน สร้างอินสแตนซ์ของ System.Random ต่อเธรดหากคุณต้องการการสุ่มและ System.Diagnostics.Stopwatch ถ้าคุณต้องการข้อมูลเวลา

    • ฟังก์ชัน Mathf, Vector, Matrix, Quaternion และ Color structs ทั้งหมดทำงานได้ดีในหลาย ๆ เธรดดังนั้นคุณสามารถทำการคำนวณส่วนใหญ่แยกกัน

    • การสร้าง GameObjects, การแนบ Monobehaviours หรือการสร้าง / อัพเดท Textures, Meshes, Materials และอื่น ๆ ล้วนต้องเกิดขึ้นในเธรดหลัก ในอดีตเมื่อฉันต้องการทำงานกับสิ่งเหล่านี้ฉันได้สร้างคิวผู้ผลิตและผู้บริโภคที่พนักงานของฉันเตรียมข้อมูลดิบ (เช่นเวกเตอร์ / สีจำนวนมากเพื่อนำไปใช้กับตาข่ายหรือพื้นผิว) และอัปเดตหรือ Coroutine ในการสำรวจความคิดเห็นหลักสำหรับข้อมูลและนำไปใช้

นี่คือรูปแบบที่ฉันมักจะใช้สำหรับงานที่ทำเกลียว ฉันไม่รับประกันว่ามันจะเป็นแนวปฏิบัติที่ดีที่สุด แต่ก็ทำให้งานเสร็จได้ (ความเห็นหรือการแก้ไขเพื่อปรับปรุงยินดีต้อนรับ - ฉันรู้ว่าการทำเกลียวเป็นหัวข้อที่ลึกมากซึ่งฉันรู้พื้นฐานเท่านั้น)

using UnityEngine;
using System.Threading; 

public class MyThreadedBehaviour : MonoBehaviour
{

    bool _threadRunning;
    Thread _thread;

    void Start()
    {
        // Begin our heavy work on a new thread.
        _thread = new Thread(ThreadedWork);
        _thread.Start();
    }


    void ThreadedWork()
    {
        _threadRunning = true;
        bool workDone = false;

        // This pattern lets us interrupt the work at a safe point if neeeded.
        while(_threadRunning && !workDone)
        {
            // Do Work...
        }
        _threadRunning = false;
    }

    void OnDisable()
    {
        // If the thread is still running, we should shut it down,
        // otherwise it can prevent the game from exiting correctly.
        if(_threadRunning)
        {
            // This forces the while loop in the ThreadedWork function to abort.
            _threadRunning = false;

            // This waits until the thread exits,
            // ensuring any cleanup we do after this is safe. 
            _thread.Join();
        }

        // Thread is guaranteed no longer running. Do other cleanup tasks.
    }
}

ถ้าคุณไม่จำเป็นต้องเคร่งครัดในการทำงานข้ามแยกหัวข้อสำหรับความเร็วและคุณเพียงแค่มองหาวิธีที่จะทำให้มันไม่ปิดกั้นเพื่อให้ส่วนที่เหลือของเกมของคุณช่วยให้ฟ้องเป็นทางออกที่มีน้ำหนักเบาในความสามัคคีเป็นCoroutines นี่คือฟังก์ชั่นที่สามารถทำงานได้บางอย่างจากนั้นควบคุมการกลับไปที่เครื่องยนต์เพื่อทำสิ่งที่มันทำต่อไปและกลับมาทำงานได้อย่างราบรื่นในเวลาต่อมา

using UnityEngine;
using System.Collections;

public class MyYieldingBehaviour : MonoBehaviour
{ 

    void Start()
    {
        // Begin our heavy work in a coroutine.
        StartCoroutine(YieldingWork());
    }    

    IEnumerator YieldingWork()
    {
        bool workDone = false;

        while(!workDone)
        {
            // Let the engine run for a frame.
            yield return null;

            // Do Work...
        }
    }
}

นี่ไม่จำเป็นต้องมีข้อควรพิจารณาในการทำความสะอาดเป็นพิเศษเนื่องจากเครื่องยนต์ (เท่าที่ฉันสามารถบอกได้) กำจัด coroutines จากวัตถุที่ถูกทำลายให้คุณ

สถานะโลคัลทั้งหมดของเมธอดจะถูกเก็บรักษาไว้เมื่อมันให้ผลตอบแทนและดำเนินการต่อดังนั้นเพื่อวัตถุประสงค์หลายอย่างราวกับว่ามันทำงานอย่างต่อเนื่องบนเธรดอื่น (แต่คุณมีความสะดวกทั้งหมดในการทำงานบนเธรดหลัก) คุณเพียงแค่ต้องแน่ใจว่าการวนซ้ำแต่ละครั้งนั้นสั้นพอที่จะไม่ทำให้เธรดหลักของคุณช้าลงอย่างไม่มีเหตุผล

โดยการทำให้มั่นใจว่าการดำเนินการที่สำคัญไม่ได้ถูกคั่นด้วยผลตอบแทนคุณสามารถได้รับความสอดคล้องของพฤติกรรมแบบเธรดเดียวโดยรู้ว่าไม่มีสคริปต์หรือระบบอื่นในเธรดหลักสามารถแก้ไขข้อมูลที่คุณกำลังทำงานอยู่

บรรทัดผลตอบแทนให้คุณเลือกได้สองสามทาง คุณสามารถ...

  • yield return null เพื่อดำเนินการต่อหลังจากอัปเดตเฟรมถัดไป ()
  • yield return new WaitForFixedUpdate() เพื่อดำเนินการต่อหลังจาก FixedUpdate ถัดไป ()
  • yield return new WaitForSeconds(delay) เพื่อดำเนินการต่อหลังจากเวลาผ่านไปตามจำนวนเกมที่กำหนด
  • yield return new WaitForEndOfFrame() เพื่อดำเนินการต่อหลังจาก GUI แสดงผลเสร็จสิ้น
  • yield return myRequestซึ่งmyRequestเป็นอินสแตนซ์ของWWWเพื่อดำเนินการต่อเมื่อข้อมูลที่ร้องขอเสร็จสิ้นการโหลดจากเว็บหรือดิสก์
  • yield return otherCoroutineโดยที่otherCoroutineเป็นอินสแตนซ์ Coroutineเพื่อดำเนินการต่อหลังจากotherCoroutineเสร็จ สิ่งนี้มักใช้ในรูปแบบของyield return StartCoroutine(OtherCoroutineMethod())การประมวลผลลูกโซ่กับ coroutine ใหม่ที่สามารถให้ผลผลิตเมื่อต้องการ

    • ทดลองกระโดดข้ามวินาทีStartCoroutineและการเขียนก็yield return OtherCoroutineMethod()บรรลุเป้าหมายเดียวกันหากคุณต้องการโยงการทำงานในบริบทเดียวกัน

      การห่อด้านในStartCoroutineอาจยังมีประโยชน์หากคุณต้องการเรียกใช้ coroutine ที่ซ้อนกันโดยเชื่อมโยงกับวัตถุที่สองเช่นyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... ขึ้นอยู่กับว่าคุณต้องการให้ coroutine เลี้ยวต่อไปเมื่อไหร่

หรือyield break;เพื่อหยุด coroutine ก่อนที่จะถึงจุดสิ้นสุดวิธีที่คุณอาจใช้return;ในการเริ่มต้นของวิธีดั้งเดิม


มีวิธีใช้ coroutines ให้ผลหลังจากการทำซ้ำใช้เวลามากกว่า 20ms หรือไม่
DarkDestry

@DarkDestry คุณสามารถใช้อินสแตนซ์ของนาฬิกาจับเวลาเพื่อจับเวลาว่าคุณใช้เวลากับวงในของคุณนานแค่ไหนและแบ่งออกเป็นวงนอกซึ่งคุณให้ผลก่อนที่จะรีเซ็ตนาฬิกาจับเวลาและกลับสู่วงภายใน
DMGregory

1
คำตอบที่ดี! ฉันต้องการเพิ่มอีกเหตุผลหนึ่งที่จะใช้ coroutines คือถ้าคุณจำเป็นต้องสร้างสำหรับ webgl ที่จะทำงานซึ่งจะไม่ถ้าคุณใช้เธรด นั่งอยู่กับอาการปวดหัวในโครงการขนาดใหญ่ในขณะนี้: P
Mikael Högström

คำตอบที่ดีและขอบคุณ เพียงแค่สงสัยว่าถ้าฉันต้องหยุดและเริ่มหัวข้อใหม่หลายครั้งฉันลองยกเลิกและเริ่มต้นทำให้ฉันเกิดข้อผิดพลาด
Flankechen

@flankechen การเริ่มต้นและการแยกเธรดนั้นเป็นการดำเนินการที่ค่อนข้างแพงดังนั้นบ่อยครั้งที่เราต้องการให้เธรดพร้อมใช้งาน แต่ไม่ได้ใช้งาน - ใช้สิ่งต่าง ๆ เช่นเซมาฟอร์หรือมอนิเตอร์เพื่อส่งสัญญาณเมื่อเรามีงานใหม่ให้ทำ ต้องการโพสต์คำถามใหม่ที่อธิบายถึงกรณีการใช้งานโดยละเอียดและผู้คนสามารถแนะนำวิธีที่มีประสิทธิภาพในการบรรลุเป้าหมายได้หรือไม่
DMGregory

0

คุณสามารถใส่การคำนวณจำนวนมากในเธรดอื่น แต่ API ของ Unity ไม่ปลอดภัยสำหรับเธรดคุณต้องเรียกใช้งานในเธรดหลัก

คุณสามารถลองใช้แพคเกจนี้ใน Asset Store จะช่วยให้คุณใช้งานเธรดได้ง่ายขึ้น http://u3d.as/wQgคุณสามารถใช้โค้ดเพียงบรรทัดเดียวเพื่อเริ่มเธรดและรัน Unity API อย่างปลอดภัย



0

@DMGregory อธิบายได้ดีจริงๆ

สามารถใช้ทั้งเธรดและ coroutines ก่อนหน้านี้เพื่อลดเธรดหลักและใหม่กว่าเพื่อคืนการควบคุมไปยังเธรดหลัก การกดยกอย่างหนักเพื่อแยกด้ายดูจะสมเหตุสมผลกว่า ดังนั้นคิวงานอาจเป็นสิ่งที่คุณเป็น

มีสคริปต์ตัวอย่างJobQueueที่ดีจริงๆบน Unity Wiki

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.