كيو تي غير متزامن مكتبة القطعة

يوم جيد للجميع. أريد أن أتحدث قليلاً عن مشروع qt-async الخاص بي ، فربما يبدو الأمر ممتعًا أو مفيدًا لشخص ما.

منذ فترة طويلة تم تضمين التزامن وتعدد مؤشرات الترابط بشكل خطير في الحياة اليومية للمطورين. تم تصميم العديد من اللغات والمكتبات الحديثة مع مراعاة الاستخدام غير المتزامن. لغة C ++ تتحرك ببطء أيضًا في هذا الاتجاه - ظهرت الأمراض المنقولة جنسياً :: الخيط ، الأمراض المنقولة جنسيا :: الوعد / المستقبل ، على وشك إحضار coroutines والشبكات. مكتبة Qt أيضًا لا تتخلف ، وتقدم نظائرها QThread ، QRunnable ، QThreadPool ، QFuture ، إلخ. في الوقت نفسه ، لم أجد عناصر واجهة مستخدم لعرض الإجراءات غير المتزامنة في كيو تي (ربما كنت أبحث بشكل سيء وصحيح إذا كنت مخطئًا).

لذلك ، قررت التعويض عن القصور ومحاولة تطبيق هذه القطعة بنفسي. التنمية متعددة مؤشرات الترابط هي عمل معقد ولكنه مثير للاهتمام.

قبل متابعة تنفيذ التطبيق المصغّر ، تحتاج إلى وصف النموذج الذي سيقدمه للمستخدم في شكل نافذة. في أكثر أشكاله عمومية ، يبدو لي تشغيل الأداة المصغّرة كما يلي: في وقت ما ، يبدأ المستخدم أو النظام عملية غير متزامنة. في هذه المرحلة ، تُظهر الأداة المصغرة تقدم العملية أو مجرد إشارة إلى العملية. اختياريا ، المستخدم لديه القدرة على إلغاء العملية. بعد ذلك ، يتم إكمال العملية غير المتزامنة بطريقتين: إما أن يحدث خطأ وتعرض الأداة المصغرة الخاصة بنا ، أو تعرض الأداة المصغرة نتيجة العملية الناجحة.

وبالتالي ، يمكن أن يكون نموذجنا في واحدة من ثلاث حالات:

  1. التقدم - هناك عملية غير متزامنة قيد التقدم
  2. خطأ - فشلت العملية غير المتزامنة
  3. تم إكمال عملية القيمة غير المتزامنة بنجاح

في كل حالة من الحالات ، يجب أن يخزن النموذج البيانات المقابلة ، لذلك دعوت نموذج AsyncValue. من المهم أن نلاحظ أن العملية غير المتزامنة بحد ذاتها ليست جزءًا من نموذجنا ، بل تقوم فقط بتبديل حالتها. اتضح أنه يمكن استخدام AsyncValue مع أي مكتبة غير متزامنة ، مع ملاحظة نمط استخدام بسيط:

  1. في بداية العملية غير المتزامنة ، اضبط AsuncValue على التقدم
  2. في النهاية - إما في خطأ أو في القيمة ، وهذا يتوقف على نجاح العملية
  3. اختياريًا ، أثناء العملية ، يمكنك تحديث بيانات التقدم والاستماع إلى علامة الإيقاف إذا كان لدى المستخدم الفرصة لإيقاف العملية.

هنا مثال تخطيطي باستخدام QRunnable:

class MyRunnable : public QRunnable { public: MyRunnable(AsyncValue& value) : m_value(value) {} void run() final { m_value.setProgress(...); // do calculation if (success) m_value.setValue(...); else m_value.setError(...); } private: AsyncValue& m_value; } 

نفس المخطط للعمل مع الأمراض المنقولة جنسيا :: موضوع:

 AsyncValue value; std::thread thread([&value] () { value.setProgress(...); // do calculation if (success) value.setValue(...); else value.setError(...); }); 

وبالتالي ، فإن الإصدار الأول من فصلنا قد يبدو مثل هذا:

 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; // public API private: QReadWriteLock m_lock; std::variant<ValueType, ErrorType, ProgressType> m_value; }; 

يعلم كل من يأتي عبر فصول تدعم تعدد مؤشرات الترابط أن واجهة هذه الفئات مختلفة عن نظائرها المترابطة. على سبيل المثال ، وظيفة size () غير مجدية وخطيرة في متجه متعدد الخيوط. يمكن أن تصبح نتيجتها غير صالحة على الفور ، حيث يمكن تعديل المتجه في الوقت الحالي في سلسلة رسائل أخرى.

يجب أن يتمكن مستخدمو فئة AsyncValue من الوصول إلى بيانات الفصل. يمكن أن يكون إصدار نسخة من البيانات مكلفًا ، ويمكن أن يكون أي نوع من أنواع ValueType / ErrorType / ProgressType ثقيلًا. يعد إصدار رابط للبيانات الداخلية أمرًا خطيرًا - في أي وقت قد يصبح غير صالح. الحل التالي هو المقترح:

1. منح حق الوصول إلى البيانات من خلال وظائف accessValue / accessError / accessProgress ، والتي يتم تلقي lambdas التي تتلقى البيانات المقابلة. على سبيل المثال:

 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; } 

وبالتالي ، يتم الوصول إلى القيمة الداخلية بالرجوع وتحت القفل للقراءة. وهذا يعني أن الارتباط في وقت الوصول لن يصبح غير صالح.

2. يمكن لمستخدم AsyncValue في وظيفة accessValue أن يتذكر الرابط إلى البيانات الداخلية ، شريطة أن يكون مشتركًا في الإشارة التي تم تغييرها وبعد معالجة الإشارة يجب ألا يستخدم هذا الرابط بعد الآن ، لأن سوف تصبح غير صالحة.

في ظل هذه الظروف ، يكون المستهلك AsyncValue مضمونًا دائمًا ليتمتع بوصول صالح ومناسب للبيانات. هذا الحل له العديد من النتائج التي تؤثر على تنفيذ فئة AsyncValue.

أولاً ، يجب أن يرسل صفنا إشارة عندما تتغير الحالة ، ولكن في نفس الوقت يكون هذا هو القالب. سيتعين علينا إضافة فئة Qt أساسية ، حيث يمكننا تحديد الإشارة التي ستقوم الأداة بتحديث محتواها ، وسيعمل جميع المهتمين على تحديث روابط للبيانات الداخلية.

 lass AsyncValueBase : public QObject { Q_OBJECT Q_DISABLE_COPY(AsyncValueBase) signals: void stateChanged(); }; 

ثانياً ، يجب حظر لحظة إرسال الإشارة للقراءة (بحيث لا يمكن تغيير AsyncValue حتى يعالج الجميع الإشارة) ، والأهم من ذلك ، في هذه اللحظة يجب أن يكون هناك روابط صالحة لبيانات جديدة وقديمة. لأنه في عملية إرسال الإشارة ، لا يزال بعض مستهلكي AsyncValue يستخدمون الارتباطات القديمة ، والذين يستخدمون الإشارة يستخدمون الروابط الجديدة.

اتضح أن std :: variant ليست مناسبة لنا وسيتعين علينا تخزين البيانات في الذاكرة الديناميكية بحيث لا تتغير عناوين البيانات الجديدة والقديمة.

استطراد صغير.

يمكنك مراعاة التطبيقات الأخرى للفئة AsyncValue التي لا تتطلب تخصيصات ديناميكية:

  1. إعطاء المستهلكين فقط نسخ من البيانات الداخلية AsyncValue. كما كتبت سابقًا ، قد يكون هذا الحل أكثر من الحد الأمثل إذا كانت البيانات كبيرة.
  2. حدد إشارتين بدلاً من واحدة: stateWillChange / stateDidChange. لإلزام المستهلكين بالتخلص من الروابط القديمة عند الإشارة الأولى وتلقي روابط جديدة عند الإشارة الثانية. هذا المخطط ، كما يبدو لي ، يزيد من تعقيد مستهلكي AsyncValue ، لأنه لديهم فترات زمنية عند رفض الوصول إلى AsyncValue.

يتم الحصول على التنفيذ التخطيطي التالي لوظيفة setValue:

 void AsyncValue::setValue(...) {  m_lock            {   m_lock          m_lock   }  stateChanged       m_lock   }; 

كما ترون ، نحتاج إلى زيادة قفل m_lock للكتابة وإعادته مرة أخرى للقراءة. لسوء الحظ ، لا يوجد مثل هذا الدعم في فئة QReadWriteLock. يمكنك تحقيق الوظيفة المطلوبة من خلال زوج من QMutex / QReadWriteLock. فيما يلي تطبيق لفئة AsyncValue ، بالقرب من الحقيقي:

 //   AsyncValue enum class ASYNC_VALUE_STATE { VALUE, ERROR, PROGRESS }; Q_DECLARE_METATYPE(ASYNC_VALUE_STATE); //        class AsyncValueBase : public QObject { Q_OBJECT Q_DISABLE_COPY(AsyncValueBase) signals: void stateChanged(ASYNC_VALUE_STATE state); protected: explicit AsyncValueBase(ASYNC_VALUE_STATE state, QObject* parent = nullptr); //     PromoteToWriteLock/DemoteToReadLock QMutex m_writeLock; QReadWriteLock m_contentLock; //   ASYNC_VALUE_STATE m_state; }; template <typename ValueType_t, typename ErrorType_t, typename ProgressType_t> class AsyncValueTemplate : public AsyncValueBase { //  struct Content { std::unique_ptr<ValueType_t> value; std::unique_ptr<ErrorType_t> error; std::unique_ptr<ProgressType+t> progress; }; Content m_content; public: using ValueType = ValueType_t; using ErrorType = ErrorType_t; using ProgressType = ProgressType_t; //    template <typename... Args> void emplaceValue(Args&& ...arguments) { moveValue(std::make_unique<ValueType>(std::forward<Args>(arguments)...)); } //    void moveValue(std::unique_ptr<ValueType> value) { //       Content oldContent; //   emplaceXXX/moveXXX    QMutexLocker writeLocker(&m_writeLock); { //       QWriteLocker locker(&m_contentLock); //      oldContent = std::move(m_content); //    m_content.value = std::move(value); //    m_state = ASYNC_VALUE_STATE::VALUE; //     } //   emitStateChanged(); //    emplaceXXX/moveXXX  //    } //   value void emplaceError(Args&& ...arguments); void moveError(std::unique_ptr<ErrorType> error); void emplaceProgress(Args&& ...arguments); void moveProgress(std::unique_ptr<ProgressType> progress); template <typename Pred> bool accessValue(Pred valuePred) { //     QReadLocker locker(&m_contentLock); //    if (m_state != ASYNC_VALUE_STATE::VALUE) return false; //      valuePred(*m_content.value); //     return true; } //  accessValue bool accessError(Pred errorPred) bool accessProgress(Pred progressPred) }; 

بالنسبة لأولئك الذين لا تعبوا ولا تضيعون ، نواصل.

كما ترون ، لدينا وظائف accessXXX التي لا تنتظر حتى يدخل AsyncValue في الحالة المقابلة ، ولكن ببساطة يعود خطأ. من المفيد في بعض الأحيان الانتظار بشكل متزامن حتى تظهر قيمة أو خطأ في AsyncValue. في الواقع ، نحن بحاجة إلى تناظرية std :: future :: get. هنا هو توقيع الوظيفة:

 template <typename ValuePred, typename ErrorPred> void wait(ValuePred valuePred, ErrorPred errorPred); 

لكي تعمل هذه الوظيفة ، نحتاج إلى متغير شرط - كائن التزامن الذي يمكن توقعه في مؤشر ترابط واحد وتنشيطه في مؤشر ترابط آخر. في وظيفة الانتظار ، يجب أن ننتظر ، وعند تغيير حالة AsyncValue من التقدم إلى القيمة أو الخطأ ، يجب علينا إخطار النوادل.

أدت إضافة حقل آخر إلى فئة AsyncValue ، وهو أمر ضروري في حالات نادرة عند استخدام وظيفة الانتظار ، إلى التفكير - هل يمكن جعل هذا الحقل اختياريًا؟ الجواب واضح ، بالطبع من الممكن إذا قمت بتخزين std :: unique_ptr وإنشائه إذا لزم الأمر. السؤال الثاني الذي يطرح نفسه - هل من الممكن جعل هذا الحقل اختياريًا وليس تخصيصات ديناميكية. من يهتم ، يرجى إلقاء نظرة على الكود التالي. الفكرة الرئيسية هي كما يلي: مكالمة الانتظار الأولى تنشئ بنية QWaitCondition على المكدس وتكتب مؤشرها إلى AsyncValue ، تحقق من مكالمات الانتظار اللاحقة ببساطة إذا كان المؤشر غير فارغ ، استخدم البنية حسب هذا المؤشر ، إذا كان المؤشر فارغًا ، انظر أعلاه للحصول على مكالمة الانتظار الأولى .

 class AsyncValueBase : public QObject { ... struct Waiter { //     QWaitCondition waitValue; //   wait quint16 subWaiters = 0; //  wait     QWaitCondition waitSubWaiters; }; //    Waiter* m_waiter = nullptr; }; template <typename ValuePred, typename ErrorPred> void wait(ValuePred valuePred, ErrorPred errorPred) { //   -      if (access(valuePred, errorPred)) return; //  AsyncValue   QMutexLocker writeLocker(&m_writeLock); //     if (access(valuePred, errorPred)) return; //    wait  if (!m_waiter) { //  Waiter   Waiter theWaiter; //       if SCOPE_EXIT { //     wait, //    theWaiter if (m_waiter->subWaiters > 0) { //    subWaiters   do { m_waiter->waitSubWaiters.wait(&m_writeLock); } while (m_waiter->subWaiters != 0); } //   wait  , //       Waiter m_waiter = nullptr; }; //    Waiter  AsyncValue //    wait   m_waiter = &theWaiter; //   AsyncValue     Value  Error //    do { m_waiter->waitValue.wait(&m_writeLock); } while (!access(valuePred, errorPred)); } //   wait   else { //       else SCOPE_EXIT { //      m_waiter->subWaiters -= 1; //     ->   wait if (m_waiter->subWaiters == 0) m_waiter->waitSubWaiters.wakeAll(); }; //      m_waiter->subWaiters += 1; //   AsyncValue     Value  Error //    do { m_waiter->waitValue.wait(&m_writeLock); } while (!access(valuePred, errorPred)); } } 

كما ذكرنا سابقًا ، لا يحتوي AsyncValue على طريقة للحوسبة غير المتزامنة حتى لا تكون مرتبطة بمكتبة معينة. بدلاً من ذلك ، يتم استخدام الوظائف المجانية التي تقوم بتنفيذ التزامن بطريقة أو بأخرى. التالي مثال عن حساب AsyncValue على تجمع مؤشر ترابط:

 template <typename AsyncValueType, typename Func, typename... ProgressArgs> bool asyncValueRunThreadPool(QThreadPool *pool, AsyncValueType& value, Func&& func, ProgressArgs&& ...progressArgs) { //    auto progress = std::make_unique<typename AsyncValueType::ProgressType>(std::forward<ProgressArgs>(progressArgs)...); //    auto progressPtr = progress.get(); //    AsyncValue if (!value.startProgress(std::move(progress))) return false; QtConcurrent::run(pool, [&value, progressPtr, func = std::forward<Func>(func)](){ SCOPE_EXIT { //     AsyncValue,    value.completeProgress(progressPtr); }; //  AsyncValue func(*progressPtr, value); }); return true; } 

تطبق المكتبة دالتين متشابهتين أخريين: asyncValueRunNetwork لمعالجة طلبات الشبكة و asyncValueRunThread ، التي تنفذ عملية على سلسلة رسائل تم إنشاؤها حديثًا. يمكن لمستخدمي المكتبة بسهولة إنشاء وظائفهم الخاصة واستخدامها هناك لاستخدام تلك الأدوات غير المتزامنة التي يستخدمونها في أماكن أخرى.

لزيادة الأمان ، تم توسيع فئة AsyncValue مع فئة قالب AsyncTrackErrorsPolicy أخرى ، والتي تسمح لك بالرد على سوء استخدام AsyncValue. على سبيل المثال ، فيما يلي التطبيق الافتراضي للدالة AsyncTrackErrorsPolicy :: inProgressWhileDestruct ، والتي سيتم استدعاؤها إذا تم تدمير AsyncValue أثناء تشغيل العملية غير المتزامنة:

  void inProgressWhileDestruct() const { Q_ASSERT(false && "Destructing value while it's in progress"); } 

أما بالنسبة للحاجيات ، فإن تنفيذها بسيط وموجز. AsyncWidget هو حاوية تحتوي على عنصر واجهة تعامل مستخدم لعرض خطأ أو تقدم أو قيمة بناءً على الحالة التي يوجد بها AsyncValue حاليًا.

  virtual QWidget* createValueWidgetImpl(ValueType& value, QWidget* parent); virtual QWidget* createErrorWidgetImpl(ErrorType& error, QWidget* parent); virtual QWidget* createProgressWidgetImpl(ProgressType& progress, QWidget* parent); 

يلتزم المستخدم بإعادة تعريف الوظيفة الأولى فقط ، لعرض القيمة ، بينما يكون الآخران لهما تطبيقات افتراضية.

تحولت مكتبة qt-async إلى أنها مضغوطة ولكنها مفيدة في الوقت نفسه. باستخدام AsyncValue / AsyncWidget ، حيث كانت هناك وظائف متزامنة في السابق وواجهة المستخدم الرسومية الثابتة ، سوف يسمح لتطبيقاتك أن تصبح حديثة وأكثر استجابة.

بالنسبة لأولئك الذين قرأوا المكافأة حتى النهاية - فيديو للتطبيق التجريبي

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


All Articles