Hari baik untuk semua Saya ingin berbicara sedikit tentang proyek
qt-async saya , mungkin itu akan terlihat menarik atau bahkan berguna bagi seseorang.
Asynchrony dan multithreading telah lama dimasukkan dalam kehidupan sehari-hari para pengembang. Banyak bahasa dan perpustakaan modern dirancang dengan mempertimbangkan penggunaan yang tidak sinkron. Bahasa C ++ juga perlahan bergerak ke arah ini - std :: utas, std :: janji / masa depan telah muncul, mereka akan membawa coroutine dan jaringan. Pustaka Qt juga tidak ketinggalan, menawarkan analognya QThread, QRunnable, QThreadPool, QFuture, dll. Pada saat yang sama, saya tidak menemukan widget untuk menampilkan tindakan asinkron di Qt (mungkin saya terlihat buruk, benar jika saya salah).
Oleh karena itu, saya memutuskan untuk menebus kekurangan dan mencoba menerapkan widget semacam itu sendiri. Pengembangan multithreaded adalah bisnis yang kompleks, tetapi menarik.
Sebelum melanjutkan dengan penerapan widget, Anda perlu menjelaskan model yang akan disajikan kepada pengguna dalam bentuk jendela. Dalam bentuknya yang paling umum, pengoperasian widget menurut saya adalah sebagai berikut: pada suatu saat, pengguna atau sistem memulai operasi asinkron. Pada titik ini, widget menunjukkan kemajuan operasi atau hanya indikasi operasi. Secara opsional, pengguna memiliki kemampuan untuk membatalkan operasi. Selanjutnya, operasi asinkron selesai dengan dua cara: kesalahan terjadi dan widget kami menunjukkannya, atau widget menunjukkan hasil operasi yang berhasil.
Dengan demikian, model kami dapat di salah satu dari tiga negara:
- Kemajuan - operasi asinkron sedang berlangsung
- Kesalahan - operasi asinkron gagal
- Nilai - operasi asinkron berhasil diselesaikan
Di setiap negara, model harus menyimpan data yang sesuai, jadi saya memanggil model AsyncValue. Penting untuk dicatat bahwa operasi asinkron itu sendiri bukan merupakan bagian dari model kami, ia hanya mengubah statusnya. Ternyata AsyncValue dapat digunakan dengan pustaka asinkron apa pun, mengamati pola penggunaan yang sederhana:
- Di awal operasi asinkron, atur AsuncValue ke Progress
- Pada akhirnya - baik dalam Kesalahan atau Nilai, tergantung pada keberhasilan operasi
- Secara opsional, selama operasi, Anda dapat memperbarui data Kemajuan dan mendengarkan bendera Berhenti jika pengguna memiliki kesempatan untuk menghentikan operasi.
Berikut ini adalah contoh skematik menggunakan QRunnable:
class MyRunnable : public QRunnable { public: MyRunnable(AsyncValue& value) : m_value(value) {} void run() final { m_value.setProgress(...);
Skema yang sama untuk bekerja dengan std :: thread:
AsyncValue value; std::thread thread([&value] () { value.setProgress(...);
Dengan demikian, versi pertama dari kelas kami dapat terlihat seperti ini:
template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t> class AsyncValue { public: using ValueType = ValueType_t; using ErrorType = ErrorType_t; using ProgressType = ProgressType_t;
Setiap orang yang datang di kelas yang mendukung multithreading tahu bahwa antarmuka kelas tersebut berbeda dari analog single-threaded. Misalnya, fungsi size () tidak berguna dan berbahaya dalam vektor multi-ulir. Hasilnya dapat langsung menjadi tidak valid, karena vektor dapat dimodifikasi saat ini di utas lainnya.
Pengguna kelas AsyncValue harus dapat mengakses data kelas. Mengeluarkan salinan data bisa mahal, salah satu tipe ValueType / ErrorType / ProgressType bisa berat. Mengeluarkan tautan ke data internal berbahaya - kapan saja itu bisa menjadi tidak valid. Solusi berikut diusulkan:
1. Berikan akses ke data melalui fungsi accessValue / accessError / accessProgress, di mana lambdas diterima yang menerima data yang sesuai. Sebagai contoh:
template <typename Pred> bool accessValue(Pred valuePred) { QReadLocker locker(&m_lock); if (m_value.index() != 0) return false; valuePred(std::get<0>(m_value)); return true; }
Dengan demikian, akses ke nilai internal dilakukan dengan referensi dan di bawah kunci untuk membaca. Artinya, tautan pada saat akses tidak akan menjadi tidak valid.
2. Pengguna AsyncValue dalam fungsi accessValue dapat mengingat tautan ke data internal, asalkan ia berlangganan sinyal stateChanged dan setelah pemrosesan sinyal tidak boleh lagi menggunakan tautan ini, karena dia akan menjadi tidak valid.
Dalam kondisi seperti itu, konsumen AsyncValue selalu dijamin memiliki akses data yang valid dan nyaman. Solusi ini memiliki beberapa konsekuensi yang mempengaruhi implementasi kelas AsyncValue.
Pertama, kelas kita harus mengirim sinyal ketika keadaan berubah, tetapi pada saat yang sama itu adalah templat. Kami harus menambahkan kelas Qt dasar, tempat kami dapat menentukan sinyal yang digunakan widget untuk memperbarui kontennya, dan semua yang berminat akan memperbarui tautan ke data internal.
lass AsyncValueBase : public QObject { Q_OBJECT Q_DISABLE_COPY(AsyncValueBase) signals: void stateChanged(); };
Kedua, saat pengiriman sinyal harus diblokir untuk dibaca (sehingga AsyncValue tidak dapat diubah sampai semua orang memproses sinyal) dan, yang
paling penting , pada saat itu harus ada tautan yang valid ke data baru dan lama. Karena dalam proses pengiriman sinyal, beberapa konsumen AsyncValue masih menggunakan tautan lama, dan mereka yang memproses sinyal menggunakan yang baru.
Ternyata varian std :: tidak cocok untuk kami dan kami harus menyimpan data dalam memori dinamis sehingga alamat data baru dan lama tidak berubah.
Penyimpangan kecil.
Anda dapat mempertimbangkan implementasi lain dari kelas AsyncValue yang tidak memerlukan alokasi dinamis:
- Berikan hanya salinan data internal AsyncValue kepada konsumen. Seperti yang saya tulis sebelumnya, solusi seperti itu mungkin lebih suboptimal jika datanya besar.
- Tentukan dua sinyal alih-alih satu: stateWillChange / stateDidChange. Untuk mewajibkan konsumen untuk menyingkirkan tautan lama pada sinyal pertama dan menerima tautan baru pada sinyal kedua. Skema ini, menurut saya, terlalu mempersulit konsumen AsyncValue, karena mereka memiliki interval waktu ketika akses ke AsyncValue ditolak.
Implementasi skematis berikut dari fungsi setValue diperoleh:
void AsyncValue::setValue(...) { m_lock { m_lock m_lock } stateChanged m_lock };
Seperti yang Anda lihat, kita perlu meningkatkan kunci m_lock untuk menulis dan mengembalikannya untuk dibaca. Sayangnya, tidak ada dukungan seperti itu di kelas QReadWriteLock. Anda dapat mencapai fungsionalitas yang diinginkan dengan sepasang QMutex / QReadWriteLock. Berikut ini adalah implementasi dari kelas AsyncValue, hampir nyata:
Bagi mereka yang tidak lelah dan tidak tersesat, kami melanjutkan.
Seperti yang Anda lihat, kami memiliki fungsi accessXXX yang tidak menunggu sampai AsyncValue memasuki kondisi terkait, tetapi mengembalikan false. Terkadang berguna untuk menunggu secara tersinkronisasi hingga nilai atau kesalahan muncul di AsyncValue. Pada dasarnya, kita membutuhkan analog dari std :: future :: get. Inilah tanda tangan fungsinya:
template <typename ValuePred, typename ErrorPred> void wait(ValuePred valuePred, ErrorPred errorPred);
Agar fungsi ini berfungsi, kita memerlukan variabel kondisi - objek sinkronisasi yang dapat diharapkan di satu utas dan diaktifkan di yang lain. Dalam fungsi menunggu, kita harus menunggu, dan ketika mengubah status AsyncValue dari Progress ke Value atau Error, kita harus memberi tahu para pelayan.
Menambahkan bidang lain ke kelas AsyncValue, yang diperlukan dalam kasus yang jarang terjadi ketika fungsi tunggu digunakan, telah membuat saya berpikir - bisakah bidang ini dibuat opsional? Jawabannya jelas, tentu saja mungkin jika Anda menyimpan std :: unique_ptr dan membuatnya jika perlu. Pertanyaan kedua muncul - apakah mungkin membuat bidang ini opsional dan tidak membuat alokasi dinamis. Siapa peduli, silakan lihat kode berikut. Gagasan utamanya adalah sebagai berikut: panggilan tunggu pertama membuat struktur QWaitCondition pada stack dan menulis pointer-nya ke AsyncValue, panggilan tunggu selanjutnya cukup memeriksa apakah pointer tidak kosong, gunakan struktur oleh pointer ini, jika pointer kosong, lihat di atas untuk panggilan tunggu pertama .
class AsyncValueBase : public QObject { ... struct Waiter {
Seperti yang telah disebutkan, AsyncValue tidak memiliki metode untuk komputasi asinkron agar tidak terikat ke perpustakaan tertentu. Sebagai gantinya, fungsi bebas digunakan yang mengimplementasikan asynchrony dengan satu atau lain cara. Berikut ini adalah contoh komputasi AsyncValue pada kumpulan utas:
template <typename AsyncValueType, typename Func, typename... ProgressArgs> bool asyncValueRunThreadPool(QThreadPool *pool, AsyncValueType& value, Func&& func, ProgressArgs&& ...progressArgs) {
Perpustakaan mengimplementasikan dua fungsi yang lebih mirip: asyncValueRunNetwork untuk memproses permintaan jaringan dan asyncValueRunThread, yang melakukan operasi pada utas yang baru dibuat. Pengguna perpustakaan dapat dengan mudah membuat fungsi mereka sendiri dan menggunakannya di sana untuk menggunakan alat asinkron yang mereka gunakan di tempat lain.
Untuk meningkatkan keamanan, kelas AsyncValue telah diperluas dengan kelas templat AsyncTrackErrorsPolicy lainnya, yang memungkinkan Anda untuk merespons penyalahgunaan AsyncValue. Misalnya, berikut ini adalah implementasi default fungsi AsyncTrackErrorsPolicy :: inProgressWhileDestruct, yang akan dipanggil jika AsyncValue dihancurkan ketika operasi asinkron berjalan:
void inProgressWhileDestruct() const { Q_ASSERT(false && "Destructing value while it's in progress"); }
Sedangkan untuk widget, implementasinya cukup sederhana dan ringkas. AsyncWidget adalah wadah yang berisi widget untuk menampilkan kesalahan, atau kemajuan, atau nilai tergantung pada kondisi AsyncValue saat ini.
virtual QWidget* createValueWidgetImpl(ValueType& value, QWidget* parent); virtual QWidget* createErrorWidgetImpl(ErrorType& error, QWidget* parent); virtual QWidget* createProgressWidgetImpl(ProgressType& progress, QWidget* parent);
Pengguna wajib mendefinisikan ulang hanya fungsi pertama, untuk menampilkan nilai, dua lainnya memiliki implementasi standar.
Pustaka
qt-async ternyata kompak, tetapi pada saat yang sama cukup bermanfaat. Menggunakan AsyncValue / AsyncWidget, yang sebelumnya memiliki fungsi sinkron dan GUI statis, akan memungkinkan aplikasi Anda menjadi modern dan lebih responsif.
Bagi mereka yang telah membaca bonus hingga akhir - video dari aplikasi demo