Perbarui dan render dalam utas terpisah


12

Saya membuat mesin permainan 2D sederhana dan saya ingin memperbarui dan membuat sprite di utas yang berbeda, untuk mempelajari bagaimana hal itu dilakukan.

Saya perlu menyinkronkan utas pembaruan dan render. Saat ini, saya menggunakan dua bendera atom. Alur kerjanya terlihat seperti:

Thread 1 -------------------------- Thread 2
Update obj ------------------------ wait for swap
Create queue ---------------------- render the queue
Wait for render ------------------- notify render done
Swap render queues ---------------- notify swap done

Dalam pengaturan ini saya membatasi FPS utas render ke FPS utas pembaruan. Selain itu saya gunakan sleep()untuk membatasi render dan memperbarui FPS utas hingga 60, jadi dua fungsi menunggu tidak akan menunggu banyak waktu.

Masalahnya adalah:

Penggunaan CPU rata-rata sekitar 0,1%. Kadang-kadang naik hingga 25% (dalam quad core PC). Ini berarti bahwa utas sedang menunggu yang lain karena fungsi tunggu adalah loop sementara dengan fungsi tes dan set, dan loop sementara akan menggunakan semua sumber daya CPU Anda.

Pertanyaan pertama saya adalah: apakah ada cara lain untuk menyinkronkan kedua utas? Saya perhatikan bahwa std::mutex::lockjangan gunakan CPU saat sedang menunggu untuk mengunci sumber daya sehingga tidak loop sementara. Bagaimana cara kerjanya? Saya tidak dapat menggunakan std::mutexkarena saya harus mengunci mereka di satu utas dan membuka kunci di utas lainnya.

Pertanyaan lainnya adalah; karena program selalu berjalan pada 60 FPS mengapa terkadang penggunaan CPU-nya melonjak hingga 25%, artinya salah satu dari keduanya menunggu banyak? (kedua utas keduanya terbatas pada 60fps sehingga idealnya tidak perlu banyak sinkronisasi).

Sunting: Terima kasih atas semua balasan. Pertama saya ingin mengatakan saya tidak memulai utas baru setiap frame untuk di-render. Saya memulai pembaruan dan render loop di awal. Saya pikir multithreading dapat menghemat waktu: Saya memiliki fungsi-fungsi berikut: FastAlg () dan Alg (). Alg () adalah kedua saya Perbarui obj dan render obj dan Fastalg () adalah "kirim antrian render ke" renderer "". Dalam satu utas:

Alg() //update 
FastAgl() 
Alg() //render

Dalam dua utas:

Alg() //update  while Alg() //render last frame
FastAlg() 

Jadi mungkin multithreading dapat menghemat waktu yang sama. (sebenarnya dalam aplikasi matematika sederhana yang dilakukannya, di mana alg adalah algoritma yang panjang dan yang lebih cepat)

Saya tahu bahwa tidur bukanlah ide yang baik, meskipun saya tidak pernah memiliki masalah. Apakah ini akan lebih baik?

While(true) 
{
   If(timer.gettimefromlastcall() >= 1/fps)
   Do_update()
}

Tapi ini akan menjadi loop sementara yang akan menggunakan semua CPU. Bisakah saya menggunakan sleep (angka <15) untuk membatasi penggunaan? Dengan cara ini ia akan berjalan pada, misalnya, 100 fps, dan fungsi pembaruan akan dipanggil hanya 60 kali per detik.

Untuk menyinkronkan dua utas, saya akan menggunakan waitforsingleobject dengan createSemaphore jadi saya akan dapat mengunci dan membuka kunci di utas yang berbeda (tidak menggunakan perulangan sementara), bukan?


5
"Jangan bilang multithreading saya tidak berguna dalam kasus ini, saya hanya ingin belajar bagaimana melakukannya" - dalam hal ini Anda harus mempelajari hal-hal dengan benar, yaitu (a) jangan gunakan sleep () untuk mengontrol bingkai yang langka , tidak pernah , dan (b) menghindari desain thread-per-komponen dan menghindari menjalankan lockstep, sebagai gantinya membagi pekerjaan dalam tugas dan menangani tugas dari antrian pekerjaan.
Damon

1
@ Damon (a) sleep () dapat digunakan sebagai mekanisme frame rate dan sebenarnya cukup populer, meskipun saya harus setuju bahwa ada pilihan yang jauh lebih baik. (B) Pengguna di sini ingin memisahkan pembaruan dan membuat dalam dua utas berbeda. Ini adalah pemisahan normal dalam mesin game dan tidak begitu "thread-per-komponen". Ini memberikan keuntungan yang jelas tetapi dapat membawa masalah jika dilakukan secara tidak benar.
Alexandre Desbiens

@AlphSpirit: Fakta bahwa sesuatu itu "biasa" tidak berarti itu tidak salah . Bahkan tanpa masuk ke timer yang berbeda, granularity hanya tidur pada setidaknya satu sistem operasi desktop yang populer adalah alasan yang cukup, jika tidak dapat diandalkan per desain pada setiap sistem konsumen yang ada. Menjelaskan mengapa memisahkan pembaruan dan merender menjadi dua utas seperti yang dijelaskan adalah tidak bijaksana dan menyebabkan lebih banyak masalah daripada nilainya akan terlalu lama. Tujuan OP dinyatakan sebagai belajar bagaimana hal itu dilakukan , yang seharusnya mempelajari bagaimana hal itu dilakukan dengan benar . Banyak artikel tentang desain mesin MT modern di sekitar.
Damon

@ Damon Ketika saya mengatakan itu populer, atau umum, saya tidak bermaksud mengatakan itu benar. Maksud saya itu digunakan oleh banyak orang. "... meskipun saya harus setuju bahwa ada pilihan yang jauh lebih baik" berarti itu memang bukan cara yang sangat baik untuk menyinkronkan waktu. Maaf atas kesalahpahaman ini.
Alexandre Desbiens

@AlphSpirit: Jangan khawatir :-) Dunia ini penuh dengan hal-hal yang dilakukan banyak orang (dan tidak selalu karena alasan yang baik), tetapi ketika seseorang mulai belajar, ia masih harus mencoba menghindari yang paling jelas salah.
Damon

Jawaban:


25

Untuk mesin 2D sederhana dengan sprite, pendekatan single-threaded sangat baik. Tetapi karena Anda ingin belajar bagaimana melakukan multithreading, Anda harus belajar melakukannya dengan benar.

Tidak

  • Gunakan 2 utas yang menjalankan langkah kunci lebih kurang, menerapkan perilaku utas tunggal dengan beberapa utas. Ini memiliki tingkat paralelisme yang sama (nol) tetapi menambahkan overhead untuk sakelar konteks dan sinkronisasi. Plus, logikanya lebih sulit untuk grok.
  • Gunakan sleepuntuk mengontrol laju bingkai. Tidak pernah. Jika seseorang memberi tahu Anda, pukul mereka.
    Pertama, tidak semua monitor bekerja pada 60Hz. Kedua, dua timer yang berdetak pada laju yang sama berjalan berdampingan akhirnya akan selalu tidak sinkron (letakkan dua bola pingpong di atas meja dari ketinggian yang sama, dan dengarkan). Ketiga, sleepsecara desain tidak akurat atau dapat diandalkan. Granularity mungkin sama buruknya dengan 15.6ms (pada kenyataannya, default pada Windows [1] ), dan sebuah frame hanya 16.6ms pada 60fps, yang menyisakan 1ms hanya untuk yang lainnya. Plus, sulit untuk mendapatkan 16,6 menjadi kelipatan 15,6 ...
    Juga, sleepdiizinkan untuk (dan kadang-kadang akan!) Kembali hanya setelah 30 atau 50 atau 100 ms, atau waktu yang lebih lama.
  • Gunakan std::mutexuntuk memberi tahu thread lain. Ini bukan untuk apa.
  • Asumsikan TaskManager pandai memberi tahu Anda apa yang terjadi, terutama jika dilihat dari angka seperti "25% CPU", yang dapat dihabiskan dalam kode Anda, atau dalam driver usermode, atau di tempat lain.
  • Memiliki satu utas per komponen tingkat tinggi (tentu saja ada beberapa pengecualian).
  • Buat utas pada "waktu acak", ad hoc, per tugas. Membuat utas bisa sangat mahal dan bisa memakan waktu yang sangat lama sebelum mereka benar-benar melakukan apa yang Anda katakan (terutama jika Anda memiliki banyak DLL yang dimuat!).

Melakukan

  • Gunakan multithreading untuk menjalankan berbagai hal secara serempak sebanyak yang Anda bisa. Kecepatan bukanlah ide utama threading, tetapi melakukan hal-hal secara paralel (jadi bahkan jika mereka mengambil lebih lama secara keseluruhan, jumlah semua masih kurang).
  • Gunakan sinkronisasi vertikal untuk membatasi kecepatan bingkai. Itu adalah satu-satunya cara yang benar (dan tidak gagal) untuk melakukannya. Jika pengguna mengabaikan Anda di panel kontrol driver display ("matikan"), maka jadilah itu. Lagi pula itu komputernya, bukan milikmu.
  • Jika Anda perlu "menandai" sesuatu secara berkala, gunakan timer . Pengatur waktu memiliki keuntungan karena memiliki akurasi dan keandalan yang jauh lebih baik dibandingkan dengan sleep[2] . Juga, pengatur waktu berulang menghitung waktu dengan benar (termasuk waktu yang lewat di antaranya) sedangkan tidur selama 16,6 ms (atau 16,6 ms minus diukur_time_elapsed) tidak.
  • Jalankan simulasi fisika yang melibatkan integrasi numerik pada langkah waktu yang tetap (atau persamaan Anda akan meledak!), Interpolasi grafik di antara langkah-langkah (ini mungkin merupakan alasan untuk thread per komponen yang terpisah, tetapi juga dapat dilakukan tanpa).
  • Gunakan std::mutexuntuk hanya memiliki satu utas mengakses sumber pada suatu waktu ("saling mengecualikan"), dan untuk mematuhi semantik aneh std::condition_variable.
  • Hindari membuat utas bersaing untuk mendapatkan sumber daya. Kunci sesedikit mungkin (tetapi tidak kurang!) Dan tahan kunci hanya selama benar-benar diperlukan.
  • Bagikan data hanya-baca di antara utas (tidak ada masalah cache, dan tidak perlu penguncian), tetapi jangan secara bersamaan mengubah data (perlu disinkronkan dan bunuh cache). Itu termasuk memodifikasi data yang ada di dekat lokasi yang mungkin dibaca orang lain.
  • Gunakan std::condition_variableuntuk memblokir utas lainnya sampai beberapa kondisi benar. Semantik std::condition_variabledengan mutex tambahan itu diakui cukup aneh dan bengkok (kebanyakan karena alasan historis yang diwarisi dari utas POSIX), tetapi variabel kondisi adalah primitif yang tepat untuk digunakan untuk apa yang Anda inginkan.
    Jika Anda merasa std::condition_variableterlalu aneh untuk merasa nyaman dengan itu, Anda juga bisa menggunakan acara Windows (sedikit lebih lambat), atau, jika Anda berani, bangun acara sederhana Anda sendiri di sekitar NtKeyedEvents (melibatkan hal-hal menakutkan tingkat rendah). Saat Anda menggunakan DirectX, Anda sudah terikat dengan Windows, jadi kehilangan portabilitas seharusnya tidak menjadi masalah besar.
  • Hancurkan pekerjaan menjadi tugas-tugas berukuran wajar yang dijalankan oleh kumpulan thread pekerja berukuran tetap (tidak lebih dari satu per inti, tidak termasuk core yang di-hyperhreaded). Biarkan menyelesaikan tugas yang membutuhkan tugas yang tergantung (sinkronisasi gratis dan otomatis). Buat tugas yang masing-masing memiliki setidaknya beberapa ratus operasi non-sepele (atau satu operasi pemblokiran yang panjang seperti pembacaan disk). Lebih suka akses cache-berdekatan.
  • Buat semua utas saat program dimulai.
  • Manfaatkan fungsi asinkron yang ditawarkan OS atau grafik API untuk paralelisme yang lebih baik / tambahan, tidak hanya pada tingkat program tetapi juga pada perangkat keras (pikirkan transfer PCIe, paralelisme CPU-GPU, disk DMA, dll.).
  • 10.000 hal lain yang saya lupa sebutkan.


[1] Ya, Anda dapat mengatur kecepatan penjadwal menjadi 1ms, tetapi ini disukai karena menyebabkan lebih banyak konteks beralih, dan mengkonsumsi lebih banyak daya (di dunia di mana semakin banyak perangkat adalah perangkat seluler). Ini juga bukan solusi karena masih tidak membuat tidur lebih dapat diandalkan.
[2] Pengatur waktu akan meningkatkan prioritas utas, yang memungkinkannya menghentikan utas menengah-prioritas lain yang setara dan dijadwalkan terlebih dahulu, yang merupakan perilaku kuasi-RT. Ini tentu saja bukan RT yang benar, tetapi sangat dekat. Bangun dari tidur hanya berarti bahwa utas menjadi siap dijadwalkan pada suatu waktu, kapan pun itu mungkin.


Bisakah Anda jelaskan mengapa Anda tidak harus "Memiliki satu utas per komponen tingkat tinggi"? Apakah maksud Anda seseorang seharusnya tidak memiliki pencampuran fisika dan audio dalam dua utas yang terpisah? Saya tidak melihat alasan untuk tidak melakukannya.
Elviss Strazdins

3

Saya tidak yakin apa yang ingin Anda capai dengan membatasi FPS dari Pembaruan dan Membuat keduanya menjadi 60. Jika Anda membatasi mereka pada nilai yang sama, Anda bisa meletakkannya di utas yang sama.

Tujuan ketika memisahkan Pembaruan dan Render dalam utas yang berbeda adalah untuk memiliki "hampir" yang independen satu sama lain, sehingga GPU dapat membuat 500 FPS dan logika Pembaruan masih berjalan pada 60 FPS. Anda tidak mencapai perolehan kinerja yang sangat tinggi dengan melakukannya.

Tapi kamu bilang kamu hanya ingin tahu cara kerjanya, dan tidak apa-apa. Dalam C ++, mutex adalah objek khusus yang digunakan untuk mengunci akses ke sumber daya tertentu untuk utas lainnya. Dengan kata lain, Anda menggunakan mutex untuk membuat data yang masuk akal dapat diakses hanya dengan satu utas pada saat itu. Untuk melakukannya, cukup sederhana:

std::mutex mutex;
mutex.lock();
// Do sensible stuff here...
mutex.unlock();

Sumber: http://en.cppreference.com/w/cpp/thread/mutex

EDIT : Pastikan mutex Anda adalah kelas atau file-lebar, seperti pada tautan yang diberikan, atau setiap thread akan membuat mutex sendiri dan Anda tidak akan mencapai apa pun.

Utas pertama untuk mengunci mutex akan memiliki akses ke kode di dalamnya. Jika utas kedua mencoba memanggil fungsi kunci (), itu akan diblok sampai utas pertama membuka kunci. Jadi mutex adalah fungsi blok, tidak seperti loop sementara. Fungsi pemblokiran tidak akan memberi tekanan pada CPU.


Dan bagaimana cara kerja blok?
Liuka

Ketika utas kedua akan memanggil kunci (), ia akan dengan sabar menunggu utas pertama untuk membuka kunci mutex, dan akan melanjutkan pada baris berikutnya setelah (dalam contoh ini, hal-hal yang masuk akal). EDIT: Thread kedua kemudian akan mengunci mutex untuk dirinya sendiri.
Alexandre Desbiens


1
Gunakan std::lock_guardatau serupa, bukan .lock()/ .unlock(). RAII bukan hanya untuk manajemen memori!
bcrist
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.