Mengapa multithreading sering lebih disukai untuk meningkatkan kinerja?

23

Saya punya pertanyaan, ini tentang mengapa programmer tampaknya menyukai concurrency dan program multi-threaded secara umum.

Saya sedang mempertimbangkan 2 pendekatan utama di sini:

  • pendekatan async pada dasarnya berdasarkan sinyal, atau hanya pendekatan async seperti yang disebut oleh banyak makalah dan bahasa seperti C # 5.0 baru misalnya, dan "utas pengiring" yang mengelola kebijakan saluran pipa Anda
  • pendekatan konkuren atau pendekatan multi-threading

Saya hanya akan mengatakan bahwa saya berpikir tentang perangkat keras di sini dan skenario terburuk, dan saya telah menguji 2 paradigma ini sendiri, paradigma async adalah pemenang pada titik yang saya tidak mengerti mengapa orang 90% dari waktu berbicara tentang multi-threading ketika mereka ingin mempercepat atau memanfaatkan sumber daya mereka dengan baik.

Saya telah menguji program multi-threaded dan program async pada mesin lama dengan Intel quad-core yang tidak menawarkan pengontrol memori di dalam CPU, memori dikelola sepenuhnya oleh motherboard, nah dalam hal ini kinerja sangat buruk dengan aplikasi multi-utas, bahkan jumlah utas yang relatif rendah seperti 3-4-5 dapat menjadi masalah, aplikasi tidak responsif dan lambat serta tidak menyenangkan.

Pendekatan async yang baik adalah, di sisi lain, mungkin tidak lebih cepat tetapi juga tidak terburuk, aplikasi saya hanya menunggu hasilnya dan tidak hang, responsif dan ada skala yang lebih baik terjadi.

Saya juga telah menemukan bahwa perubahan konteks dalam dunia threading itu tidak semurah dalam skenario dunia nyata, itu sebenarnya cukup mahal terutama ketika Anda memiliki lebih dari 2 utas yang perlu siklus dan bertukar satu sama lain untuk dihitung.

Pada CPU modern, situasinya tidak terlalu berbeda, pengontrol memori terintegrasi tetapi poin saya adalah bahwa x86 CPU pada dasarnya adalah mesin seri dan pengontrol memori bekerja dengan cara yang sama seperti mesin lama dengan pengontrol memori eksternal pada motherboard. . Switch konteks masih merupakan biaya yang relevan dalam aplikasi saya dan fakta bahwa pengontrol memori terintegrasi atau bahwa CPU yang lebih baru memiliki lebih dari 2 inti itu tidak murah bagi saya.

Untuk apa yang saya alami, pendekatan konkuren baik dalam teori tetapi tidak begitu bagus dalam praktiknya, dengan model memori yang dipaksakan oleh perangkat keras, sulit untuk memanfaatkan paradigma ini dengan baik, juga memperkenalkan banyak masalah mulai dari penggunaan dari struktur data saya untuk bergabung dengan banyak utas.

Juga kedua paradigma tidak menawarkan keamanan apa pun ketika tugas atau pekerjaan akan dilakukan pada titik waktu tertentu, menjadikannya sangat mirip dari sudut pandang fungsional.

Menurut model memori X86, mengapa mayoritas orang menyarankan untuk menggunakan konkurensi dengan C ++ dan bukan hanya pendekatan async? Juga mengapa tidak mempertimbangkan skenario terburuk komputer di mana konteksnya mungkin lebih mahal daripada perhitungan itu sendiri?

pengguna1849534
sumber
2
Salah satu cara untuk membandingkan adalah dengan melihat dunia JavaScript, jika tidak ada threading dan semuanya secara asinkron secara agresif, menggunakan callback. Berhasil, tetapi memiliki masalah sendiri.
Gort the Robot
2
@ SvenvenBurnap Apa yang Anda sebut pekerja web?
user16764
2
"Bahkan jumlah utas yang relatif rendah seperti 3-4-5 bisa menjadi masalah, aplikasinya tidak responsif dan lambat dan tidak menyenangkan." => Mungkin karena desain yang buruk / penggunaan utas yang tidak tepat. Anda biasanya menemukan situasi seperti itu ketika utas Anda terus bertukar data, dalam hal ini multi threading mungkin bukan jawaban yang tepat atau Anda mungkin perlu mempartisi ulang data.
assylias
1
@assylias Untuk melihat pelambatan signifikan pada utas UI menunjukkan terlalu banyak penguncian pada utas. Anda memiliki implementasi yang buruk atau Anda mencoba menumbuk pasak persegi ke dalam lubang bundar.
Evan Plaice
5
Anda mengatakan "programmer tampaknya menyukai concurrency dan program multi-threaded secara umum" Saya ragu itu. Saya akan mengatakan "programmer membencinya" ... tapi sering itu satu-satunya hal yang berguna untuk dilakukan ...
johannes

Jawaban:

34

Anda memiliki beberapa core / procesors, menggunakan mereka

Async adalah yang terbaik untuk melakukan pemrosesan IO yang berat, tetapi bagaimana dengan pemrosesan CPU yang berat?

Masalah muncul ketika blok kode single-threaded (yaitu macet) pada proses yang berjalan lama. Misalnya, ingat kembali ketika mencetak dokumen pengolah kata akan membuat seluruh aplikasi membeku sampai pekerjaan itu dikirim? Pembekuan aplikasi adalah efek samping dari pemblokiran aplikasi berulir tunggal selama tugas intensif CPU.

Dalam aplikasi multi-utas, tugas-tugas yang intensif CPU (ex sebuah pekerjaan cetak) dapat dikirim ke utas pekerja latar belakang sehingga membebaskan utas UI.

Demikian juga, dalam aplikasi multi-proses pekerjaan dapat dikirim melalui olahpesan (ex IPC, soket, dll) ke subproses yang dirancang khusus untuk memproses pekerjaan.

Dalam praktiknya, masing-masing kode async dan multi-threaded / proses memiliki kelebihan dan kekurangan masing-masing.

Anda dapat melihat tren di platform cloud utama, karena mereka akan menawarkan contoh khusus untuk pemrosesan terikat CPU dan contoh khusus untuk pemrosesan terikat IO.

Contoh:

  • Penyimpanan (ex Amazon S3, Google Cloud Drive) terikat CPU
  • Server Web terikat IO (Amazon EC2, Google App Engine)
  • Basis data keduanya, CPU terikat untuk penulisan / pengindeksan dan IO terikat untuk dibaca

Untuk memasukkannya ke dalam perspektif ...

Server web adalah contoh sempurna platform yang sangat terikat IO. Webserver multi-utas yang memberikan satu utas per koneksi tidak menskala dengan baik karena setiap utas mengeluarkan lebih banyak biaya karena peningkatan jumlah pengalihan konteks dan penguncian utas pada sumber daya bersama. Sedangkan server web async akan menggunakan ruang alamat tunggal.

Demikian juga, aplikasi khusus untuk encoding video akan bekerja lebih baik di lingkungan multi-threaded karena pemrosesan yang berat akan mengunci utas utama sampai pekerjaan selesai. Ada beberapa cara untuk memitigasi hal ini, tetapi jauh lebih mudah untuk memiliki satu thread yang mengelola antrian, pembersihan thread yang kedua, dan kumpulan thread yang mengelola proses yang berat. Komunikasi antara utas terjadi hanya ketika tugas ditetapkan / diselesaikan sehingga overhead penguncian benang dijaga seminimal mungkin.

Aplikasi terbaik sering menggunakan kombinasi keduanya. Webapp, misalnya dapat menggunakan nginx (yaitu async single-threaded) sebagai penyeimbang beban untuk mengelola torrent permintaan yang masuk, server web async yang sama (ex Node.js) untuk menangani permintaan http, dan satu set server multi-threaded menangani pengunggahan / streaming / penyandian konten, dll ...

Ada banyak perang agama selama bertahun-tahun antara model multi-threaded, multi-proses, dan async. Seperti kebanyakan hal, jawaban terbaik seharusnya, "itu tergantung."

Ini mengikuti garis pemikiran yang sama yang membenarkan menggunakan arsitektur GPU dan CPU secara paralel. Dua sistem khusus yang berjalan dalam konser dapat memiliki peningkatan yang jauh lebih besar daripada pendekatan monolitik tunggal.

Tidak ada yang lebih baik karena keduanya memiliki kegunaannya. Gunakan alat terbaik untuk pekerjaan itu.

Memperbarui:

Saya menghapus referensi ke Apache dan membuat koreksi kecil. Apache menggunakan model multiproses yang melakukan proses pada setiap permintaan untuk meningkatkan jumlah pengalihan konteks pada level kernel. Selain itu, karena memori tidak dapat dibagi di seluruh proses, setiap permintaan dikenakan biaya memori tambahan.

Multi-threading memerlukan memori tambahan karena bergantung pada memori bersama antara utas. Memori bersama menghilangkan overhead memori tambahan tetapi masih dikenakan penalti peningkatan konteks switching. Selain itu - untuk memastikan bahwa kondisi balapan tidak terjadi - kunci utas (yang memastikan akses eksklusif ke hanya satu utas pada satu waktu) diperlukan untuk setiap sumber daya yang dibagikan melintasi utas.

Sangat lucu bahwa Anda berkata, "programmer tampaknya menyukai concurrency dan program multi-threaded secara umum." Pemrograman multi-utas secara universal ditakuti oleh siapa saja yang telah melakukan jumlah substansial dalam waktu mereka. Kunci mati (bug yang terjadi ketika suatu sumber daya dikunci secara keliru oleh dua sumber berbeda yang menghalangi keduanya dari saat selesai) dan kondisi balapan (di mana program akan secara salah menampilkan hasil yang salah secara acak karena urutan yang salah) adalah beberapa yang paling sulit dilacak. turun dan perbaiki.

Pembaruan2:

Berlawanan dengan pernyataan selimut tentang IPC yang lebih cepat dari komunikasi jaringan (mis. Soket). Itu tidak selalu terjadi . Perlu diingat bahwa ini adalah generalisasi dan detail khusus implementasi mungkin memiliki dampak besar pada hasilnya.

Evan Plaice
sumber
mengapa seorang programmer harus multi-proses? Maksud saya, saya berasumsi bahwa dengan lebih dari 1 proses Anda juga memerlukan semacam komunikasi antar-proses yang dapat menambah overhead yang signifikan, apakah ini sesuatu seperti cara windows-programmer lama dalam melakukan sesuatu? kapan saya harus pergi multi-proses? Terima kasih atas balasan Anda, gambar yang bagus untuk apa async dan multi-threaded bagus.
user1849534
1
Anda mengasumsikan bahwa komunikasi antarproses akan meningkatkan overhead keseluruhan. Namun jika kondisi pemrosesan tidak dapat diubah, atau hanya perlu menangani sinkronisasi saat mulai / selesai. bisa jauh lebih efisien untuk melakukan tugas-tugas yang lebih paralel. Pola aktor adalah contoh yang baik, dan jika Anda belum membacanya - itu sangat layak untuk dibaca. akka.io
sylvanaar
1
@ user1849534 Beberapa utas dapat berbicara satu sama lain melalui memori bersama + penguncian atau IPC. Mengunci lebih mudah tetapi lebih sulit untuk di-debug jika Anda membuat kesalahan (mis, melewatkan kunci, kunci mati). IPC adalah yang terbaik jika Anda memiliki banyak utas pekerja karena penguncian tidak berskala baik. Either way, jika Anda menggunakan pendekatan multi-threaded, penting untuk menjaga komunikasi / sinkronisasi di utas menjadi minimum absolut (yaitu untuk meminimalkan overhead).
Evan Plaice
1
@ akka.io Kamu sepenuhnya benar. Kekekalan adalah salah satu cara untuk meminimalkan / menghilangkan overhead dari penguncian tetapi Anda masih dikenakan biaya waktu pengalihan konteks. Jika Anda ingin memperluas jawaban untuk memasukkan perincian tentang bagaimana ketidakberdayaan dapat menyelesaikan masalah sinkronisasi utas, jangan ragu. Poin utama yang ingin saya ilustrasikan adalah, ada beberapa kasus di mana komunikasi async memiliki keunggulan berbeda dibandingkan multi-threaded / proses dan sebaliknya.
Evan Plaice
(lanjutan) Tapi, jujur ​​jika saya membutuhkan banyak kemampuan pemrosesan yang terikat CPU, saya akan melewatkan model aktor dan membuatnya untuk mampu menskalakan ke beberapa node jaringan. Solusi terbaik yang saya lihat untuk ini adalah menggunakan model ventilator tugas 0MQ melalui komunikasi tingkat soket. Lihat Gambar 5 @ zguide.zeromq.org/page:all .
Evan Plaice
13

Pendekatan asinkron Microsoft adalah pengganti yang baik untuk tujuan paling umum dari pemrograman multithread: meningkatkan respons terhadap tugas-tugas IO.

Namun, penting untuk menyadari bahwa pendekatan asinkron tidak mampu meningkatkan kinerja sama sekali, atau meningkatkan daya tanggap sehubungan dengan tugas-tugas intensif CPU.

Multithreading untuk Responsif

Multithreading untuk responsif adalah cara tradisional untuk membuat program responsif selama tugas IO yang berat atau tugas komputasi yang berat. Anda menyimpan file di utas latar belakang, sehingga pengguna dapat melanjutkan pekerjaan mereka, tanpa harus menunggu hard drive menyelesaikan tugasnya. Utas IO sering memblokir menunggu sebagian penulisan selesai, sehingga sakelar konteks sering dilakukan.

Demikian pula, ketika melakukan perhitungan yang rumit, Anda ingin mengizinkan pengalihan konteks biasa sehingga UI dapat tetap responsif, dan pengguna tidak menganggap programnya macet.

Tujuannya di sini bukan, secara umum, untuk membuat banyak utas berjalan pada CPU yang berbeda. Alih-alih, kami hanya tertarik untuk membuat sakelar konteks terjadi antara tugas latar belakang yang telah lama berjalan dan UI, sehingga UI dapat memperbarui dan merespons pengguna saat tugas latar belakang sedang berjalan. Secara umum, UI tidak akan memakan banyak daya CPU, dan kerangka kerja threading atau OS biasanya akan memutuskan untuk menjalankannya pada CPU yang sama.

Kami benar-benar kehilangan kinerja keseluruhan karena biaya tambahan untuk pengalihan konteks, tetapi kami tidak peduli karena kinerja CPU bukanlah tujuan kami. Kami tahu bahwa kami biasanya memiliki daya CPU lebih dari yang kami butuhkan, dan tujuan kami sehubungan dengan multithreading adalah menyelesaikan tugas bagi pengguna tanpa membuang waktu pengguna.

Alternatif "Asinkron"

"Pendekatan asinkron" mengubah gambar ini dengan mengaktifkan sakelar konteks dalam satu utas. Ini menjamin bahwa semua tugas kami akan berjalan pada satu CPU, dan dapat memberikan beberapa peningkatan kinerja sederhana dalam hal pembuatan / pembersihan thread yang lebih sedikit dan lebih sedikit saklar konteks nyata antar thread.

Alih-alih membuat utas baru untuk menunggu penerimaan sumber daya jaringan (misalnya mengunduh gambar), asyncmetode digunakan, yang awaitgambarnya tersedia, dan, sementara itu, menghasilkan metode panggilan.

Keuntungan utama di sini adalah Anda tidak perlu khawatir tentang masalah threading seperti menghindari kebuntuan, karena Anda tidak menggunakan kunci dan sinkronisasi sama sekali, dan ada sedikit pekerjaan untuk programmer mengatur utas latar belakang, dan mendapatkan kembali di utas UI saat hasilnya kembali untuk memperbarui UI dengan aman.

Saya belum melihat terlalu dalam ke detail teknis, tetapi kesan saya adalah bahwa mengelola unduhan dengan aktivitas CPU ringan sesekali menjadi tugas bukan untuk utas terpisah, melainkan sesuatu yang lebih seperti tugas pada antrian acara UI, dan ketika unduhan selesai, metode asinkron dilanjutkan dari antrian acara itu. Dengan kata lain, awaitberarti sesuatu yang mirip dengan "memeriksa apakah hasil yang saya butuhkan tersedia, jika tidak, masukkan saya kembali dalam antrian tugas utas ini".

Perhatikan bahwa pendekatan ini tidak akan menyelesaikan masalah tugas intensif-CPU: tidak ada data untuk ditunggu, jadi kami tidak bisa mendapatkan sakelar konteks yang kami butuhkan tanpa membuat thread pekerja latar belakang yang sebenarnya. Tentu saja, mungkin masih nyaman untuk menggunakan metode asinkron untuk memulai utas latar belakang dan mengembalikan hasilnya, dalam program yang secara luas menggunakan pendekatan asinkron.

Multithreading untuk Kinerja

Karena Anda berbicara tentang "kinerja", saya juga ingin membahas bagaimana multithreading dapat digunakan untuk peningkatan kinerja, sesuatu yang sama sekali tidak mungkin dengan pendekatan asinkron berulir tunggal.

Ketika Anda benar-benar dalam situasi di mana Anda tidak memiliki kekuatan CPU yang cukup pada satu CPU, dan ingin menggunakan multithreading untuk kinerja, sebenarnya sering kali sulit dilakukan. Di sisi lain, jika satu CPU tidak cukup daya pemrosesan, itu juga sering merupakan satu-satunya solusi yang dapat memungkinkan program Anda untuk melakukan apa yang ingin Anda capai dalam jangka waktu yang masuk akal, yang membuat pekerjaan menjadi berharga.

Paralelisme Trivial

Tentu saja, kadang-kadang bisa mudah untuk mendapatkan speedup nyata dari multithreading.

Jika Anda memiliki sejumlah besar tugas intensif komputasi independen (yaitu, tugas yang input dan output datanya sangat kecil sehubungan dengan perhitungan yang harus dilakukan untuk menentukan hasilnya), maka Anda seringkali dapat memperoleh percepatan signifikan dengan membuat kumpulan utas (berukuran tepat berdasarkan jumlah CPU yang tersedia), dan memiliki master utas mendistribusikan pekerjaan dan mengumpulkan hasilnya.

Multithreading Praktis untuk Kinerja

Saya tidak ingin mengajukan diri terlalu banyak sebagai ahli, tetapi kesan saya adalah bahwa, secara umum, multithreading paling praktis untuk kinerja yang terjadi hari ini adalah mencari tempat dalam aplikasi yang memiliki paralelisme sepele, dan menggunakan banyak utas. untuk menuai manfaat.

Seperti halnya optimasi apa pun, biasanya lebih baik untuk mengoptimalkan setelah Anda membuat profil kinerja program Anda, dan mengidentifikasi hot spot: mudah untuk memperlambat program dengan memutuskan secara sewenang-wenang bahwa bagian ini harus berjalan di satu utas dan bagian lain, tanpa pertama-tama menentukan apakah kedua bagian mengambil sebagian besar waktu CPU.

Utas tambahan berarti lebih banyak biaya setup / teardown, dan lebih banyak konteks beralih atau lebih banyak biaya komunikasi antar-CPU. Jika itu tidak melakukan pekerjaan yang cukup untuk menebus biaya-biaya itu jika pada CPU yang terpisah, dan tidak perlu menjadi utas terpisah untuk alasan responsif, itu akan memperlambat segalanya tanpa manfaat.

Cari tugas-tugas yang memiliki sedikit saling ketergantungan, dan yang mengambil porsi signifikan dari runtime program Anda.

Jika mereka tidak memiliki saling ketergantungan, maka itu adalah kasus paralelisme sepele, Anda dapat dengan mudah mengatur masing-masing dengan utas dan menikmati manfaatnya.

Jika Anda dapat menemukan tugas dengan saling ketergantungan terbatas, sehingga penguncian dan sinkronisasi untuk bertukar informasi tidak memperlambatnya secara signifikan, maka multithreading dapat mempercepat, asalkan Anda berhati-hati untuk menghindari bahaya kebuntuan karena kesalahan logika saat menyinkronkan atau hasil yang salah karena tidak disinkronkan saat diperlukan.

Atau, beberapa aplikasi yang lebih umum untuk multithreading tidak (dalam arti) mencari speedup dari algoritma yang telah ditentukan, tetapi sebaliknya untuk anggaran yang lebih besar untuk algoritma yang mereka rencanakan untuk ditulis: jika Anda menulis mesin gim , dan AI Anda harus membuat keputusan dalam frame rate Anda, Anda sering dapat memberi AI Anda siklus siklus CPU lebih besar jika Anda bisa memberikannya CPU sendiri.

Namun, pastikan untuk membuat profil utas dan memastikan bahwa mereka melakukan cukup banyak pekerjaan untuk menebus biaya di beberapa titik.

Algoritma Paralel

Ada juga banyak masalah yang dapat dipercepat dengan menggunakan beberapa prosesor, tetapi terlalu monolitik untuk hanya membagi antara CPU.

Algoritma paralel harus dianalisis dengan cermat untuk runtime O-besar mereka sehubungan dengan algoritma non-paralel terbaik yang tersedia, karena sangat mudah untuk biaya komunikasi antar-CPU untuk menghilangkan manfaat dari menggunakan beberapa CPU. Secara umum, mereka harus menggunakan lebih sedikit komunikasi antar-CPU (dalam istilah O-besar) daripada mereka menggunakan perhitungan pada setiap CPU.

Saat ini, sebagian besar masih merupakan ruang untuk penelitian akademis, sebagian karena analisis kompleks yang diperlukan, sebagian karena paralelisme sepele cukup umum, sebagian karena kami belum memiliki begitu banyak inti CPU di komputer kami yang bermasalah yang tidak dapat diselesaikan dalam kerangka waktu yang masuk akal pada satu CPU dapat diselesaikan dalam kerangka waktu yang wajar menggunakan semua CPU kami.

Theodore Murdock
sumber
Memberi +1 untuk jawaban yang dipikirkan dengan matang. Saya akan berhati-hati untuk mengambil saran Microsoft pada nilai nominalnya. Perlu diingat bahwa .NET adalah platform sinkron-pertama, oleh karena itu ekosistem bias menyediakan fasilitas / dokumentasi yang lebih baik yang mendukung pembangunan solusi sinkron. Yang sebaliknya akan berlaku untuk platform async-first seperti Node.js.
Evan Plaice
3

aplikasi tidak responsif dan lambat dan tidak menyenangkan.

Dan ini masalahnya. UI responsif tidak membuat aplikasi berkinerja. Seringkali sebaliknya. Banyak waktu dihabiskan untuk memeriksa input UI daripada meminta pekerja melakukan tugasnya.

Sejauh 'hanya' memiliki pendekatan async, itu multithreading juga meskipun tweak untuk satu use case tertentu di sebagian besar lingkungan . Di yang lain, async itu dilakukan melalui coroutine yang ... tidak selalu bersamaan.

Terus terang, saya menemukan operasi async menjadi lebih sulit untuk dipikirkan dan digunakan dengan cara yang benar-benar memberikan manfaat (kinerja, ketahanan, pemeliharaan) bahkan dibandingkan dengan ... pendekatan lebih manual.

Telastyn
sumber
kenapa misalnya apa yang Anda temukan sehingga pisang di perpustakaan boost signal2?
user1849534