Buku "Kode kinerja tinggi pada platform .NET. Edisi ke-2

gambar Buku ini akan mengajarkan Anda bagaimana memaksimalkan kinerja kode terkelola, idealnya tanpa mengorbankan manfaat dari lingkungan .NET, atau dalam kasus terburuk, mengorbankan sejumlah kecil dari mereka. Anda akan mempelajari metode pemrograman rasional, mencari tahu apa yang harus dihindari dan, kemungkinan besar, yang paling penting, cara menggunakan alat yang tersedia secara bebas untuk dengan mudah mengukur tingkat produktivitas. Materi pelatihan akan memiliki air minimum - hanya yang paling diperlukan. Buku ini memberikan apa yang perlu Anda ketahui, itu relevan dan ringkas, tidak mengandung terlalu banyak. Sebagian besar bab dimulai dengan informasi umum dan latar belakang, diikuti oleh tip-tip spesifik, ditetapkan seperti resep, dan diakhiri dengan pengukuran langkah-demi-langkah dan bagian debugging untuk berbagai skenario.

Sepanjang jalan, Ben Watson akan terjun ke komponen tertentu dari lingkungan .NET, khususnya, Common Language Runtime (CLR) berdasarkan itu, dan kita akan melihat bagaimana memori mesin Anda dikelola, kode dihasilkan, eksekusi multi-threaded diatur, dan banyak lagi yang dilakukan . Anda akan diperlihatkan bagaimana arsitektur .NET sekaligus membatasi alat perangkat lunak Anda dan menyediakannya dengan fitur tambahan dan bagaimana pilihan jalur pemrograman dapat secara signifikan mempengaruhi kinerja keseluruhan aplikasi. Sebagai bonus, penulis akan berbagi dengan Anda cerita dari pengalaman menciptakan sistem .NET yang sangat besar, kompleks, berkinerja tinggi di Microsoft selama sembilan tahun terakhir.

Kutipan: Pilih ukuran kumpulan thread yang sesuai


Seiring waktu, thread pool dikonfigurasikan secara independen, tetapi pada awalnya ia tidak memiliki sejarah dan itu akan dimulai pada keadaan awal. Jika produk perangkat lunak Anda sangat tidak sinkron dan menggunakan prosesor terpusat secara signifikan, produk ini mungkin menderita biaya peluncuran awal yang sangat tinggi, sambil menunggu pembuatan dan ketersediaan lebih banyak utas. Menyesuaikan parameter awal sehingga dari saat aplikasi diluncurkan Anda akan memiliki sejumlah utas siap pakai di ujung jari Anda:

const int MinWorkerThreads = 25; const int MinIoThreads = 25; ThreadPool.SetMinThreads(MinWorkerThreads, MinIoThreads); 

Hati-hati di sini. Saat menggunakan objek Task, pengiriman mereka akan didasarkan pada jumlah utas yang tersedia untuk ini. Jika ada terlalu banyak dari mereka, objek Tugas dapat menjalani penjadwalan yang berlebihan, yang setidaknya akan menyebabkan penurunan efisiensi prosesor pusat karena lebih sering beralih konteks. Jika beban kerja tidak terlalu tinggi, kumpulan thread dapat beralih menggunakan algoritme yang dapat mengurangi jumlah utas, membawanya ke jumlah yang lebih rendah dari yang ditentukan.

Anda juga dapat mengatur angka maksimumnya menggunakan metode SetMaxThreads, tetapi teknik ini memiliki risiko serupa.

Untuk mengetahui jumlah utas yang diperlukan, biarkan parameter ini sendirian dan analisis aplikasi Anda dalam keadaan stabil menggunakan metode ThreadPool.GetMaxThreads dan ThreadPool.GetMinThreads atau penghitung kinerja yang menunjukkan jumlah utas yang terlibat dalam proses.

Jangan menyela arus


Mengganggu pekerjaan utas tanpa koordinasi dengan pekerjaan utas lain adalah prosedur yang agak berbahaya. Streaming harus membersihkan diri mereka sendiri, dan memanggil mereka metode Abort tidak memungkinkan mereka untuk menutup tanpa konsekuensi negatif. Ketika sebuah utas dihancurkan, bagian-bagian aplikasi berada dalam kondisi yang tidak ditentukan. Akan lebih baik untuk crash program, tetapi idealnya restart bersih diperlukan.

Untuk mematikan utas dengan aman, Anda perlu menggunakan beberapa jenis status bersama, dan fungsi utas itu sendiri harus memeriksa status ini untuk menentukan kapan harus dimatikan. Keamanan harus dicapai melalui koherensi.

Secara umum, Anda harus selalu menggunakan objek Tugas - API tidak disediakan untuk mengganggu tugas. Agar dapat menghentikan utas secara konsisten, Anda harus, seperti yang disebutkan sebelumnya, menggunakan token PembatalanToken.

Jangan ubah prioritas utas


Secara umum, mengubah prioritas utas adalah upaya yang sangat tidak berhasil. Di Windows, pengiriman utas dilakukan sesuai dengan tingkat prioritasnya. Jika utas prioritas tinggi selalu siap dijalankan, maka utas prioritas rendah akan diabaikan dan sangat jarang mereka akan mendapatkan kesempatan untuk menjalankan. Dengan meningkatkan prioritas utas, Anda mengatakan bahwa pekerjaannya harus didahulukan dari semua pekerjaan lain, termasuk proses lainnya. Ini tidak aman untuk sistem yang stabil.

Lebih baik menurunkan prioritas utas jika menjalankan sesuatu yang dapat menunggu hingga tugas-tugas prioritas normal diselesaikan. Salah satu alasan bagus untuk menurunkan prioritas utas mungkin untuk menemukan utas yang tidak terkontrol yang menjalankan loop tak terbatas. Tidak mungkin untuk menghentikan utas dengan aman, jadi satu-satunya cara untuk mengembalikan utas dan sumber daya prosesor yang diberikan adalah memulai kembali proses. Sampai menjadi mungkin untuk menutup aliran dan melakukannya dengan bersih, menurunkan prioritas aliran di luar kendali akan menjadi cara yang masuk akal untuk meminimalkan konsekuensinya. Perlu dicatat bahwa bahkan utas dengan prioritas yang lebih rendah masih akan dijamin berjalan seiring waktu: semakin lama mereka kehilangan permulaan, semakin tinggi prioritas dinamis mereka akan ditetapkan oleh Windows. Pengecualian adalah prioritas siaga THREAD_ - PRIORITY_IDLE, di mana sistem operasi hanya menjadwalkan utas untuk dieksekusi ketika secara harfiah tidak ada lagi yang harus dimulai.

Mungkin ada alasan yang dibenarkan untuk meningkatkan prioritas aliran, misalnya, kebutuhan untuk cepat menanggapi situasi langka. Tetapi menggunakan teknik seperti itu harus sangat hati-hati. Utas pengiriman di Windows dilakukan terlepas dari proses yang menjadi bagiannya, sehingga utas prioritas tinggi dari proses Anda akan diluncurkan dengan mengorbankan tidak hanya utas lainnya, tetapi juga semua utas dari aplikasi lain yang berjalan pada sistem Anda.

Jika kolam utas digunakan, maka setiap perubahan prioritas dibuang setiap kali utas kembali ke kolam. Jika Anda terus mengelola utas dasar saat menggunakan pustaka Tugas Paralel, Anda harus ingat bahwa beberapa tugas dapat diluncurkan di utas yang sama sebelum dikembalikan ke kumpulan.

Sinkronisasi dan pemblokiran utas


Segera setelah percakapan muncul di beberapa utas, menjadi perlu untuk menyinkronkannya. Sinkronisasi terdiri dari penyediaan akses hanya satu utas ke status bersama, misalnya, ke bidang kelas. Biasanya, utas disinkronkan menggunakan objek sinkronisasi seperti Monitor, Semaphore, ManualResetEvent, dll. Kadang-kadang mereka disebut kunci secara tidak resmi, dan proses sinkronisasi dalam utas tertentu disebut kunci.

Salah satu kebenaran mendasar tentang kunci adalah ini: mereka tidak pernah meningkatkan kinerja. Dalam skenario kasus terbaik - dengan primitif sinkronisasi yang diimplementasikan dengan baik dan tidak ada persaingan - pemblokiran dapat menjadi netral. Ini mengarah pada penghentian pelaksanaan pekerjaan yang bermanfaat oleh utas lain dan fakta bahwa waktu CPU terbuang, meningkatkan konteks waktu pengalihan dan menyebabkan konsekuensi negatif lainnya. Anda harus menerima ini karena kebenaran jauh lebih penting daripada kinerja sederhana. Apakah hasil yang salah dihitung dengan cepat tidak masalah!

Sebelum Anda mulai memecahkan masalah menggunakan peralatan kunci, kami akan mempertimbangkan prinsip-prinsip paling mendasar.

Apakah saya perlu peduli dengan kinerja?


Membenarkan kebutuhan untuk meningkatkan produktivitas terlebih dahulu. Ini membawa kita kembali ke prinsip-prinsip yang dibahas dalam bab 1. Kinerja tidak sama pentingnya untuk semua kode aplikasi Anda. Tidak semua kode harus menjalani optimasi tingkat ke-n. Sebagai aturan, semuanya dimulai dengan "loop dalam" - kode yang paling sering dieksekusi atau paling penting untuk kinerja - dan menyebar ke segala arah hingga biaya melebihi manfaat yang diterima. Ada banyak area dalam kode yang kurang penting dalam hal kinerja. Dalam situasi seperti itu, jika Anda membutuhkan kunci, dengan tenang menerapkannya.

Dan sekarang kamu harus hati-hati. Jika kode non-kritis Anda dijalankan di utas dari utas dan Anda memblokirnya untuk waktu yang lama, kumpulan utas mungkin mulai memasukkan lebih banyak utas untuk menangani permintaan lainnya. Jika satu atau dua utas melakukan ini dari waktu ke waktu, tidak apa-apa. Tetapi jika banyak thread melakukan hal-hal seperti itu, masalah mungkin timbul, karena karena ini, sumber daya yang harus melakukan pekerjaan nyata dihabiskan sia-sia. Ketidaksengajaan ketika memulai suatu program dengan beban konstan yang signifikan dapat menyebabkan dampak negatif pada sistem bahkan dari bagian-bagian yang kinerjanya tinggi tidak penting, karena pengalihan konteks yang tidak perlu atau keterlibatan yang tidak masuk akal dari kumpulan ulir. Seperti dalam semua kasus lain, pengukuran harus dilakukan untuk menilai situasi.

Apakah Anda benar-benar membutuhkan kunci?


Mekanisme penguncian yang paling efektif adalah yang tidak. Jika Anda dapat sepenuhnya menghilangkan kebutuhan sinkronisasi utas, ini akan menjadi cara terbaik untuk mendapatkan kinerja tinggi. Ini adalah cita-cita yang tidak mudah dicapai. Biasanya ini berarti bahwa Anda perlu memastikan bahwa tidak ada status bersama yang dapat diubah - setiap permintaan yang melewati aplikasi Anda dapat diproses secara terpisah dari permintaan lain atau beberapa data yang dapat diubah (baca-tulis) terpusat. Fitur ini akan menjadi skenario terbaik untuk mencapai kinerja tinggi.

Dan tetap hati-hati. Dengan restrukturisasi, mudah untuk keluar dari jalur dan mengubah kode menjadi berantakan yang tak seorang pun, termasuk Anda, bisa mengetahuinya. Anda tidak boleh melangkah terlalu jauh kecuali produktivitas tinggi benar-benar merupakan faktor kritis dan tidak dapat dicapai sebaliknya. Ubah kode menjadi asinkron dan independen, tetapi agar tetap jelas.

Jika beberapa utas baru saja membaca dari variabel (dan tidak ada petunjuk untuk menulis dari suatu aliran), sinkronisasi tidak diperlukan. Semua utas dapat memiliki akses tanpa batas. Ini secara otomatis berlaku untuk objek yang tidak dapat diubah seperti string atau nilai dari tipe yang tidak dapat diubah, tetapi dapat berlaku untuk semua jenis objek jika Anda menjamin keabadian nilainya selama membaca oleh banyak utas.

Jika ada beberapa utas yang menulis ke beberapa variabel bersama, lihat apakah akses yang disinkronkan dapat dihilangkan dengan pindah menggunakan variabel lokal. Jika Anda dapat membuat salinan sementara untuk pekerjaan, kebutuhan untuk sinkronisasi akan hilang. Ini sangat penting untuk akses sinkronisasi yang berulang. Dari mengakses kembali variabel yang dibagikan, Anda harus pindah ke mengakses kembali variabel lokal mengikuti akses satu kali ke variabel yang dibagikan, seperti dalam contoh sederhana berikut untuk menambahkan item ke yang dibagikan oleh koleksi beberapa utas.

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { for (int j = 0; j < 5000000; j++) { lock (syncObj) { masterList.Add(j); } } }); } Task.WaitAll(tasks); 

Kode ini dapat dikonversi sebagai berikut:

 object syncObj = new object(); var masterList = new List<long >(); const int NumTasks = 8; Task[] tasks = new Task[NumTasks]; for (int i = 0; i < NumTasks; i++) { tasks[i] = Task.Run(()=> { var localList = new List<long >(); for (int j = 0; j < 5000000; j++) { localList.Add(j); } lock (syncObj) { masterList.AddRange(localList); } }); } Task.WaitAll(tasks); 

Di mesin saya, versi kedua kode berjalan lebih dari dua kali lebih cepat daripada yang pertama.
Pada akhirnya, keadaan bersama yang bisa berubah adalah musuh fundamental kinerja. Dibutuhkan sinkronisasi untuk keamanan data, yang menurunkan kinerja. Jika desain Anda memiliki setidaknya sedikit peluang untuk menghindari pemblokiran, maka Anda hampir menerapkan sistem multi-threaded yang ideal.

Sinkronkan Urutan Preferensi


Ketika memutuskan apakah jenis sinkronisasi apa pun diperlukan, harus dipahami bahwa tidak semuanya memiliki karakteristik kinerja atau perilaku yang sama. Dalam sebagian besar situasi, Anda hanya perlu menggunakan kunci, dan biasanya ini harus menjadi opsi asli. Penggunaan sesuatu selain pemblokiran, untuk menjustifikasi kompleksitas tambahan, membutuhkan pengukuran intensif. Secara umum, kami mempertimbangkan mekanisme sinkronisasi dalam urutan berikut.

1. Kunci / kelas Monitor - menjaga kesederhanaan, kelengkapan kode dan memberikan keseimbangan kinerja yang baik.

2. Kurangnya sinkronisasi. Singkirkan status yang dapat diubah bersama, restrukturisasi, dan optimalkan. Ini lebih sulit, tetapi jika berhasil, pada dasarnya ia akan bekerja lebih baik daripada menerapkan pemblokiran (kecuali ketika kesalahan dibuat atau arsitektur terdegradasi).

3. Metode saling bertautan yang sederhana - dalam beberapa skenario mungkin lebih cocok, tetapi begitu situasinya menjadi lebih rumit, lanjutkan menggunakan kunci kunci.

Dan akhirnya, jika Anda benar-benar dapat membuktikan manfaat dari penggunaannya, gunakan kunci yang lebih rumit dan kompleks (perlu diingat: mereka jarang menjadi berguna seperti yang Anda harapkan):

  1. kunci asinkron (akan dibahas nanti dalam bab ini);
  2. semua orang.

Keadaan tertentu dapat menentukan atau menghalangi penggunaan beberapa teknologi ini. Misalnya, menggabungkan beberapa metode yang saling terkait tidak akan mengungguli pernyataan kunci tunggal.

»Informasi lebih lanjut tentang buku ini dapat ditemukan di situs web penerbit
» Isi
» Kutipan

Kupon diskon 25% untuk penjaja - .NET

Setelah pembayaran versi kertas buku, sebuah buku elektronik dikirim melalui email.

Source: https://habr.com/ru/post/id458520/


All Articles