Penggunaan libdispatch yang efektif

( Catatan: penulis bahan aslinya adalah Thomas @tclementdev, pengguna github dan twitter . Narasi orang pertama yang digunakan oleh penulis disimpan dalam terjemahan di bawah ini. )

Saya pikir sebagian besar pengembang menggunakan libdispatch secara tidak efisien karena cara itu diperkenalkan ke komunitas, juga karena dokumentasi dan API yang membingungkan. Saya sampai pada pemikiran ini setelah membaca diskusi tentang "konkurensi" di milis pengembangan Swift (evolusi cepat). Pesan dari Pierre Habouzit (Pierre Habouzit - terlibat dalam mendukung libdispatch di Apple) secara khusus tercerahkan:


Dia juga memiliki banyak tweet tentang topik ini:


Dibuat oleh saya:

  • Program harus memiliki sangat sedikit antrian menggunakan kumpulan global ( utas - kira-kira Per. ). Jika semua antrian ini aktif secara bersamaan, maka Anda akan menerima jumlah utas yang berjalan secara bersamaan. Antrian ini harus dianggap sebagai konteks eksekusi dalam program (GUI, penyimpanan, pekerjaan di latar belakang, ...) yang mendapat manfaat dari eksekusi bersamaan.
  • Mulai dengan eksekusi berurutan. Saat Anda menemukan masalah kinerja, lakukan pengukuran untuk mengetahui penyebabnya. Dan jika eksekusi paralel membantu, gunakan dengan hati-hati. Selalu pastikan bahwa kode paralel berfungsi di bawah tekanan dari sistem. Secara default, gunakan kembali antrian. Tambahkan baris ketika membawa manfaat terukur. Sebagian besar aplikasi tidak boleh menggunakan lebih dari tiga hingga empat antrian.
  • Antrian yang memiliki antrian lain ditetapkan sebagai target bekerja dengan baik dan skala.
    ( Catatan perev.: Tentang pengaturan antrian sebagai target untuk antrian lain dapat dibaca, misalnya, di sini . )
  • Jangan gunakan dispatch_get_global_queue (). Ini tidak sesuai dengan kualitas layanan dan prioritas dan dapat menyebabkan pertumbuhan eksplosif dalam jumlah arus. Alih-alih, jalankan kode Anda di salah satu konteks eksekusi Anda.
  • dispatch_async () adalah pemborosan sumber daya untuk blok kecil yang dapat dieksekusi (<1 ms), karena panggilan ini cenderung memerlukan utas baru karena semangat libdispatch yang berlebihan. Alih-alih mengalihkan konteks eksekusi untuk melindungi keadaan bersama, gunakan mekanisme kunci untuk mengakses keadaan bersama pada saat yang sama.
  • Beberapa kelas / pustaka dirancang dengan baik karena mereka menggunakan kembali konteks eksekusi yang diberikan kode panggilan kepada mereka. Ini memungkinkan penggunaan penguncian konvensional untuk memastikan keamanan benang. os_unfair_lock biasanya merupakan mekanisme penguncian tercepat dalam sistem: ia bekerja lebih baik dengan prioritas dan menyebabkan lebih sedikit saklar konteks.
  • Dalam kasus eksekusi paralel, tugas-tugas Anda tidak harus berjuang di antara mereka sendiri, jika tidak produktivitas turun tajam. Pertempuran terjadi dalam berbagai bentuk. Kasus yang jelas: perjuangan untuk merebut kunci. Namun pada kenyataannya, pertarungan semacam itu tidak lebih dari menggunakan sumber daya bersama, yang menjadi penghambat: IPC (komunikasi antarproses) / daemon OS, malloc (pemblokiran), memori bersama, I / O.
  • Anda tidak perlu semua kode untuk mengeksekusi secara tidak sinkron untuk menghindari peningkatan jumlah thread yang eksplosif. Jauh lebih baik untuk menggunakan jumlah antrian yang lebih rendah dan menolak menggunakan dispatch_get_global_queue ().
    ( Catatan perev. 1: tampaknya, ini adalah kasus ketika peningkatan eksplosif dalam jumlah utas terjadi ketika menyinkronkan sejumlah besar tugas paralel '' Jika saya memiliki banyak blok dan semuanya ingin menunggu, kita bisa mendapatkan apa yang kita sebut utas) ledakan. " )
    ( Catatan hal. 2: dari diskusi dapat dipahami bahwa Pierre Habuzit berarti antrian yang lebih rendah "yang diketahui oleh kernel ketika mereka memiliki tugas . " Di sini kita berbicara tentang kernel OS. )
  • Kita tidak boleh lupa tentang kerumitan dan bug yang muncul dalam arsitektur yang diisi dengan eksekusi asinkron dan panggilan balik. Kode yang dapat dieksekusi secara berurutan masih jauh lebih mudah untuk dibaca, ditulis, dan dipelihara.
  • Antrian kompetitif kurang dioptimalkan daripada yang berurutan. Gunakan mereka jika Anda mengukur perolehan kinerja, jika tidak itu adalah optimasi prematur.
  • Jika Anda perlu mengirim tugas dalam satu antrian baik secara sinkron maupun sinkron, maka alih-alih dispatch_sync () gunakan dispatch_async_and_wait (). dispatch_async_and_wait () tidak menjamin eksekusi pada utas dari mana panggilan berasal, yang mengurangi pengalihan konteks ketika antrian target aktif.
    ( Catatan terjemahan. 1: sebenarnya dispatch_sync () juga tidak menjamin, dokumentasi tentangnya hanya menyatakan "mengeksekusi blok pada utas saat ini, bila memungkinkan. Dengan satu pengecualian: blok yang dikirim ke antrian utama selalu dieksekusi pada utas utama. " )
    ( Catatan, terjemahan 2: tentang dispatch_async_and_wait () dalam dokumentasi dan dalam kode sumber )
  • Menggunakan 3-4 core dengan benar tidak sesederhana itu. Sebagian besar dari mereka yang mencoba, pada kenyataannya, tidak dapat mengatasi penskalaan dan pemborosan energi demi peningkatan kecil dalam produktivitas. Bagaimana prosesor bekerja dengan panas berlebih tidak akan membantu. Misalnya, Intel akan menonaktifkan Turbo-Boost jika cukup banyak core yang digunakan.
  • Ukur kinerja produk Anda di dunia nyata untuk memastikan Anda membuatnya lebih cepat, bukan lebih lambat. Hati-hati dengan tes kinerja mikro - mereka menyembunyikan pengaruh cache dan menjaga agar thread pool tetap panas. Untuk memeriksa apa yang Anda lakukan, Anda harus selalu memiliki tes makro.
  • libdispatch efektif, tetapi tidak ada keajaiban. Sumber dayanya tidak terbatas. Anda tidak dapat mengabaikan realitas OS dan perangkat keras tempat kode dijalankan. Juga, tidak setiap kode diparalelkan dengan baik.

Lihatlah semua panggilan dispatch_async () dalam kode Anda dan tanyakan pada diri Anda: apakah tugas yang Anda ajukan dengan panggilan ini benar-benar layak untuk saklar konteks. Dalam kebanyakan kasus, mengunci mungkin merupakan pilihan terbaik.

Segera setelah Anda mulai menggunakan dan menggunakan kembali antrian (konteks eksekusi) dari set yang dirancang sebelumnya, akan ada bahaya kebuntuan. Bahaya muncul saat mengirim tugas ke antrian ini menggunakan dispatch_sync (). Ini biasanya terjadi ketika antrian digunakan untuk keamanan thread. Jadi sekali lagi: solusinya adalah menggunakan mekanisme penguncian dan menggunakan dispatch_async () hanya ketika Anda perlu beralih ke konteks eksekusi lain.

Saya pribadi melihat peningkatan kinerja yang sangat besar dengan mengikuti panduan ini.
(dalam program yang sangat dimuat). Ini adalah pendekatan baru, tetapi sepadan.

Lebih banyak tautan


Program harus memiliki sangat sedikit antrian menggunakan kumpulan global


( Catatan perev.: Membaca tautan terakhir, tidak dapat menolak dan mentransfer satu keping dari tengah korespondensi Pierre Habuzit dengan Chris Luttner. Di bawah ini adalah salah satu jawaban dari Pierre Habuzit di 039420.html )
<...>
Saya mengerti bahwa sulit bagi saya untuk menyampaikan sudut pandang saya, karena saya bukan seorang pria dalam arsitektur bahasa, saya seorang pria dalam arsitektur sistem. Dan saya jelas tidak mengerti Aktor cukup untuk memutuskan bagaimana mengintegrasikan mereka ke dalam OS. Tetapi bagi saya, kembali ke contoh database, Actor-Database-Data, atau Actor-Network-Interface dari korespondensi sebelumnya, berbeda dari, katakanlah, query SQL ini atau permintaan jaringan ini. Yang pertama adalah entitas yang harus diketahui oleh OS di dalam kernel. Sementara query SQL atau permintaan jaringan hanya aktor yang antri untuk eksekusi di tempat pertama. Dengan kata lain, aktor tingkat atas ini berbeda karena mereka adalah tingkat atas, langsung di atas kernel / runtime tingkat rendah. Dan inilah intisari yang harus dipikirkan oleh inti. Ini membuat mereka hebat.

Ada 2 jenis antrian dan tingkat API yang sesuai di perpustakaan pengiriman:
  • antrian global yang bukan antrian seperti yang lain. Dan pada kenyataannya, mereka hanyalah abstraksi dari kumpulan thread.
  • semua antrian lain yang dapat Anda tetapkan target satu untuk yang lain seperti yang Anda inginkan.

Hari ini menjadi jelas bahwa ini adalah kesalahan dan harus ada 3 jenis antrian:

  • antrian global, yang bukan antrian nyata, tetapi mewakili kumpulan sistem mana yang memerlukan konteks eksekusi Anda (terutama prioritas). Dan kita harus melarang pengiriman tugas langsung ke antrian ini.
  • antrian lebih rendah (yang telah dilacak GCD dalam beberapa tahun terakhir dan memanggil "basis" dalam kode sumber ( tampaknya kode sumber GCD itu sendiri dimaksudkan - kira-kira diterjemahkan. ). Antrian yang lebih rendah diketahui oleh kernel ketika mereka memiliki tugas.
  • antrian "internal" lainnya yang tidak diketahui oleh kernel sama sekali.

Di grup pengembangan pengiriman, kami menyesal setiap hari yang berlalu bahwa perbedaan antara grup antrian kedua dan ketiga awalnya tidak dijelaskan dalam API.

Saya suka menyebut grup kedua sebagai "konteks eksekusi", tetapi saya bisa mengerti mengapa Anda ingin menyebutnya Aktor. Ini mungkin lebih konsisten (dan GCD melakukan hal yang sama, menyajikan ini dan itu sebagai antrian). "Aktor" tingkat atas seperti itu harus jumlahnya sedikit karena jika mereka semua menjadi aktif pada saat yang sama, maka mereka akan membutuhkan jumlah utas yang sama dalam proses. Dan ini bukan sumber daya yang bisa ditingkatkan. Itulah mengapa penting untuk membedakannya. Dan, seperti yang kita diskusikan, mereka juga biasa digunakan untuk melindungi keadaan bersama, sumber daya, atau sejenisnya. Ini tidak mungkin dilakukan dengan menggunakan aktor internal.
<...>


Mulai dengan eksekusi berurutan.


Jangan gunakan antrian global


Waspadalah terhadap garis kompetitif


Jangan gunakan panggilan async untuk melindungi keadaan bersama


Jangan gunakan panggilan async untuk tugas-tugas kecil


Beberapa kelas / perpustakaan seharusnya hanya sinkron


Perjuangan tugas paralel di antara mereka sendiri adalah pembunuh produktivitas


Untuk menghindari kebuntuan, gunakan mekanisme penguncian saat Anda perlu melindungi keadaan bersama


Jangan gunakan semaphores untuk menunggu tugas asinkron


NSOperation API memiliki beberapa jebakan serius yang dapat menyebabkan penurunan kinerja.


Hindari Tes Kinerja Mikro


Sumber daya tidak terbatas


Tentang dispatch_async_and_wait ()


Menggunakan 3-4 core tidak mudah


Banyak peningkatan kinerja di iOS 12 telah dicapai dengan daemon single-threaded

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


All Articles