“Bebas kunci, atau tidak bebas kunci, itulah pertanyaannya” atau “Tidur sehat lebih buruk daripada lobak pahit”

Komentar pada artikel " Cara tidur dengan benar dan salah " mengilhami saya untuk menulis artikel ini.


Artikel ini akan fokus pada pengembangan aplikasi multi-threaded, penerapan bebas-penguncian untuk beberapa kasus yang timbul dalam proses bekerja pada LAppS , fungsi nanosleep dan kekerasan terhadap penjadwal tugas.


NB:      C++  Linux,       POSIX.1-2008  a (    ). 

Secara umum, semuanya cukup berantakan, saya berharap alur pemikiran dalam presentasi akan jelas. Jika tertarik, maka saya minta kucing.


Perangkat lunak yang berorientasi pada peristiwa selalu menunggu sesuatu. Baik itu GUI atau server jaringan, mereka menunggu acara apa pun: input keyboard, acara mouse, paket data yang tiba melalui jaringan. Tetapi semua perangkat lunak menunggu berbeda. Sistem bebas kunci tidak harus menunggu sama sekali. Setidaknya penggunaan algoritma bebas kunci harus terjadi di mana Anda tidak perlu menunggu, dan bahkan berbahaya. Tetapi kita berbicara tentang sistem kompetitif (multi-threaded), dan anehnya, algoritma bebas kunci juga menunggu. Ya, mereka tidak memblokir eksekusi thread paralel, tetapi mereka sendiri sedang menunggu kesempatan untuk melakukan sesuatu tanpa memblokir.


LAppS menggunakan mutex dan semaphores dengan sangat aktif. Pada saat yang sama, tidak ada semaphores dalam standar C ++. Mekanisme ini sangat penting dan nyaman, tetapi C ++ harus bekerja pada sistem yang tidak memiliki dukungan semaphore, dan oleh karena itu semaphores tidak termasuk dalam standar. Selain itu, jika saya menggunakan semaphores karena mereka nyaman, maka mutex karena saya harus.


Perilaku mutex dalam kasus kunci kompetitif (), seperti sem_wait () di Linux, menempatkan utas menunggu di akhir antrian penjadwal tugas, dan ketika berada di atas, pemeriksaan diulang tanpa kembali ke tanah pengguna, utas dimasukkan kembali dalam antrian jika acara yang diharapkan belum terjadi. Ini adalah poin yang sangat penting.


Dan saya memutuskan untuk memeriksa apakah saya dapat menolak std :: mutex dan POSIX semaphores, meniru mereka dengan std :: atomic, mentransfer sebagian besar beban ke userland. Sebenarnya gagal, tapi yang pertama dulu.


Pertama, saya memiliki beberapa bagian di mana eksperimen ini dapat bermanfaat:


  • mengunci LibreSSL (kasus 1);
  • memblokir ketika mentransfer muatan yang diterima paket ke aplikasi Lua (kasus 2);
  • Menunggu acara payload siap diproses oleh aplikasi Lua (kasus 3).

Mari kita mulai dengan non-blocking-locks. Mari kita menulis mutex menggunakan atom, seperti yang ditunjukkan dalam beberapa pidato oleh H. Sutter (oleh karena itu, tidak ada kode asli, oleh karena itu, kode tersebut tidak sesuai dengan yang asli 100%, dan di Satter kode ini terkait dengan kemajuan C ++ 20, oleh karena itu ada perbedaan). Dan terlepas dari kesederhanaan kode ini, ada jebakan di dalamnya.


 #include <atomic> #include <pthread.h> namespace test { class mutex { private: std::atomic<pthread_t> mLock; public: explicit mutex():mLock{0} { } mutex(const mutex&)=delete; mutex(mutex&)=delete; void lock() { pthread_t locked_by=0; //  C++20     , .. compare_exchange_strong          while(!mLock.compare_exchange_strong(locked_by,pthread_self())) { locked_by=0; //      } } void unlock() { pthread_t current=pthread_self(); if(!mLock.compare_exchange_strong(current,0)) { throw std::system_error(EACCES, std::system_category(), "An attempt to unlock the mutex owned by other thread"); } } const bool try_lock() { pthread_t unused=0; return mLock.compare_exchange_strong(unused,pthread_self()); } }; } 

Tidak seperti std :: mutex :: unlock (), perilaku pengujian :: mutex: unlock () ketika mencoba membuka kunci dari utas lainnya adalah deterministik. Pengecualian akan dilempar. Ini bagus, meskipun tidak konsisten dengan perilaku standar. Dan apa yang buruk di kelas ini? Berita buruknya adalah bahwa metode test :: mutex: lock () akan tanpa malu-malu menghabiskan sumber daya CPU dalam kuota waktu yang dialokasikan untuk utas, dalam upaya untuk mengambil alih mutex yang sudah dimiliki oleh utas lain. Yaitu loop in test :: mutex: lock () akan menjadi pemborosan sumber daya CPU. Apa pilihan kita untuk mengatasi situasi ini?


Kita dapat menggunakan sched_yield () (seperti yang disarankan dalam salah satu komentar pada artikel di atas). Apakah sesederhana itu? Pertama, untuk menggunakan sched_yield (), perlu bahwa utas eksekusi menggunakan SCHED_RR, SCHED_FIFO kebijakan untuk memprioritaskan mereka dalam penjadwal tugas. Kalau tidak, memanggil sched_yield () akan menjadi pemborosan sumber daya CPU. Kedua, panggilan yang sangat sering ke sched_yield () masih akan meningkatkan konsumsi CPU. Selain itu, penggunaan kebijakan waktu-nyata dalam aplikasi Anda, dan asalkan tidak ada aplikasi waktu-nyata lain dalam sistem, akan membatasi antrian penjadwal dengan kebijakan yang dipilih hanya untuk utas Anda. Tampaknya ini bagus! Tidak bagus Seluruh sistem akan menjadi kurang responsif, karena sibuk dengan tugas prioritas. CFQ akan berada di kandang. Tetapi ada utas lain dalam aplikasi, dan sangat sering muncul situasi ketika utas yang telah menangkap mutex diletakkan di akhir antrian (kuota telah kedaluwarsa), dan utas yang menunggu mutex akan dirilis tepat di depannya. Dalam percobaan saya (kasus 2), metode ini memberikan hasil yang sama (3,8% lebih buruk) daripada std :: mutex, tetapi sistem ini kurang responsif dan konsumsi CPU meningkat 5% -7%.


Anda dapat mencoba mengubah test :: mutex :: lock () seperti ini (juga buruk):


 void lock() { pthread_t locked_by=0; while(!mLock.compare_exchange_strong(locked_by,pthread_self())) { static thread_local const struct timespec pause{0,4}; // -      nanosleep(&pause,nullptr); locked_by=0; } } 

Di sini Anda dapat bereksperimen dengan durasi tidur dalam nanodetik, penundaan 4 ns optimal untuk CPU saya dan penurunan kinerja relatif ke std :: mutex dalam kasus yang sama 2 adalah 1,2%. Bukan fakta bahwa nanosleep tidur 4ns. Bahkan, atau lebih (dalam kasus umum) atau kurang (jika terputus). Turunnya (!) Dalam konsumsi CPU adalah 12% -20%. Yaitu itu adalah mimpi yang sehat.


OpenSSL dan LibreSSL memiliki dua fungsi yang mengatur panggilan balik untuk diblokir saat menggunakan pustaka ini di lingkungan multi-utas. Ini terlihat seperti ini:


 //  callback void openssl_crypt_locking_function_callback(int mode, int n, const char* file, const int line) { static std::vector<std::mutex> locks(CRYPTO_num_locks()); if(n>=static_cast<int>(locks.size())) { abort(); } if(mode & CRYPTO_LOCK) locks[n].lock(); else locks[n].unlock(); } //  callback-a CRYPTO_set_locking_callback(openssl_crypt_locking_function_callback); //  id CRYPTO_set_id_callback(pthread_self); 

Dan sekarang yang terburuk adalah menggunakan tes di atas :: mutex mutex di LibreSSL mengurangi kinerja LAppS hampir 2 kali. Selain itu, terlepas dari opsi (loop tunggu kosong, sched_yield (), nanosleep ()).


Secara umum, kami menghapus case 2 dan case 1, dan tetap menggunakan std :: mutex.


Mari kita beralih ke semaphores. Ada banyak contoh bagaimana menerapkan semaphores menggunakan std :: condition_variable. Mereka semua menggunakan std :: mutex juga. Dan simulator semaphore seperti itu lebih lambat (menurut tes saya) daripada semaphores sistem POSIX.


Oleh karena itu, kami akan membuat semafor pada atom:


  class semaphore { private: std::atomic<bool> mayRun; mutable std::atomic<int64_t> counter; public: explicit semaphore() : mayRun{true},counter{0} { } semaphore(const semaphore&)=delete; semaphore(semaphore&)=delete; const bool post() const { ++counter; return mayRun.load(); } const bool try_wait() { if(mayRun.load()) { if(counter.fetch_sub(1)>0) return true; else { ++counter; return false; } }else{ throw std::system_error(ENOENT,std::system_category(),"Semaphore is destroyed"); } } void wait() { while(!try_wait()) { static thread_local const struct timespec pause{0,4}; nanosleep(&pause,nullptr); } } void destroy() { mayRun.store(false); } const int64_t decrimentOn(const size_t value) { if(mayRun.load()) { return counter.fetch_sub(value); }else{ throw std::system_error(ENOENT,std::system_category(),"Semaphore is destroyed"); } } ~semaphore() { destroy(); } }; 

Oh, semaphore ini jauh lebih cepat daripada sistem semaphore. Hasil pengujian terpisah dari semaphore ini dengan satu penyedia dan 20 konsumen:


 OS semaphores test. Started 20 threads waiting on a semaphore Thread(OS): wakes: 500321 Thread(OS): wakes: 500473 Thread(OS): wakes: 501504 Thread(OS): wakes: 502337 Thread(OS): wakes: 498324 Thread(OS): wakes: 502755 Thread(OS): wakes: 500212 Thread(OS): wakes: 498579 Thread(OS): wakes: 499504 Thread(OS): wakes: 500228 Thread(OS): wakes: 499696 Thread(OS): wakes: 501978 Thread(OS): wakes: 498617 Thread(OS): wakes: 502238 Thread(OS): wakes: 497797 Thread(OS): wakes: 498089 Thread(OS): wakes: 499292 Thread(OS): wakes: 498011 Thread(OS): wakes: 498749 Thread(OS): wakes: 501296 OS semaphores test. 10000000 of posts for 20 waiting threads have taken 9924 milliseconds OS semaphores test. Post latency: 0.9924ns ======================================= AtomicEmu semaphores test. Started 20 threads waiting on a semaphore Thread(EmuAtomic) wakes: 492748 Thread(EmuAtomic) wakes: 546860 Thread(EmuAtomic) wakes: 479375 Thread(EmuAtomic) wakes: 534676 Thread(EmuAtomic) wakes: 501014 Thread(EmuAtomic) wakes: 528220 Thread(EmuAtomic) wakes: 496783 Thread(EmuAtomic) wakes: 467563 Thread(EmuAtomic) wakes: 608086 Thread(EmuAtomic) wakes: 489825 Thread(EmuAtomic) wakes: 479799 Thread(EmuAtomic) wakes: 539634 Thread(EmuAtomic) wakes: 479559 Thread(EmuAtomic) wakes: 495377 Thread(EmuAtomic) wakes: 454759 Thread(EmuAtomic) wakes: 482375 Thread(EmuAtomic) wakes: 512442 Thread(EmuAtomic) wakes: 453303 Thread(EmuAtomic) wakes: 480227 Thread(EmuAtomic) wakes: 477375 AtomicEmu semaphores test. 10000000 of posts for 20 waiting threads have taken 341 milliseconds AtomicEmu semaphores test. Post latency: 0.0341ns 

Semafor ini dengan pos hampir bebas (), yang 29 kali lebih cepat daripada sistem satu, juga sangat cepat dalam membangunkan utas menunggu: 29325 bangun per milidetik, terhadap 1007 bangun per milidetik dari sistem. Ini memiliki perilaku deterministik dengan semaphore hancur atau semaphore dirusak. Dan tentu saja, segfault ketika mencoba menggunakan yang sudah hancur.


(¹) Sebenarnya, berkali-kali dalam milidetik aliran tidak dapat ditunda dan dibangunkan oleh penjadwal. Karena post () tidak menghalangi, dalam tes sintetik ini, tunggu () sangat sering menemukan dirinya dalam situasi di mana Anda tidak perlu tidur. Pada saat yang sama, setidaknya 7 utas secara paralel membaca nilai dari semaphore.


Tetapi menggunakannya dalam kasus 3 dalam LAppS menyebabkan kerugian kinerja terlepas dari waktu tidur. Dia bangun terlalu sering untuk memeriksa, dan peristiwa di LAppS tiba jauh lebih lambat (latensi jaringan, latensi sisi klien menghasilkan beban, dll.). Dan memeriksa lebih jarang berarti juga kehilangan kinerja.


Selain itu, penggunaan tidur dalam kasus seperti itu dan dengan cara yang sama benar-benar berbahaya, karena pada perangkat keras lain, hasilnya mungkin benar-benar berbeda (seperti dalam kasus jeda instruksi perakitan), dan untuk setiap model CPU, Anda juga harus memilih waktu tunda.


Keuntungan dari sistem mutex dan semaphore adalah bahwa thread eksekusi tidak terbangun sampai suatu peristiwa (membuka kunci mutex atau menambah semaphore) terjadi. Siklus CPU ekstra tidak sia-sia - untung.


Secara umum, segala sesuatu dari yang jahat ini, menonaktifkan iptables pada sistem saya memberikan dari 12% (dengan TLS) hingga 30% (tanpa TLS) keuntungan kinerja ...

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


All Articles