Bagaimana seharusnya saya menyusun kelas untuk memungkinkan simulasi multithread?

8

Dalam permainan saya, ada bidang tanah dengan bangunan (rumah, pusat sumber daya). Bangunan seperti rumah memiliki penyewa, kamar, add-on, dan lain-lain, dan ada beberapa nilai yang harus disimulasikan berdasarkan semua variabel ini.

Sekarang, saya ingin menggunakan AndEngine untuk hal-hal ujung depan, dan membuat utas lain untuk melakukan perhitungan simulasi (mungkin juga nanti termasuk AI dalam utas ini). Ini agar satu utas utuh tidak melakukan semua pekerjaan dan menyebabkan masalah seperti pemblokiran. Ini memperkenalkan masalah konkurensi dan ketergantungan .

Masalah mata uang adalah utas utama UI saya dan utas perhitungan keduanya perlu mengakses semua objek simulasi. Jadi saya harus membuatnya aman, tetapi saya tidak tahu bagaimana menyimpan dan menyusun objek simulasi untuk mengaktifkannya.

Masalah ketergantungan adalah bahwa untuk menghitung nilai, perhitungan saya bergantung pada nilai objek lain.

Apa cara terbaik untuk menghubungkan objek penyewa saya di gedung dengan perhitungan saya? Hard-code ke kelas penyewa? Apa cara yang baik untuk melakukan algoritma "toko" sehingga mudah diubah?

Cara malas yang sederhana adalah dengan memasukkan semuanya ke dalam kelas yang menampung semua objek, seperti bidang tanah (yang pada gilirannya memegang bangunan, dan lain-lain). Kelas ini juga akan menyimpan status permainan seperti teknologi yang tersedia untuk pengguna, kumpulan objek untuk hal-hal seperti sprite. Tapi ini cara yang malas dan berbahaya, benar?

Sunting: Saya sedang melihat Dependency Injection, tetapi seberapa baik itu mengatasi seperti kelas yang memegang objek lain? yaitu, tanah saya, dengan bangunan, yang memiliki penyewa dan sejumlah nilai lainnya. DI terlihat seperti rasa sakit di gelandangan dengan AndEngine juga.

NiffyShibby
sumber
Sekedar catatan singkat, tidak ada kekhawatiran tentang akses bersamaan ke data jika salah satu akses hanya pernah dibaca saja. Selama Anda membiarkan rendering Anda hanya membaca data mentah untuk menggunakannya untuk rendering dan Tidak memperbarui data selama pemrosesan, tidak ada masalah. Satu utas memperbarui data, utas lainnya hanya membacanya dan menjadikannya.
James
Yah akses konkurensi masih menjadi masalah, karena pengguna dapat membeli sebidang tanah, membangun gedung di atas tanah itu dan menempatkan penyewa ke dalam rumah, jadi utas utama adalah membuat data dan dapat memodifikasi data. Akses bersamaan tidak begitu menjadi masalah, ini lebih tentang contohnya berbagi antara utas utama dan utas anak.
NiffyShibby
Saya berbicara tentang ketergantungan sebagai masalah, sepertinya orang-orang seperti Google berpendapat bahwa menyembunyikan ketergantungan bukanlah hal yang bijaksana. Perhitungan penyewa saya bergantung pada plot bangunan, bangunan, membuat sprite di layar (saya bisa memiliki hubungan relasional antara penyewa bangunan dan membuat sprite penyewa di tempat lain)
NiffyShibby
Saya kira saran saya harus ditafsirkan sebagai menjadikan hal-hal yang di-threaded menjadi hal-hal yang mandiri atau memerlukan akses baca-saja ke data yang dikelola oleh utas lain. Rendering akan menjadi contoh dari sesuatu yang dapat Anda ulas seperti itu akan hanya perlu akses baca ke data sehingga dapat menampilkannya.
James
1
James, bahkan akses read-only bisa menjadi ide yang buruk jika utas lain sedang membuat perubahan pada objek itu. Dengan struktur data yang kompleks dapat menyebabkan kerusakan, dan dengan tipe data biasa dapat menyebabkan pembacaan yang tidak konsisten.
Kylotan

Jawaban:

4

Masalah Anda pada dasarnya serial - Anda harus menyelesaikan pembaruan simulasi sebelum dapat membuatnya. Mengosongkan simulasi ke utas berbeda hanya berarti utas UI utama tidak melakukan apa - apa sementara utas simulasi dicentang (yang artinya diblokir).

"Praktik terbaik" yang umum diadakan untuk konkurensi bukanlah untuk menempatkan rendering Anda pada satu utas dan simulasi Anda pada yang lain, seperti yang Anda usulkan. Saya sangat merekomendasikan menentang pendekatan itu, sebenarnya. Kedua operasi secara alami terkait seri, dan sementara mereka bisa dipaksa, itu tidak optimal dan tidak skala .

Pendekatan yang lebih baik adalah membuat bagian-bagian dari pembaruan atau rendering bersamaan, tetapi biarkan memperbarui dan rendering sendiri serial. Jadi misalnya, jika Anda memiliki batas alami dalam simulasi Anda (misalnya, jika rumah tidak pernah saling mempengaruhi dalam simulasi Anda), Anda dapat mendorong semua rumah menjadi ember N rumah, dan memutar seikat benang yang masing-masing proses satu ember, dan biarkan utas bergabung sebelum langkah pembaruan selesai. Timbangan ini jauh lebih baik dan jauh lebih cocok untuk desain bersamaan.

Anda terlalu memikirkan sisa masalah:

Ketergantungan injeksi adalah herring merah di sini: semua injeksi ketergantungan benar-benar berarti bahwa Anda lulus ("menyuntikkan") dependensi dari antarmuka ke instance dari antarmuka itu, biasanya selama konstruksi.

Itu berarti jika Anda memiliki kelas yang memodelkan a House, yang perlu mengetahui hal-hal Cityyang ada di dalamnya, maka Housekonstruktornya akan terlihat seperti:

public House( City containingCity ) {
  m_city = containingCity; // Store in a member variable for later access
  ...
}

Tidak ada yang spesial.

Menggunakan singleton tidak perlu (Anda sering melihatnya dilakukan di beberapa "DI framework" gila-gilaan yang terlalu rumit, seperti Caliburn yang dirancang untuk aplikasi GUI "perusahaan" - ini tidak menjadikannya solusi yang baik). Bahkan, memperkenalkan lajang sering merupakan antitesis dari manajemen ketergantungan yang baik. Mereka juga dapat menyebabkan masalah serius dengan kode multithreaded karena biasanya tidak dapat dibuat aman tanpa kunci - semakin banyak kunci yang harus Anda peroleh, semakin buruk masalah Anda yang cocok untuk ditangani secara paralel.


sumber
Saya ingat mengatakan lajang buruk dalam posting asli saya ...
NiffyShibby
Saya ingat mengatakan lajang buruk di posting asli saya, tetapi itu dihapus. Saya pikir saya mengerti apa yang Anda katakan. Contohnya, orang kecil saya berjalan melintasi layar, saat ia melakukan itu utas pembaruan dipanggil, ia perlu memperbarui, tetapi tidak bisa karena utas utama menggunakan objek, sehingga utas saya yang lain diblokir. Di mana saya harus memperbarui antara rendering.
NiffyShibby
Seseorang mengirimi saya tautan yang bermanfaat. gamedev.stackexchange.com/questions/95/…
NiffyShibby
5

Solusi biasa untuk masalah konkurensi adalah isolasi data .

Isolasi berarti bahwa setiap utas memiliki data sendiri, dan tidak menyentuh data utas lainnya. Dengan cara ini tidak ada masalah dengan konkurensi ... tetapi kemudian kita memiliki masalah komunikasi. Bagaimana utas ini dapat bekerja bersama jika tidak berbagi data apa pun?

Ada dua pendekatan di sini.

Yang pertama adalah kekekalan . Struktur / variabel yang tidak dapat berubah adalah yang tidak pernah mengubah keadaannya. Pada awalnya, ini mungkin terdengar tidak berguna - bagaimana kita bisa menggunakan "variabel" yang tidak pernah berubah? Namun, kita dapat menukar variabel-variabel ini! Pertimbangkan contoh ini: misalkan Anda memiliki Tenantkelas dengan sekelompok bidang, yang diperlukan dalam keadaan konsisten. Jika Anda mengubah Tenantobjek di utas A, dan pada saat yang sama mengamatinya dari utas B, utas B dapat melihat objek dalam keadaan tidak konsisten. Namun, jika Tenanttidak dapat diubah, utas A tidak dapat mengubahnya. Sebaliknya, itu menciptakan yang baru Tenantobjek dengan bidang yang diatur sesuai kebutuhan, dan menukar dengan yang lama. Swapping hanyalah perubahan ke satu referensi, yang mungkin atom, dan dengan demikian tidak ada cara untuk mengamati objek dalam keadaan tidak konsisten.

Pendekatan kedua adalah olahpesan . Gagasan di baliknya adalah bahwa ketika semua data "dimiliki" oleh beberapa utas, kami dapat memberi tahu utas ini apa yang harus dilakukan dengan data. Setiap utas dalam arsitektur ini memiliki antrian pesan - daftar Messageobjek, dan pompa pesan - metode yang terus berjalan yang menghapus pesan dari antrian, menafsirkannya dan memanggil beberapa metode penangan. Misalnya, Anda mengetuk sebidang tanah, menandakan bahwa itu perlu dibeli. Thread UI tidak dapat mengubah Plotobjek secara langsung, karena itu milik thread logika (dan mungkin tidak berubah). Jadi UI thread membangun BuyMessageobjek sebagai gantinya, dan menambahkannya ke antrian logika thread. Logika utas, saat dijalankan, mengambil pesan dari antrian dan panggilanBuyPlot(), mengekstraksi parameter dari objek pesan. Mungkin mengirim pesan kembali, misalnya BuySuccessfulMessage, menginstruksikan utas UI untuk memasang "Sekarang Anda memiliki lebih banyak tanah!" jendela di layar. Tentu saja, akses ke antrian pesan harus disinkronkan dengan kunci, bagian kritis atau apa pun namanya di AndEngine. Tapi ini adalah satu titik sinkronisasi antara utas, dan utas ditangguhkan untuk waktu yang sangat singkat, jadi itu bukan masalah.

Kedua pendekatan ini paling baik digunakan dalam kombinasi. Utas Anda harus berkomunikasi dengan pesan, dan memiliki beberapa data abadi "terbuka" untuk utas lainnya - misalnya, daftar plot yang tidak dapat diubah untuk ditarik oleh UI.

Perhatikan juga bahwa "hanya-baca" tidak selalu berarti tidak berubah ! Setiap struktur data yang kompleks seperti hashtable dapat mengubah status internalnya pada akses baca, jadi tanyakan pada dokumentasi terlebih dahulu.

Sudahlah
sumber
Kedengarannya cara yang rapi, saya harus melakukan beberapa pengujian dengan itu, kedengarannya cukup mahal dengan cara ini, saya berpikir sepanjang garis DI dengan lingkup tunggal, kemudian menggunakan kunci untuk akses bersamaan. Tetapi saya tidak pernah berpikir untuk melakukannya dengan cara ini, ini mungkin bisa berhasil: D
NiffyShibby
Nah, begitulah cara kami melakukan konkurensi pada server multithread yang sangat konkuren. Mungkin sedikit berlebihan untuk permainan sederhana, tapi itulah pendekatan yang saya gunakan sendiri.
Nevermind
4

Mungkin 99% dari program komputer yang ditulis dalam sejarah hanya menggunakan 1 utas dan berfungsi dengan baik. Saya tidak memiliki pengalaman tentang AndEngine tetapi sangat jarang menemukan sistem yang memerlukan threading, hanya beberapa yang dapat memperoleh manfaat darinya, mengingat perangkat keras yang tepat.

Secara tradisional, untuk melakukan simulasi dan GUI / rendering dalam satu utas, Anda cukup melakukan sedikit simulasi, lalu Anda render, dan Anda ulangi, biasanya berkali-kali per detik.

Ketika seseorang memiliki sedikit pengalaman dalam menggunakan beberapa proses, atau tidak sepenuhnya menghargai apa arti 'keselamatan' utas (yang merupakan istilah samar yang dapat berarti banyak hal yang berbeda), terlalu mudah untuk memperkenalkan banyak bug ke dalam suatu sistem. Jadi secara pribadi saya akan merekomendasikan mengambil pendekatan single-threaded, simulasi interleaving dan rendering, dan menyimpan segala threading untuk operasi yang Anda tahu pasti akan memakan waktu lama dan benar-benar membutuhkan thread dan bukan model berbasis peristiwa.

Kylotan
sumber
Andengine melakukan render untuk saya, tetapi saya masih merasa bahwa perhitungan harus dilakukan di utas lain, karena utas ui utama akan melambat jika tidak diblokir jika semuanya dilakukan dalam satu utas.
NiffyShibby
Mengapa Anda merasakannya? Apakah Anda memiliki perhitungan yang lebih mahal daripada game 3D biasa? Dan tahukah Anda bahwa sebagian besar perangkat Android hanya memiliki 1 inti dan karenanya tidak memperoleh manfaat kinerja intrinsik dari utas tambahan?
Kylotan
Tidak ada tetapi menyenangkan untuk memisahkan logika dan dengan jelas mendefinisikan apa yang sedang dilakukan, jika Anda menyimpannya di utas yang sama, Anda harus merujuk kelas utama tempat ini diadakan atau melakukan DI dengan lingkup tunggal. Yang tidak terlalu masalah. Sedangkan untuk core, kami melihat lebih banyak perangkat android dual core yang keluar, ide gim saya mungkin tidak bekerja dengan baik pada perangkat single core sementara pada dual core itu bisa bekerja dengan cukup baik.
NiffyShibby
Jadi merancang semuanya dalam 1 utas utuh sepertinya bukan ide yang bagus bagi saya, setidaknya dengan utas saya dapat memisahkan logika dan di masa depan tidak perlu khawatir tentang mencoba meningkatkan kinerja seperti yang telah saya rancang sejak awal.
NiffyShibby
Tetapi masalah Anda masih bersifat serial, jadi Anda mungkin akan memblokir kedua utas Anda menunggu mereka bergabung, kecuali jika Anda telah mengisolasi data (memberi untaian render sesuatu untuk benar-benar dilakukan ketika utas logika berdetak), dan merender bingkai atau lebih di belakang simulasi. Pendekatan yang Anda gambarkan bukanlah praktik terbaik yang diterima secara umum untuk desain concurrency.