Bagaimana cara membekukan utas utama di Unity?

33

Saya memiliki algoritma level generation yang komputasional berat. Karena itu, menyebutnya selalu menghasilkan pembekuan layar game. Bagaimana saya bisa menempatkan fungsi pada utas kedua saat game masih terus membuat layar pemuatan untuk menunjukkan bahwa game tidak beku?

DarkDestry
sumber
1
Anda dapat menggunakan sistem threading untuk menjalankan pekerjaan lain di latar belakang ... tetapi mereka mungkin tidak memanggil apa pun di API Persatuan karena hal itu bukan threadsafe. Hanya berkomentar karena saya belum melakukan ini dan tidak dapat dengan percaya diri memberi Anda contoh kode dengan cepat.
Almo
Gunakan ListTindakan untuk menyimpan fungsi yang ingin Anda panggil di utas utama. kunci dan salin Actiondaftar dalam Updatefungsi ke daftar sementara, kosongkan daftar asli kemudian jalankan Actionkode di Listutas utama. Lihat UnityThread dari posting saya yang lain tentang cara melakukan ini. Misalnya, untuk memanggil fungsi di Thread utama, UnityThread.executeInUpdate(() => { transform.Rotate(new Vector3(0f, 90f, 0f)); });
Programmer

Jawaban:

48

Pembaruan: Pada tahun 2018, Unity meluncurkan C # Job System sebagai cara untuk membongkar pekerjaan dan memanfaatkan beberapa inti CPU.

Jawaban di bawah ini ada sebelum sistem ini. Itu masih akan berfungsi, tetapi mungkin ada opsi yang lebih baik tersedia di Unity modern, tergantung pada kebutuhan Anda. Secara khusus, sistem pekerjaan tampaknya menyelesaikan beberapa batasan pada apa yang dapat diakses secara manual oleh thread yang dibuat secara manual, dijelaskan di bawah ini. Pengembang bereksperimen dengan laporan pratinjau melakukan raycast dan membangun jerat secara paralel , misalnya.

Saya akan mengundang pengguna dengan pengalaman menggunakan sistem pekerjaan ini untuk menambahkan jawaban mereka sendiri yang mencerminkan kondisi mesin saat ini.


Saya telah menggunakan threading untuk tugas-tugas kelas berat di Unity di masa lalu (biasanya pemrosesan gambar & geometri), dan itu tidak jauh berbeda dari menggunakan utas dalam aplikasi C # lainnya, dengan dua peringatan:

  1. Karena Unity menggunakan subset yang agak lebih lama dari .NET, ada beberapa fitur threading yang lebih baru dan pustaka yang tidak dapat kita gunakan di luar kotak, tetapi semua dasar-dasarnya ada di sana.

  2. Seperti yang dicatat oleh Almo dalam komentar di atas, banyak tipe Unity bukan threadsafe, dan akan mengeluarkan pengecualian jika Anda mencoba membuat, menggunakan, atau bahkan membandingkannya dari utas utama. Hal-hal yang perlu diingat:

    • Satu kasus umum adalah memeriksa untuk melihat apakah referensi GameObject atau Monobehaviour adalah nol sebelum mencoba mengakses anggotanya. myUnityObject == nullpanggilan operator kelebihan beban untuk apa pun yang diturunkan dari UnityEngine.Object, tetapi System.Object.ReferenceEquals()bekerja di sekitar ini ke tingkat - hanya ingat bahwa Hancurkan () ed GameObject membandingkan sama dengan nol menggunakan kelebihan beban, tetapi belum ReferenceEqual ke nol.

    • Membaca parameter dari jenis Unity biasanya aman di thread lain (dalam hal itu tidak akan segera melemparkan sebuah pengecualian selama Anda sedang berhati-hati untuk memeriksa nulls seperti di atas), tetapi catatan peringatan Philipp sini bahwa thread utama mungkin memodifikasi negara saat Anda membacanya. Anda harus disiplin tentang siapa yang diizinkan untuk mengubah apa & kapan untuk menghindari membaca keadaan yang tidak konsisten, yang dapat menyebabkan bug yang sangat sulit untuk dilacak karena mereka bergantung pada waktu sub-milidetik di antara utas yang kami dapat dapat mereproduksi sesuka hati.

    • Anggota statis acak dan Waktu tidak tersedia. Buat instance dari System.Random per utas jika Anda memerlukan keacakan, dan System.Diagnostics.Stopwatch jika Anda memerlukan info waktu.

    • Fungsi Mathf, Vector, Matrix, Quaternion, dan struct Warna semua berfungsi dengan baik di seluruh utas, sehingga Anda dapat melakukan sebagian besar perhitungan secara terpisah

    • Membuat GameObjects, melampirkan Monobehaviours, atau membuat / memperbarui Tekstur, Jerat, Bahan, dll. Semua harus terjadi pada utas utama. Di masa lalu ketika saya harus bekerja dengan ini, saya telah membuat antrian produsen-konsumen, di mana utas pekerja saya menyiapkan data mentah (seperti array besar vektor / warna untuk diterapkan pada mesh atau tekstur), dan Pembaruan atau Coroutine pada jajak pendapat utas utama untuk data dan menerapkannya.

Dengan catatan itu, inilah pola yang sering saya gunakan untuk pekerjaan berulir. Saya tidak membuat jaminan bahwa itu adalah gaya praktik terbaik, tetapi itu menyelesaikan pekerjaan. (Komentar atau suntingan untuk meningkatkan dipersilahkan - Saya tahu threading adalah topik yang sangat mendalam yang saya hanya tahu dasar-dasarnya)

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.
    }
}

Jika Anda tidak benar-benar perlu membagi pekerjaan di thread untuk kecepatan, dan Anda hanya mencari cara untuk membuatnya non-blocking sehingga sisa gim Anda terus berdetak, solusi yang lebih ringan di Unity adalah Coroutine . Ini adalah fungsi yang dapat melakukan beberapa pekerjaan kemudian menghasilkan kontrol kembali ke mesin untuk melanjutkan apa yang dilakukannya, dan melanjutkan dengan mulus di lain waktu.

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...
        }
    }
}

Ini tidak memerlukan pertimbangan pembersihan khusus, karena mesin (sejauh yang saya tahu) menghilangkan coroutine dari benda yang hancur untuk Anda.

Semua keadaan lokal dari metode ini dipertahankan ketika menghasilkan dan melanjutkan, jadi untuk banyak tujuan seolah-olah itu berjalan tanpa gangguan pada utas lainnya (tetapi Anda memiliki semua kemudahan menjalankan pada utas utama). Anda hanya perlu memastikan setiap iterasi itu cukup pendek sehingga tidak akan memperlambat utas Anda secara tidak masuk akal.

Dengan memastikan operasi penting tidak dipisahkan oleh hasil, Anda bisa mendapatkan konsistensi perilaku single-threaded - mengetahui bahwa tidak ada skrip atau sistem lain di utas utama yang dapat mengubah data yang sedang Anda kerjakan.

Garis pengembalian hasil memberi Anda beberapa opsi. Kamu bisa...

  • yield return null untuk melanjutkan setelah pembaruan frame berikutnya ()
  • yield return new WaitForFixedUpdate() untuk melanjutkan setelah FixedUpdate berikutnya ()
  • yield return new WaitForSeconds(delay) untuk melanjutkan setelah sejumlah waktu permainan berlalu
  • yield return new WaitForEndOfFrame() untuk melanjutkan setelah GUI selesai render
  • yield return myRequestdi mana myRequestadalah contoh WWW , untuk melanjutkan setelah data yang diminta selesai memuat dari web atau disk.
  • yield return otherCoroutinedi mana otherCoroutineadalah contoh coroutine , untuk melanjutkan setelah otherCoroutineselesai. Ini sering digunakan dalam bentuk yield return StartCoroutine(OtherCoroutineMethod())untuk menghubungkan eksekusi ke coroutine baru yang dapat dengan sendirinya menghasilkan bila diinginkan.

    • Secara eksperimental, melewatkan yang kedua StartCoroutinedan hanya menulis yield return OtherCoroutineMethod()mencapai tujuan yang sama jika Anda ingin rantai eksekusi dalam konteks yang sama.

      Membungkus di dalam StartCoroutinemungkin masih berguna jika Anda ingin menjalankan coroutine bersarang dalam kaitannya dengan objek kedua, sepertiyield return otherObject.StartCoroutine(OtherObjectsCoroutineMethod())

... tergantung kapan Anda ingin coroutine mengambil giliran berikutnya.

Atau yield break;untuk menghentikan coroutine sebelum mencapai akhir, cara yang mungkin Anda gunakan return;untuk memulai metode konvensional.

DMGregory
sumber
Apakah ada cara untuk menggunakan coroutine untuk menghasilkan hanya setelah iterasi memakan waktu lebih dari 20 ms?
DarkDestry
@DarkDestry Anda bisa menggunakan instance stopwatch untuk menentukan berapa lama Anda menghabiskan waktu di loop batin Anda, dan keluar ke loop luar tempat Anda menghasilkan sebelum menyetel kembali stopwatch dan melanjutkan loop dalam.
DMGregory
1
Jawaban yang bagus! Saya hanya ingin menambahkan bahwa satu lagi alasan untuk menggunakan coroutine adalah bahwa jika Anda perlu membuat untuk webgl yang akan berfungsi yang tidak akan terjadi jika Anda menggunakan utas. Duduk dengan sakit kepala itu dalam proyek besar sekarang: P
Mikael Högström
Jawaban yang bagus dan terima kasih. Hanya ingin tahu bagaimana jika saya harus berhenti dan memulai kembali utas beberapa kali, saya mencoba batalkan dan mulai, membuat saya kesalahan
flankechen
@ flankechen Start-up dan teardown adalah operasi yang relatif mahal, jadi seringkali kita akan lebih memilih untuk menjaga agar thread tetap tersedia tetapi tidak aktif - menggunakan hal-hal seperti semafor atau monitor untuk memberi sinyal ketika kita memiliki pekerjaan baru untuk dilakukan. Ingin memposting pertanyaan baru yang menjelaskan kasus penggunaan Anda secara terperinci, dan orang-orang dapat menyarankan cara-cara efisien untuk mencapainya?
DMGregory
0

Anda dapat memasukkan perhitungan berat Anda ke utas lain, tetapi API Unity tidak aman, Anda harus menjalankannya di utas utama.

Nah Anda bisa mencoba paket ini di Asset Store akan membantu Anda menggunakan threading lebih mudah. http://u3d.as/wQg Anda cukup menggunakan hanya satu baris kode untuk memulai utas dan menjalankan API Persatuan dengan aman.

Qi Haoyan
sumber
0

@DMGregory menjelaskannya dengan sangat baik.

Baik threading maupun coroutine dapat digunakan. Sebelumnya untuk membongkar utas utama dan kemudian untuk mengembalikan kontrol ke utas utama. Mendorong pengangkatan berat untuk memisahkan benang tampaknya lebih masuk akal. Karena itu, antrian pekerjaan mungkin seperti apa yang Anda cari.

Ada contoh script JobQueue yang sangat bagus di Unity Wiki.

IndieForger
sumber