Multithreading: apakah saya melakukan kesalahan?

23

Saya sedang mengerjakan aplikasi yang memutar musik.

Selama pemutaran, seringkali hal-hal perlu terjadi pada utas terpisah karena mereka perlu terjadi secara bersamaan. Misalnya, nada chord perlu didengarkan bersama, sehingga masing-masing diberi utas sendiri untuk dimainkan. (Edit untuk memperjelas: memanggil note.play()membekukan utas sampai not selesai dimainkan, dan inilah mengapa saya perlu tiga pisahkan utas agar tiga nada terdengar pada saat bersamaan.)

Perilaku semacam ini menciptakan banyak utas selama pemutaran karya musik.

Sebagai contoh, perhatikan musik dengan melodi pendek dan progresi akor pendek. Keseluruhan melodi dapat dimainkan pada satu utas, tetapi progresi membutuhkan tiga utas untuk dimainkan, karena masing-masing akornya berisi tiga not.

Jadi pseudo-code untuk memainkan progresi terlihat seperti ini:

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            runOnNewThread( func(){ note.play(); } );
}

Jadi dengan anggapan progres memiliki 4 akor, dan kami memainkannya dua kali, daripada yang kami buka 3 notes * 4 chords * 2 times= 24 utas. Dan ini hanya untuk memainkannya sekali saja.

Sebenarnya, itu berfungsi dengan baik dalam praktek. Saya tidak melihat adanya latensi yang terlihat, atau bug yang dihasilkan dari ini.

Tetapi saya ingin bertanya apakah ini praktik yang benar, atau apakah saya melakukan sesuatu yang secara fundamental salah. Apakah masuk akal untuk membuat begitu banyak utas setiap kali pengguna menekan tombol? Jika tidak, bagaimana saya bisa melakukannya secara berbeda?

Aviv Cohn
sumber
14
Mungkin Anda harus mempertimbangkan mencampur audio sebagai gantinya? Saya tidak tahu kerangka apa yang Anda gunakan tetapi inilah contohnya: wiki.libsdl.org/SDL_MixAudioFormat Atau, Anda dapat menggunakan saluran: libsdl.org/projects/SDL_mixer/docs/SDL_mixer_25.html#SEC25
Rufflewind
5
Is it reasonable to create so many threads...tergantung pada model threading bahasa. Thread yang digunakan untuk paralelisme sering ditangani pada level OS sehingga OS dapat memetakannya ke beberapa core. Utas semacam itu mahal untuk dibuat dan dialihkan. Utas untuk konkurensi (interleaving dua tugas, tidak harus menjalankan keduanya secara bersamaan) dapat diimplementasikan pada tingkat bahasa / VM dan dapat dibuat sangat "murah" untuk menghasilkan dan beralih di antara sehingga Anda dapat, katakanlah, berbicara dengan 10 soket jaringan lebih atau kurang secara bersamaan, tetapi Anda tidak perlu mendapatkan lebih banyak CPU throughput seperti itu.
Doval
3
Selain itu, yang lain benar tentang thread menjadi cara yang salah untuk menangani memainkan banyak suara sekaligus.
Doval
3
Apakah Anda terbiasa dengan cara kerja gelombang suara? Biasanya Anda membuat akor dengan menyusun nilai dua gelombang suara (ditentukan pada bitrate yang sama) bersama-sama menjadi gelombang suara baru. Gelombang kompleks dapat dibangun dari yang sederhana; Anda hanya membutuhkan satu gelombang tunggal untuk memutar lagu.
KChaloux
Karena Anda mengatakan bahwa note.play () tidak asinkron, maka utas untuk setiap note.play () karenanya merupakan pendekatan untuk memainkan beberapa catatan secara bersamaan. KECUALI .., Anda dapat menggabungkan catatan itu menjadi satu yang kemudian Anda mainkan dalam satu utas. Jika itu tidak mungkin maka dengan pendekatan Anda, Anda harus menggunakan beberapa mekanisme untuk memastikan mereka tetap sinkron
pnizzle

Jawaban:

46

Satu asumsi yang Anda buat mungkin tidak valid: Anda memerlukan (antara lain) agar thread Anda dieksekusi secara bersamaan. Mungkin berfungsi untuk 3, tetapi pada titik tertentu sistem akan perlu memprioritaskan utas mana yang akan dijalankan pertama, dan yang mana yang menunggu.

Implementasi Anda pada akhirnya akan bergantung pada API Anda, tetapi sebagian besar API modern akan memberi tahu Anda terlebih dahulu apa yang ingin Anda mainkan dan mengurus waktu serta antri sendiri. Jika Anda sendiri yang membuat kode API semacam itu, mengabaikan API sistem yang ada (kenapa Anda mau ?!), antrian acara yang mencampur catatan Anda dan memutarnya dari satu utas tampak seperti pendekatan yang lebih baik daripada model utas per catatan.

ptyx
sumber
36
Atau untuk menyatakannya secara berbeda - sistem tidak akan dan tidak dapat menjamin urutan, urutan, atau durasi utas sekali dimulai.
James Anderson
2
@JamesAnderson kecuali jika seseorang akan melakukan upaya besar-besaran dalam sinkronisasi, yang pada akhirnya akan berakhir secara hampir sama sekali lagi.
Markus
Maksudnya 'API' maksud Anda perpustakaan audio yang saya gunakan?
Aviv Cohn
1
@Prog Ya. Saya cukup yakin ia memiliki sesuatu yang lebih nyaman note.play ()
ptyx
26

Jangan mengandalkan utas yang mengeksekusi berbaris. Setiap sistem operasi yang saya kenal tidak membuat jaminan bahwa thread dijalankan dalam waktu yang konsisten satu sama lain. Ini berarti bahwa jika CPU menjalankan 10 utas, mereka tidak harus menerima waktu yang sama dalam detik yang diberikan. Mereka dapat dengan cepat keluar dari selaras, atau mereka dapat tetap selaras sempurna. Itulah sifat utas: tidak ada yang dijamin, karena perilaku pelaksanaannya tidak menentukan .

Untuk aplikasi khusus ini, saya yakin Anda perlu memiliki utas tunggal yang mengkonsumsi catatan musik. Sebelum dapat mengkonsumsi catatan, beberapa proses lain perlu menggabungkan instrumen, staf, apa pun menjadi satu karya musik .

Mari kita asumsikan bahwa Anda memiliki misalnya tiga utas yang menghasilkan catatan. Saya akan menyinkronkan mereka pada struktur data tunggal di mana mereka semua dapat menempatkan musik mereka. Utas lain (konsumen) membaca catatan gabungan dan memainkannya. Mungkin perlu ada penundaan singkat di sini untuk memastikan bahwa waktu utas tidak menyebabkan catatan hilang.

Bacaan terkait: masalah produsen-konsumen .


sumber
1
Terimakasih telah menjawab. Saya tidak 100% yakin saya mengerti Anda menjawab, tetapi saya ingin memastikan Anda mengerti mengapa saya perlu 3 utas untuk memainkan 3 catatan pada saat yang sama: itu karena memanggil note.play()membekukan utas sampai not selesai dimainkan. Jadi agar saya dapat membuat play()3 catatan sekaligus, saya perlu 3 utas berbeda untuk melakukan ini. Apakah solusi Anda mengatasi situasi saya?
Aviv Cohn
Tidak, dan itu tidak jelas dari pertanyaan. Apakah tidak ada cara untuk utas memainkan akord?
4

Pendekatan klasik untuk masalah musik ini adalah menggunakan tepat dua utas. Satu, utas prioritas yang lebih rendah akan menangani UI atau kode penghasil suara

void playProgression(Progression prog){
    for(Chord chord : prog)
        for(Note note : chord)
            otherthread.startPlaying(note);
}

(catat konsep hanya memulai not secara sinkron dan berlangsung tanpa menunggu sampai selesai)

dan yang kedua, utas realtime secara konsisten akan melihat semua catatan, suara, sampel, dll yang seharusnya diputar sekarang; campur mereka dan hasilkan bentuk gelombang terakhir. Bagian ini dapat (dan sering) diambil dari perpustakaan pihak ketiga yang dipoles.

Utas itu seringkali sangat sensitif terhadap "kelaparan" sumber daya - jika pemrosesan output suara aktual diblokir lebih lama dari output buffered kartu suara Anda, maka hal itu akan menyebabkan artefak yang dapat didengar seperti gangguan atau muncul. Jika Anda memiliki 24 utas yang secara langsung menghasilkan audio, maka Anda memiliki peluang yang jauh lebih tinggi bahwa salah satu dari mereka akan gagap di beberapa titik. Ini sering dianggap tidak dapat diterima, karena manusia agak sensitif terhadap gangguan audio (lebih dari artefak visual) dan bahkan melihat gagap kecil.

Peter adalah
sumber
1
OP mengatakan API hanya dapat memainkan satu not pada satu waktu
Mooing Duck
4
@ MoooDuck jika API dapat bercampur, maka itu harus bercampur; jika OP mengatakan bahwa API tidak dapat mencampur maka solusinya adalah mencampur kode Anda dan membuat utas lain melakukan my_mixed_notes_from_whole_chord_progress.play () melalui api.
Peteris
1

Ya, Anda melakukan sesuatu yang salah.

Hal pertama adalah membuat Threads itu mahal. Ini memiliki overhead lebih dari sekedar memanggil fungsi.

Jadi yang harus Anda lakukan, jika Anda membutuhkan banyak utas untuk pekerjaan ini adalah mendaur ulang utas.

Seperti yang terlihat oleh saya, Anda memiliki satu utas utama yang melakukan penjadwalan utas lainnya. Jadi utas utama memimpin melalui musik dan memulai utas baru setiap kali ada not baru untuk dimainkan. Jadi, daripada membiarkan utas mati dan memulai kembali, lebih baik menjaga utas lainnya tetap hidup dalam lingkaran tidur, di mana mereka memeriksa setiap x milidetik (atau nanodetik) jika ada catatan baru untuk dimainkan dan jika tidak tidur. Maka utas utama tidak memulai utas baru tetapi memberi tahu kumpulan utas yang ada untuk memutar catatan. Hanya jika tidak ada cukup utas di kumpulan itu, ia dapat membuat utas baru.

Yang berikutnya adalah sinkronisasi. Hampir tidak ada sistem multithreading modern yang benar-benar menjamin bahwa semua utas dieksekusi pada saat yang sama. Jika Anda memiliki lebih banyak utas dan proses yang berjalan pada mesin daripada inti (yang sebagian besar terjadi), maka utas tidak mendapatkan 100% dari waktu CPU. Mereka harus berbagi CPU. Ini berarti setiap utas mendapat sejumlah kecil waktu CPU dan kemudian setelah itu bagikan utas berikutnya mendapatkan CPU untuk waktu yang singkat. Sistem tidak menjamin bahwa utas Anda mendapatkan waktu CPU yang sama dengan utas lainnya. Ini berarti, satu utas mungkin menunggu yang lain untuk selesai, dan dengan demikian ditunda.

Anda seharusnya melihat jika tidak mungkin memainkan beberapa catatan pada satu utas, sehingga utas menyiapkan semua catatan dan kemudian hanya memberikan perintah "mulai".

Jika Anda perlu melakukannya dengan utas, setidaknya gunakan kembali utas. Maka Anda tidak perlu memiliki utas sebanyak ada catatan di keseluruhan, tetapi hanya perlu sebanyak utas sebagai jumlah maksimum not yang dimainkan pada saat yang sama.

Dakkaron
sumber
"[Apakah] mungkin memainkan beberapa catatan pada satu utas, sehingga utas menyiapkan semua not dan kemudian hanya memberikan perintah" mulai "." - Ini adalah pikiran pertamaku juga. Saya kadang bertanya-tanya apakah dibombardir dengan komentar tentang multithreading (mis. Programer.stackexchange.com/questions/43321/… ) tidak menyebabkan banyak programmer tersesat selama desain. Saya skeptis terhadap proses besar di muka yang akhirnya menunjukkan perlunya banyak thread. Saya akan menyarankan mencari solusi single-thread dengan seksama.
user1172763
1

Jawaban pragmatis adalah bahwa jika itu berfungsi untuk aplikasi Anda dan memenuhi persyaratan Anda saat ini maka Anda tidak melakukannya salah :) Namun jika Anda maksudkan "apakah ini solusi yang dapat diukur, efisien, dapat digunakan kembali" maka jawabannya adalah tidak. Desain membuat banyak asumsi tentang bagaimana ulir akan berperilaku yang mungkin atau mungkin tidak benar dalam situasi yang berbeda (melodi yang lebih panjang, nada yang lebih simultan, perangkat keras yang berbeda, dll.).

Sebagai alternatif, pertimbangkan untuk menggunakan timing loop dan pool thread . Loop waktu berjalan di utasnya sendiri dan terus memeriksa apakah catatan perlu dimainkan. Ini dilakukan dengan membandingkan waktu sistem dengan waktu melodi dimulai. Waktu setiap not biasanya dapat dihitung dengan sangat mudah dari tempo melodi dan urutan not. Karena catatan baru perlu dimainkan, pinjam utas dari kumpulan utas dan hubungiplay() fungsi pada catatan tersebut. Loop waktu dapat bekerja dalam berbagai cara tetapi yang paling sederhana adalah loop kontinu dengan tidur pendek (mungkin antara akor) untuk meminimalkan penggunaan CPU.

Keuntungan dari desain ini adalah bahwa jumlah utas tidak akan melebihi jumlah maksimum catatan simultan +1 (loop pengaturan waktu). Juga loop waktu melindungi Anda terhadap selip dalam timing yang bisa disebabkan oleh latensi utas. Ketiga, tempo melodi tidak tetap dan dapat diubah dengan mengubah perhitungan timing.

Selain itu, saya setuju dengan komentator lain bahwa fungsinya note.play()adalah API yang sangat buruk untuk bekerja dengannya. Semua API yang masuk akal akan memungkinkan Anda untuk mencampur dan menjadwalkan catatan dengan cara yang jauh lebih fleksibel. Yang mengatakan, kadang-kadang kita harus hidup dengan apa yang kita punya :)

John
sumber
0

Tampak baik bagi saya sebagai implementasi sederhana, dengan asumsi itu adalah API yang harus Anda gunakan . Jawaban lain mencakup mengapa ini bukan API yang sangat baik, jadi saya tidak berbicara tentang itu lagi, saya hanya berasumsi bahwa Anda harus hidup dengan itu. Pendekatan Anda akan menggunakan sejumlah besar utas, tetapi pada PC modern yang seharusnya tidak menjadi perhatian, selama jumlah utas ada di lusinan.

Satu hal yang harus Anda lakukan, jika memungkinkan (seperti, bermain dari file alih-alih pengguna menekan keyboard), adalah menambahkan beberapa latensi. Jadi Anda memulai utas, ia tidur sampai waktu tertentu dari jam sistem, dan mulai memutar catatan pada waktu yang tepat (perhatikan, terkadang tidur mungkin terganggu lebih awal, jadi periksa jam dan tidur lebih banyak jika diperlukan). Meskipun tidak ada jaminan bahwa OS akan melanjutkan utas tepat ketika tidur Anda berakhir (kecuali jika Anda menggunakan sistem operasi waktu-nyata), kemungkinan besar akan jauh lebih akurat daripada jika Anda baru saja memulai utas dan mulai bermain tanpa memeriksa waktunya. .

Kemudian langkah selanjutnya, yang menyulitkan hal-hal sedikit tetapi tidak terlalu banyak, dan akan memungkinkan Anda untuk mengurangi latensi yang disebutkan di atas adalah, gunakan kumpulan thread. Yaitu, ketika utas menyelesaikan catatan, itu tidak keluar, tetapi menunggu not baru untuk dimainkan. Dan ketika Anda meminta mulai memainkan catatan, Anda terlebih dahulu mencoba mengambil utas gratis dari kolam, dan menambahkan utas baru hanya jika diperlukan. Ini akan membutuhkan beberapa komunikasi antar thread sederhana, tentu saja, alih-alih pendekatan api-dan-lupa Anda saat ini.

Hyde
sumber