Artikel ini menjelaskan antipattern berbahaya "Zombies", yang dalam beberapa situasi secara alami terjadi ketika menggunakan std :: enable_shared_from_this. Materi tersebut berada di persimpangan teknologi C ++ modern dan arsitektur.
Pendahuluan
C ++ 11 memberi pengembang alat yang luar biasa untuk bekerja dengan memori - smart pointer std :: unique_ptr dan sekelompok std :: shared_ptr + std :: lemah_ptr. Penggunaan pointer cerdas untuk kenyamanan dan keamanan jauh melebihi penggunaan pointer mentah. Pointer pintar banyak digunakan dalam praktik, seperti memungkinkan pengembang untuk fokus pada masalah tingkat yang lebih tinggi daripada melacak kebenaran penciptaan / penghapusan entitas yang dibuat secara dinamis.
Std :: enable_shared_from_ template kelas ini juga merupakan bagian dari standar, dan sepertinya agak aneh ketika Anda pertama kali bertemu.
Artikel ini akan membahas bagaimana Anda bisa terjebak dengan penggunaannya.
Program pendidikan
RAII dan pointer cerdasTujuan langsung dari smart pointer adalah untuk merawat sepotong RAM yang dialokasikan pada heap. Pointer pintar mengimplementasikan idiom RAII (Akuisisi sumber daya adalah inisialisasi) dan dapat dengan mudah diadaptasi untuk menangani jenis sumber daya lain yang memerlukan inisialisasi dan de-inisialisasi non-sepele, seperti:
- file;
- folder sementara pada disk;
- koneksi jaringan (http, websockets);
- utas eksekusi (utas);
- mutex;
- Lainnya (yang cukup untuk fantasi).
Untuk generalisasi seperti itu, cukup untuk menulis sebuah kelas (pada kenyataannya, kadang-kadang Anda bahkan tidak bisa menulis sebuah kelas, tetapi cukup gunakan deleter - tetapi hari ini kisahnya bukan tentang itu), yang mengimplementasikan:
- inisialisasi dalam konstruktor atau dalam metode terpisah;
- deinitialization pada destructor,
kemudian "bungkus" dalam smart pointer yang sesuai, tergantung pada model kepemilikan yang diperlukan - joint (std :: shared_ptr) atau sole (std :: unique_ptr). Ini menghasilkan "RAII dua lapis": pointer pintar memungkinkan Anda untuk mentransfer / berbagi kepemilikan sumber daya, dan kelas pengguna menginisialisasi / menonaktifkan inisialisasi sumber daya non-standar.
std :: shared_ptr menggunakan mekanisme penghitungan tautan. Standar mendefinisikan penghitung tautan kuat (menghitung jumlah salinan std :: shared_ptr) dan penghitung tautan lemah (menghitung jumlah salinan std :: lemah_ptr yang dibuat untuk instance std :: shared_ptr). Kehadiran setidaknya satu mata rantai yang kuat memastikan bahwa kehancuran belum terjadi. Properti std :: shared_ptr ini banyak digunakan untuk memastikan validitas suatu objek hingga bekerja dengannya selesai di semua bagian program. Kehadiran tautan yang lemah tidak mencegah penghancuran objek dan memungkinkan Anda untuk mendapatkan tautan yang kuat hanya sampai dihancurkan.
RAII menjamin pelepasan sumber daya jauh lebih dapat diandalkan daripada panggilan eksplisit untuk menghapus / menghapus [] / gratis / tutup / reset / membuka, karena:
- Anda dapat dengan mudah melupakan panggilan eksplisit;
- panggilan eksplisit dapat dibuat secara keliru lebih dari sekali;
- tantangan eksplisit sulit ketika menerapkan kepemilikan bersama atas sumber daya;
- mekanisme promosi stack di c ++ menjamin panggilan destruktor untuk semua objek yang keluar dari ruang lingkup dalam kasus pengecualian.
Jaminan de-inisialisasi dalam idiom sangat penting sehingga layak mendapat tempat yang baik dalam nama idiom bersama dengan inisialisasi.
Pointer pintar juga memiliki kelemahan:
- keberadaan overhead dalam hal kinerja dan memori (untuk sebagian besar aplikasi tidak signifikan);
- kemungkinan tautan siklis yang menghalangi pelepasan sumber daya dan menyebabkan kebocorannya.
Tentunya setiap pengembang lebih dari sekali membaca tentang tautan melingkar dan melihat contoh sintetik kode bermasalah.
Bahayanya mungkin tampak tidak signifikan karena alasan berikut:
- jika memori sering bocor dan banyak - ini terlihat dalam konsumsinya, dan jika jarang dan sedikit - maka masalahnya tidak mungkin terwujud pada tingkat pengguna akhir;
- menggunakan analisis kode dinamis untuk kebocoran (Valgrind, Clang LeakSanitizer, dll.);
- "Saya tidak menulis seperti itu";
- "arsitektur saya benar";
"Kode kita sedang ditinjau."
std :: enable_share_from_iniDi C ++ 11, kelas helper std :: enable_shared_from_this diperkenalkan. Untuk pengembang yang berhasil membangun kode tanpa std :: enable_share_from_this, potensi penggunaan kelas ini mungkin tidak jelas.
Apa yang dilakukan std :: enable_shared_from_this?
Ini memungkinkan fungsi anggota dari kelas yang dipakai di std :: shared_ptr untuk menerima tambahan kuat (shared_from_this ()) atau lemah (lemah_from_ini (), mulai dari C ++ 17) salinan std :: shared_ptr di mana ia dibuat . Anda tidak dapat memanggil shared_from_this () dan lemah_from_this () dari konstruktor dan destruktor.
Kenapa begitu sulit? Anda cukup membangun std :: shared_ptr <T> (ini)
Tidak, kamu tidak bisa. Semua std :: shared_ptrs yang peduli dengan instance kelas yang sama harus menggunakan satu unit penghitungan tautan. Tidak ada cara untuk melakukannya tanpa sihir khusus.
Prasyarat untuk menggunakan std :: enable_shared_from_this adalah untuk awalnya membuat objek kelas di std :: shared_ptr. Membuat di tumpukan, mengalokasikan secara dinamis di tumpukan, membuat di std :: unique_ptr - semua ini tidak cocok. Hanya di std :: shared_ptr.
Apakah mungkin membatasi pengguna dengan cara membuat instance kelas?
Ya kamu bisa. Untuk melakukan ini, cukup:
- menyediakan metode statis untuk membuat instance yang awalnya ditempatkan di std :: shared_ptr;
- Menempatkan konstruktor secara pribadi atau dilindungi;
- Melarang menyalin dan memindahkan semantik.
Kelas masuk ke kandang, menguncinya dan menelan kunci - mulai sekarang semua instansnya akan hidup hanya di std :: shared_ptr, dan tidak ada cara hukum untuk mengeluarkannya dari sana.
Pembatasan seperti itu tidak dapat disebut solusi arsitektur yang baik, tetapi metode ini sepenuhnya sesuai dengan standar.
Selain itu, Anda dapat menggunakan idiom PIMPL: satu-satunya pengguna kelas yang berubah-ubah - fasad - akan membuat implementasi secara ketat di std :: shared_ptr, dan fasad itu sendiri sudah akan dicabut dari pembatasan semacam ini.
std :: enable_shared_from_this memiliki nuansa signifikan dalam pewarisan, tetapi mendiskusikannya berada di luar cakupan artikel ini.
Langsung ke intinya
Semua contoh kode yang disediakan dalam artikel dipublikasikan di
github .
Kode menunjukkan teknik buruk yang disamarkan sebagai penggunaan C ++ modern yang aman dan aman
Simplecyclic
Tampaknya tidak ada yang menandakan masalah. Deklarasi kelas terlihat sederhana dan mudah. Kecuali untuk satu detail "kecil" - untuk beberapa alasan pewarisan dari std :: enable_shared_from_ini diterapkan.
SimpleCyclic.h#pragma once #include <memory> #include <functional> namespace SimpleCyclic { class Cyclic final : public std::enable_shared_from_this<Cyclic> { public: static std::shared_ptr<Cyclic> create(); Cyclic(const Cyclic&) = delete; Cyclic(Cyclic&&) = delete; Cyclic& operator=(const Cyclic&) = delete; Cyclic& operator=(Cyclic&&) = delete; ~Cyclic(); void doSomething(); private: Cyclic(); std::function<void(void)> _fn; }; } // namespace SimpleCyclic
Dan dalam implementasi:
SimpleCyclic.cpp #include <iostream> #include "SimpleCyclic.h" namespace SimpleCyclic { Cyclic::Cyclic() = default; Cyclic::~Cyclic() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::shared_ptr<Cyclic> Cyclic::create() { return std::shared_ptr<Cyclic>(new Cyclic); } void Cyclic::doSomething() { _fn = [shis = shared_from_this()](){}; std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } } // namespace SimpleCyclic
main.cpp #include "SimpleCyclic/SimpleCyclic.h" int main() { auto simpleCyclic = SimpleCyclic::Cyclic::create(); simpleCyclic->doSomething(); return 0; }
Output KonsolN12SimpleCyclic6CyclicE :: doSomething
Di dalam tubuh fungsi doSomething (), instance kelas
itu sendiri akan membuat salinan kuat tambahan std :: shared_ptr di mana ia ditempatkan. Kemudian, menggunakan tangkapan umum, salinan ini ditempatkan di fungsi lambda ditugaskan ke bidang data kelas dengan kedok fungsi std :: tidak berbahaya. Panggilan ke doSomething () menghasilkan referensi melingkar, dan instance kelas tidak akan lagi dihancurkan bahkan setelah penghancuran semua tautan kuat eksternal.
Ada kebocoran memori. SimpleCyclic :: Cyclic :: ~ Cyclic destructor tidak dipanggil.
Instance class "menyimpan" dirinya sendiri.
Kode macet.
(gambar diambil
dari sini )
Dan apa, ini antipattern "Zombie"?Tidak, ini hanya latihan. Semua yang paling menarik belum datang.
Mengapa pengembang menulis ini?Contoh sintetis. Saya tidak mengetahui adanya situasi di mana kode seperti itu akan diperoleh secara harmonis.
Jadi, apakah analisis kode dinamis tetap diam?Tidak, Valgrind dengan jujur ββmelaporkan kebocoran memori:
Posting Valgrind96 (64 langsung, 32 tidak langsung) byte dalam 1 blok pasti hilang dalam catatan kerugian 29 dari 46
di SimpleCyclic :: Cyclic :: create () di /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
1: malloc di /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: operator baru (tidak ditandai lama) di /usr/lib/libc++abi.dylib
3: SimpleCyclic :: Cyclic :: create () di /Users/User/Projects/Zomby_antipattern_concept/SimpleCyclic/SimpleCyclic.cpp:15
4: utama di / Pengguna / Pengguna / Proyek / Zomby_antipattern_concept/SimpleCyclic/main.cpphaps
Pimplcyclic
Dalam hal ini, file header terlihat benar dan ringkas. Itu menyatakan fasad yang menyimpan implementasi tertentu di std :: shared_ptr. Warisan - termasuk dari std :: enable_shared_from_this - tidak ada, tidak seperti contoh sebelumnya.
Pimplcyclic.h #pragma once #include <memory> namespace PimplCyclic { class Cyclic { public: Cyclic(); ~Cyclic(); private: class Impl; std::shared_ptr<Impl> _impl; }; } // namespace PimplCyclic
Dan dalam implementasi:
Pimplcyclic.cpp #include <iostream> #include <functional> #include "PimplCyclic.h" namespace PimplCyclic { class Cyclic::Impl : public std::enable_shared_from_this<Cyclic::Impl> { public: ~Impl() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } void doSomething() { _fn = [shis = shared_from_this()](){}; std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } private: std::function<void(void)> _fn; }; Cyclic::Cyclic() : _impl(std::make_shared<Impl>()) { if (_impl) { _impl->doSomething(); } } Cyclic::~Cyclic() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } } // namespace PimplCyclic
main.cpp #include "PimplCyclic/PimplCyclic.h" int main() { auto pimplCyclic = PimplCyclic::Cyclic(); return 0; }
Output KonsolN11PimplCyclic6Cyclic4ImplE :: doSomething
N11PimplCyclic6CyclicE :: ~ Cyclic
Memanggil Impl :: doSomething () membuat referensi melingkar dalam sebuah instance dari kelas Impl. Fasad dihancurkan dengan benar, tetapi implementasinya bocor. Destuktor PimplCyclic :: Cyclic :: Impl :: ~ Impl tidak disebut.
Contohnya lagi sintetis, tapi kali ini lebih berbahaya - semua peralatan buruk terletak dalam implementasi dan tidak muncul dalam iklan.
Selain itu, untuk membuat tautan melingkar, kode pengguna tidak memerlukan tindakan apa pun selain konstruksi.
Analisis dinamis dalam menghadapi Valgrind, dan kali ini mengungkapkan kebocoran:
Posting Valgrind96 byte dalam 1 blok pasti hilang dalam catatan kerugian 29 dari 46
dalam PimplCyclic :: Cyclic :: Cyclic () di / Pengguna / Pengguna / Proyek / Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
1: malloc di /usr/local/Cellar/valgrind/HEAD-60ab74a/lib/valgrind/vgpreload_memcheck-amd64-darwin.so
2: operator baru (tidak ditandai lama) di /usr/lib/libc++abi.dylib
3: std :: __ 1 :: __ libcpp_allocate (lama tidak ditandatangani, lama tidak ditandatangani) di /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/new:25
4: std :: __ 1 :: pengalokasi <std :: __ 1 :: __ shared_ptr_emplace <PimplCyclic :: Cyclic :: Impl, std :: __ 1 :: pengalokasikan <PimplCyclic :: Cyclic :: Impl >>> mengalokasikan (tanpa tanda lama) , batal const *) di /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:1813
5: std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> std :: __ 1 :: shared_ptr <PimplCyclic :: Cyclic :: Impl> :: make_shared <> () di /Applications/Xcode.app/Contents /Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4326
6: _ZNSt3__1L11make_sharedIN11PimplCyclic6Cyclic4ImplEJEEENS_9enable_ifIXntsr8is_arrayIT_EE5valueENS_10shared_ptrIS5_EEE4typeEDpOT0_ di /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include/c++/v1/memory:4706
7: PimplCyclic :: Cyclic :: Cyclic () di / Pengguna / Pengguna / Proyek / Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:28
8: PimplCyclic :: Cyclic :: Cyclic () di / Pengguna / Pengguna / Proyek / Zomby_antipattern_concept/PimplCyclic/PimplCyclic.cpp:29
9: utama di / Pengguna / Pengguna / Proyek / Zomby_antipattern_concept/PimplCyclic/main.cpphaps
Agak mencurigakan melihat Pimpl, di mana implementasinya disimpan di std :: shared_ptr.Pimpl klasik berdasarkan pada pointer mentah terlalu kuno, dan std :: unique_ptr memiliki efek samping menyebarkan larangan copy-semantik pada fasad. Fasad seperti itu akan menerapkan idiom kepemilikan tunggal, yang mungkin tidak sesuai dengan ide arsitektur. Dari penggunaan std :: shared_ptr untuk menyimpan implementasi, kami menyimpulkan bahwa kelas dirancang untuk memberikan kepemilikan bersama.
Bagaimana hal ini berbeda dari memori alokasi kebocoran klasik dengan secara eksplisit memanggil baru tanpa penghapusan berikutnya? Dengan cara yang sama, semuanya akan menjadi indah di antarmuka, dan dalam implementasi - bug.Kami sedang mendiskusikan cara
- cara
modern untuk menembak diri sendiri.
Antipattern "Zombies"
Jadi, dari materi di atas jelas:
- pointer pintar dapat diikat ke node;
- penggunaan std :: enable_shared_from_ini dapat berkontribusi untuk ini, karena memungkinkan turunan suatu kelas untuk mengikat ke dalam simpul dengan hampir tanpa bantuan dari luar.
Dan sekarang - perhatian - pertanyaan kunci dari artikel ini: apakah jenis sumber daya dibungkus dengan smart pointer? Apakah ada perbedaan antara perawatan file RAII dan koneksi HTTPS asinkron?Simplezomby
Kode umum untuk semua contoh zombie berikutnya telah dipindahkan ke Perpustakaan umum.
Antarmuka zombie abstrak dengan Pengelola nama sederhana:
Umum / Manajer.h #pragma once #include <memory> namespace Common { class Listener; class Manager { public: Manager() = default; Manager(const Manager&) = delete; Manager(Manager&&) = delete; Manager& operator=(const Manager&) = delete; Manager& operator=(Manager&&) = delete; virtual ~Manager() = default; virtual void runOnce(std::shared_ptr<Common::Listener> listener) = 0; }; } // namespace Common
Antarmuka abstrak pendengar, siap menerima teks aman:
Umum / Pendengar.h #pragma once #include <string> #include <memory> namespace Common { class Listener { public: virtual ~Listener() = default; using Data = std::string; // thread-safe virtual void processData(const std::shared_ptr<const Data> data) = 0; }; } // namespace Common
Pendengar yang menampilkan teks ke konsol. Menerapkan konsep SingletonShared dari
Teknik artikel saya
untuk Menghindari Perilaku Tidak Terdefinisi Ketika Memanggil Singleton :
Umum / Impl / WriteToConsoleListener.h #pragma once #include <mutex> #include "Common/Listener.h" namespace Common { class WriteToConsoleListener final : public Listener { public: WriteToConsoleListener(const WriteToConsoleListener&) = delete; WriteToConsoleListener(WriteToConsoleListener&&) = delete; WriteToConsoleListener& operator=(const WriteToConsoleListener&) = delete; WriteToConsoleListener& operator=(WriteToConsoleListener&&) = delete; ~WriteToConsoleListener() override; static std::shared_ptr<WriteToConsoleListener> instance(); // blocking void processData(const std::shared_ptr<const Data> data) override; private: WriteToConsoleListener(); std::mutex _mutex; }; } // namespace Common
Umum / Impl / WriteToConsoleListener.cpp #include <iostream> #include "WriteToConsoleListener.h" namespace Common { WriteToConsoleListener::WriteToConsoleListener() = default; WriteToConsoleListener::~WriteToConsoleListener() { auto lock = std::lock_guard(_mutex); std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::shared_ptr<WriteToConsoleListener> WriteToConsoleListener::instance() { static auto inst = std::shared_ptr<WriteToConsoleListener>(new WriteToConsoleListener); return inst; } void WriteToConsoleListener::processData(const std::shared_ptr<const Data> data) { if (data) { auto lock = std::lock_guard(_mutex); std::cout << *data << std::flush; } } } // namespace Common
Dan akhirnya, zombie pertama, paling sederhana dan paling cerdik.
SimpleZomby.h #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" namespace Common { class Listener; } // namespace Common namespace SimpleZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; std::shared_ptr<Common::Listener> _listener; Semaphore _semaphore = false; std::thread _thread; }; } // namespace SimpleZomby
SimpleZomby.cpp #include <sstream> #include "SimpleZomby.h" #include "Common/Listener.h" namespace SimpleZomby { std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::Zomby() = default; Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("SimpleZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()](){ while (shis && shis->_listener && shis->_semaphore) { shis->_listener->processData(std::make_shared<Common::Listener::Data>("SimpleZomby is alive!\n")); std::this_thread::sleep_for(std::chrono::seconds(1)); } }); } } // namespace SimpleZomby
Zombie menjalankan fungsi lambda di utas terpisah, secara berkala mengirim string ke pendengar. Fungsi Lambda untuk bekerja membutuhkan semaphore dan pendengar, yang merupakan bidang kelas zombie. Fungsi lambda tidak menangkap mereka sebagai bidang yang terpisah, tetapi menggunakan objek sebagai agregator. Menghancurkan instance dari kelas zombie sebelum fungsi lambda selesai akan menghasilkan perilaku yang tidak terdefinisi. Untuk menghindari ini, fungsi lambda menangkap salinan shared_from_this () yang kuat.
Dalam destruktor zombie, semaphore disetel ke false, setelah itu detach () dipanggil untuk stream. Mengatur semaphore memberitahu thread untuk dimatikan.
Di destructor, perlu untuk memanggil not detach (), tetapi join ()!... dan dapatkan destructor yang memblokir eksekusi untuk waktu yang tidak terbatas, yang mungkin tidak dapat diterima.
Jadi ini merupakan pelanggaran RAII! RAII seharusnya keluar dari destructor hanya setelah melepaskan sumber daya!Jika benar - maka ya, perusak zombie tidak melepaskan sumber daya, tetapi hanya
menjamin bahwa rilis akan dibuat . Kadang diproduksi - mungkin segera, atau mungkin tidak juga. Dan bahkan mungkin bahwa main akan selesai bekerja lebih awal - maka utas akan dihapus secara paksa oleh sistem operasi. Tetapi pada kenyataannya, garis antara RAII "benar" dan "salah" bisa sangat tipis: misalnya, "benar" RAII, yang memanggil std :: filesystem :: remove () di destructor untuk file sementara, mungkin mengembalikan kontrol ke sana saat ketika perintah tulis masih akan berada di salah satu cache yang mudah menguap dan tidak akan ditulis dengan jujur ββke piring magnetik hard disk.
main.cpp #include <chrono> #include <thread> #include <sstream> #include "Common/Impl/WriteToConsoleListener.h" #include "SimpleZomby/SimpleZomby.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto simpleZomby = SimpleZomby::Zomby::create(); simpleZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(4500)); } // Zomby should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; }
Output KonsolSimpleZomby masih hidup!
SimpleZomby masih hidup!
SimpleZomby masih hidup!
SimpleZomby masih hidup!
SimpleZomby masih hidup!
=================================================== ===========
| Zomby terbunuh |
=================================================== ===========
SimpleZomby masih hidup!
SimpleZomby masih hidup!
SimpleZomby masih hidup!
SimpleZomby masih hidup!
SimpleZomby masih hidup!
Apa yang bisa dilihat dari output program:
- zombie terus bekerja bahkan setelah meninggalkan bidang visibilitas;
- tidak ada destruktor yang dipanggil untuk zombie atau WriteToConsoleListener.
Kebocoran memori telah terjadi.
Terjadi kebocoran sumber daya. Dan sumber daya dalam hal ini adalah utas eksekusi.
Kode yang seharusnya dihentikan terus bekerja di utas terpisah.
Kebocoran WriteToConsoleListener bisa dicegah dengan menggunakan teknik SingletonWeak dari artikel saya
Menghindari Perilaku Tidak Pasti Saat Memanggil Singleton , tetapi saya sengaja tidak melakukannya.

(gambar diambil
dari sini )
Kenapa Zombies?Karena dia terbunuh, dan dia masih hidup.
Apa bedanya dengan referensi sirkuler pada contoh sebelumnya?Fakta bahwa sumber daya yang hilang bukan hanya sepotong memori, tetapi sesuatu yang secara independen mengeksekusi kode secara independen dari utas yang meluncurkannya.
Apakah mungkin untuk menghancurkan "Zombies"?Setelah meninggalkan ruang lingkup (mis., Setelah menghancurkan semua referensi eksternal yang kuat dan lemah terhadap zombie) - itu tidak mungkin. Zombie akan dihancurkan ketika dia memutuskan untuk menghancurkan dirinya sendiri (ya, itu adalah sesuatu dengan perilaku aktif), mungkin tidak pernah, yaitu. akan bertahan hingga sistem operasi membersihkan ketika aplikasi berakhir. Tentu saja, kode pengguna mungkin memiliki beberapa efek pada kondisi untuk keluar dari kode zombie, tetapi efek ini akan tidak langsung dan tergantung pada implementasi.
Dan sebelum meninggalkan ruang lingkup?Anda dapat secara eksplisit memanggil destruktor zombie, tetapi Anda tidak mungkin untuk menghindari perilaku tidak terdefinisi karena penghancuran berulang objek oleh smart pointer destructor juga - ini adalah pertarungan melawan RAII Atau Anda dapat menambahkan fungsi de-inisialisasi eksplisit - dan ini adalah penolakan RAII.
Bagaimana ini berbeda dari hanya memulai utas diikuti oleh detach ()?Dalam kasus zombie, berbeda dengan panggilan sederhana untuk melepaskan (), ada ide untuk menghentikan aliran. Hanya saja tidak berhasil. Memiliki ide yang tepat membantu menutupi masalah.
Apakah contohnya masih sintetis?Sebagian. Dalam contoh sederhana ini, tidak ada alasan yang cukup untuk menggunakan shared_from_this () - misalnya, Anda bisa melakukan dengan menangkap lemah_from_this () atau mengambil semua bidang yang diperlukan di kelas. Tetapi dengan kompleksitas tugas, keseimbangan mungkin bergeser ke samping
shared_from_ini ().
Valgrind, Valgrind! Kami memiliki garis pertahanan tambahan terhadap zombie!Alas dan ah - tetapi Valgrind tidak mengungkapkan kebocoran memori. Kenapa - saya tidak tahu. Dalam diagnosa, hanya ada entri
"mungkin hilang" yang menunjukkan fungsi sistem - kurang lebih sama dan dengan jumlah yang sama seperti ketika mengerjakan main kosong. Tidak ada referensi kode pengguna. Alat analisis dinamis lain mungkin lebih baik, tetapi jika Anda masih mengandalkannya, baca terus.
Steppingzomby
Kode dalam contoh ini melanjutkan melalui langkah-langkah resolDnsName ---> connectTcp ---> createSsl ---> sendHttpRequest ---> readHttpReply, mensimulasikan operasi koneksi HTTPS klien dalam eksekusi asinkron. Setiap langkah membutuhkan waktu sekitar satu detik.
Steppingzomby.h #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" namespace Common { class Listener; } // namespace Common namespace SteppingZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; std::shared_ptr<Common::Listener> _listener; Semaphore _semaphore = false; std::thread _thread; void resolveDnsName(); void connectTcp(); void establishSsl(); void sendHttpRequest(); void readHttpReply(); }; } // namespace SteppingZomby
Steppingzomby.cpp #include <sstream> #include <string> #include "SteppingZomby.h" #include "Common/Listener.h" namespace { void doSomething(Common::Listener& listener, std::string&& callingFunctionName) { listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " started\n")); std::this_thread::sleep_for(std::chrono::milliseconds(1000)); listener.processData(std::make_shared<Common::Listener::Data>(callingFunctionName + " finished\n")); } } // namespace namespace SteppingZomby { Zomby::Zomby() = default; std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("SteppingZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()](){ if (shis && shis->_listener && shis->_semaphore) { shis->resolveDnsName(); } if (shis && shis->_listener && shis->_semaphore) { shis->connectTcp(); } if (shis && shis->_listener && shis->_semaphore) { shis->establishSsl(); } if (shis && shis->_listener && shis->_semaphore) { shis->sendHttpRequest(); } if (shis && shis->_listener && shis->_semaphore) { shis->readHttpReply(); } }); } void Zomby::resolveDnsName() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::connectTcp() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::establishSsl() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::sendHttpRequest() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } void Zomby::readHttpReply() { doSomething(*_listener, std::string(typeid(*this).name()) + "::" + __func__); } } // namespace SteppingZomby
main.cpp #include <chrono> #include <thread> #include <sstream> #include "SteppingZomby/SteppingZomby.h" #include "Common/Impl/WriteToConsoleListener.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto steppingZomby = SteppingZomby::Zomby::create(); steppingZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(1500)); } // Zombies should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; }
Output KonsolN13SteppingZomby5ZombyE :: resolDnsName dimulai
N13SteppingZomby5ZombyE :: resolDnsName selesai
N13SteppingZomby5ZombyE :: connectTcp dimulai
=================================================== ===========
| Zomby terbunuh |
=================================================== ===========
N13SteppingZomby5ZombyE :: connectTcp selesai
N13SteppingZomby5ZombyE :: establishmentSsl dimulai
N13SteppingZomby5ZombyE :: establishmentSsl selesai
N13SteppingZomby5ZombyE :: sendHttpRequest dimulai
N13SteppingZomby5ZombyE :: sendHttpRequest selesai
N13SteppingZomby5ZombyE :: readHttpReply dimulai
N13SteppingZomby5ZombyE :: readHttpReply selesai
N13SteppingZomby5ZombyE :: ~ Zomby
N6Common22WriteToConsoleListenerE :: ~ WriteToConsoleListener
Seperti pada contoh sebelumnya, panggilan untuk menjalankanOnce () menyebabkan referensi melingkar.
Tapi kali ini, penghancur Zomby dan WriteToConsoleListener dipanggil. Semua sumber daya dirilis dengan benar hingga aplikasi dihentikan. Kebocoran memori tidak terjadi.
Lalu apa masalahnya?Masalahnya adalah bahwa zombie hidup terlalu lama - sekitar tiga setengah detik setelah penghancuran semua tautan eksternal yang kuat dan lemah. Sekitar tiga detik lebih lama dari seharusnya. Dan selama ini dia terlibat dalam mempromosikan implementasi koneksi HTTPS - sampai dia membawanya ke akhir. Terlepas dari kenyataan bahwa hasilnya tidak diperlukan lagi. Terlepas dari kenyataan bahwa logika bisnis yang unggul mencoba menghentikan zombie.
Nah, pikirkanlah, Anda mendapatkan jawaban yang tidak Anda butuhkan ....Dalam kasus koneksi HTTPS klien, konsekuensi
di pihak kami mungkin sebagai berikut:
- konsumsi memori;
- Konsumsi CPU;
- Konsumsi port TCP;
- bandwidth saluran komunikasi (baik permintaan maupun responsnya dapat berupa volume dalam megabyte);
- data yang tidak terduga dapat mengganggu pengoperasian logika bisnis tingkat tinggi - hingga transisi ke cabang eksekusi yang salah atau perilaku yang tidak terdefinisi, karena mekanisme pemrosesan respons mungkin sudah dihancurkan.
Dan
di sisi yang jauh (jangan lupa - permintaan HTTPS ditujukan untuk seseorang) - pemborosan sumber daya yang persis sama, plus itu mungkin:
- Menerbitkan foto kucing di situs web perusahaan;
- Menonaktifkan pemanas di bawah lantai di dapur Anda;
- pelaksanaan perintah perdagangan di bursa;
- transfer uang dari akun Anda;
- Peluncuran rudal balistik antarbenua.
Logika bisnis mencoba menghentikan zombie dengan menghapus semua tautan kuat dan lemah ke sana. Penghentian kemajuan permintaan HTTPS
seharusnya terjadi - belum terlambat, data level aplikasi belum dikirim.
Tapi zombie memutuskan dengan caranya sendiri.
Logika bisnis dapat membuat objek baru sebagai ganti zombie dan sekali lagi mencoba menghancurkannya, melipatgandakan sumber daya.
Dalam kasus proses yang berkelanjutan (misalnya, koneksi Websocket), pemborosan sumber daya dapat berlanjut selama berjam-jam, dan jika ada mekanisme rekoneksi otomatis dalam implementasi ketika koneksi terputus, umumnya dapat dihentikan.
Valgrind?Tidak ada peluang. Semuanya dilepaskan dan dibersihkan dengan benar. Terlambat dan bukan dari utas utama, tetapi sepenuhnya benar.
Boozdedzomby
Contoh ini menggunakan boozd :: azzio library, yang merupakan tiruan dari boost :: asio. Terlepas dari kenyataan bahwa tiruannya agak kasar, itu memungkinkan kami untuk menunjukkan esensi masalah. io_context::async_read ( , ), :
β stream, ;
β , ;
β callback-, .
io_context::async_read callback, (, ). io_context::run() ( , ).
buffer.h #pragma once #include <vector> namespace boozd::azzio { using buffer = std::vector<int>; } // namespace boozd::azzio
stream.h #pragma once #include <optional> namespace boozd::azzio { class stream { public: virtual ~stream() = default; virtual std::optional<int> read() = 0; }; } // namespace boozd::azzio
io_context.h #pragma once #include <functional> #include <optional> #include "buffer.h" namespace boozd::azzio { class stream; class io_context { public: ~io_context(); enum class error_code {no_error, good_error, bad_error, unknown_error, known_error, well_known_error}; using handler = std::function<void(error_code)>; // Start an asynchronous operation to read a certain amount of data from a stream. // This function is used to asynchronously read a certain number of bytes of data from a stream. // The function call always returns immediately. void async_read(stream& s, buffer& b, handler&& handler); // Run the io_context object's event processing loop. void run(); private: using pack = std::tuple<stream&, buffer&>; using pack_optional = std::optional<pack>; using handler_optional = std::optional<handler>; pack_optional _pack_optional; handler_optional _handler_optional; }; } // namespace boozd::azzio
io_context.cpp #include <iostream> #include <thread> #include <chrono> #include "io_context.h" #include "stream.h" namespace boozd::azzio { io_context::~io_context() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } void io_context::async_read(stream& s, buffer& b, io_context::handler&& handler) { _pack_optional.emplace(s, b); _handler_optional.emplace(std::move(handler)); } void io_context::run() { if (_pack_optional && _handler_optional) { auto& [s, b] = *_pack_optional; using namespace std::chrono; auto start = steady_clock::now(); while (duration_cast<milliseconds>(steady_clock::now() - start).count() < 1000) { if (auto read = s.read()) b.emplace_back(*read); std::this_thread::sleep_for(milliseconds(100)); } (*_handler_optional)(error_code::no_error); } } } // namespace boozd::azzio
boozd::azzio::stream, :
impl/random_stream.h #pragma once #include "boozd/azzio/stream.h" namespace boozd::azzio { class random_stream final : public stream { public: ~random_stream() override; std::optional<int> read() override; }; } // namespace boozd::azzio
impl/random_stream.cpp #include <iostream> #include "random_stream.h" namespace boozd::azzio { boozd::azzio::random_stream::~random_stream() { std::cout << typeid(*this).name() << "::" << __func__ << std::endl; } std::optional<int> random_stream::read() { if (!(rand() & 0x1)) return rand(); return std::nullopt; } } // namespace boozd::azzio
BoozdedZomby -. - async_read(), boozd::azzio run(). boozd::azzio ( ) callback-. , , - shared_from_this.
BoozdedZomby.h #pragma once #include <memory> #include <atomic> #include <thread> #include "Common/Manager.h" #include "boozd/azzio/buffer.h" #include "boozd/azzio/io_context.h" #include "boozd/azzio/impl/random_stream.h" namespace Common { class Listener; } // namespace Common namespace BoozdedZomby { class Zomby final : public Common::Manager, public std::enable_shared_from_this<Zomby> { public: static std::shared_ptr<Zomby> create(); ~Zomby() override; void runOnce(std::shared_ptr<Common::Listener> listener) override; private: Zomby(); using Semaphore = std::atomic<bool>; Semaphore _semaphore = false; std::shared_ptr<Common::Listener> _listener; boozd::azzio::random_stream _stream; boozd::azzio::buffer _buffer; boozd::azzio::io_context _context; std::thread _thread; }; } // namespace BoozdedZomby
BoozdedZomby.cpp #include <iostream> #include <sstream> #include "boozd/azzio/impl/random_stream.h" #include "BoozdedZomby.h" #include "Common/Listener.h" namespace BoozdedZomby { Zomby::Zomby() = default; std::shared_ptr<Zomby> Zomby::create() { return std::shared_ptr<Zomby>(new Zomby()); } Zomby::~Zomby() { _semaphore = false; if (_thread.joinable()) { _thread.detach(); } if (_listener) { std::ostringstream buf; buf << typeid(*this).name() << "::" << __func__ << std::endl; _listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } void Zomby::runOnce(std::shared_ptr<Common::Listener> listener) { if (_semaphore) { throw std::runtime_error("BoozdedZomby::Zomby::runOnce() called twice"); } _listener = listener; _semaphore = true; _thread = std::thread([shis = shared_from_this()]() { while (shis && shis->_semaphore && shis->_listener) { auto handler = [shis](auto errorCode) { if (shis && shis->_listener && errorCode == boozd::azzio::io_context::error_code::no_error) { std::ostringstream buf; buf << "BoozdedZomby has got a fresh data: "; for (auto const &elem : shis->_buffer) buf << elem << ' '; buf << std::endl; shis->_listener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } }; shis->_buffer.clear(); shis->_context.async_read(shis->_stream, shis->_buffer, handler); shis->_context.run(); } }); } } // namespace BoozdedZomby
main.cpp #include <chrono> #include <thread> #include <sstream> #include "BoozdedZomby/BoozdedZomby.h" #include "Common/Impl/WriteToConsoleListener.h" int main() { auto writeToConsoleListener = Common::WriteToConsoleListener::instance(); { auto boozdedZomby = BoozdedZomby::Zomby::create(); boozdedZomby->runOnce(writeToConsoleListener); std::this_thread::sleep_for(std::chrono::milliseconds(4500)); } // Zombies should be killed here { std::ostringstream buf; buf << "============================================================\n" << "| Zomby was killed |\n" << "============================================================\n"; if (writeToConsoleListener) { writeToConsoleListener->processData(std::make_shared<Common::Listener::Data>(buf.str())); } } std::this_thread::sleep_for(std::chrono::milliseconds(5000)); return 0; }
BoozdedZomby has got a fresh data: 1144108930 101027544 1458777923 1115438165 74243042
BoozdedZomby has got a fresh data: 143542612 1131570933
BoozdedZomby has got a fresh data: 893351816 563613512 704877633
BoozdedZomby has got a fresh data: 1551901393 1399125485 1899894091 937186357 590357944 357571490
============================================================
| Zomby was killed |
============================================================
BoozdedZomby has got a fresh data: 1927702196 130060903 1083454666 2118797801 2035308228 824938981
BoozdedZomby has got a fresh data: 2020739063 1635339425 34075629
BoozdedZomby has got a fresh data: 2146319451 500782188 1269406752 884936716 892053144
BoozdedZomby has got a fresh data: 330111137 1723153177 1070477904
BoozdedZomby has got a fresh data: 343098142 280090412 589673557 889688008 2014119113 388471006
run_once() . . , :
β boozdedZomby;
β writeToConsoleListener;
β .
.
.
?. . boost::asio. , β ( ).
Valgrind?Dulu. Meskipun sepertinya telah mendeteksi kebocoran.Zombi di alam liar
! !.
HTTP-Websocket-boost , BoozdedZomby + SteppingZomby. , . , production β , .
, boost::asio::io_context!β¦ n (, -), .
:
stackoverflow ,,Kesimpulan
, «».
, .
std::thread β .
, .
event-driven, (polling-based).
.
,
. std::enable_shared_from_this, ( β ). , : - .
, SteppingZomby. β shared_from_this ( , , β 1 6 ).
β , . .
, , . std::enable_shared_from_this β
.
PS: β .