أفضل الممارسات العمليّة لتعدّد مؤشّرات الترابط: نسخة C++ ── إزالة الحوادث بالبنية عبر RAII وjthread

· · Windows, تعدّد مؤشّرات الترابط, C++, Visual Studio, تطبيقات الأعمال, تحقيق الأخطاء, التصميم

«تصميم كان يعمل جيّداً في C# بدأ ينهار أحياناً بعد نقله إلى C++.» «استخدمنا std::thread، وعندما رُمي استثناء مات التطبيق كلّه فوراً عبر terminate.» «كنّا نوقف الأشياء بعلم volatile bool، لكن في إصدارات الإطلاق فقط، لم يتوقّف.» ── يحمل تعدّد مؤشّرات الترابط في C++ خطراً لا تملكه اللغات المُدارة: سباق البيانات، كما هو، سلوك غير معرَّف (UB). ليس الأمر أنّك قد تقرأ قيمة فاسدة فحسب؛ تنهار افتراضات تحسين المترجم، فتنتهي إلى حالة يمكن أن يحدث فيها أيّ شيء حرفاً.

هذه المقالة هي نسخة C++ من سلسلتنا العمليّة لتعدّد مؤشّرات الترابط. موجَّهة إلى المطوِّرين الذين يكتبون تطبيقات الأعمال وبرمجيّات التحكّم بالمعدّات ومكتبات DLL بـ C++ الحديث (C++17/20)، وتترجم المبادئ العامّة لتصميم تعدّد مؤشّرات الترابط ── لا تُضِف مؤشّرات الترابط مباشرة، قلِّل الحالة القابلة للتغيير المشتركة، طبِّق انضباط الأقفال، صمِّم كيف تتوقّف قبل أيّ شيء ── إلى أدوات C++ وWindows، مع الفخاخ الخاصّة بـ C++، كلّ ذلك مرتكزاً على مصادر أوّليّة حتّى أغسطس 2026. كُتبت لتقف وحدها. المبادئ نفسها، مبسوطة للغات أخرى، موجودة أيضاً في «نسخة .NET» و«نسخة C» و«نسخة Java».

1. الخلاصة أوّلاً

  • في C++، سباق البيانات ليس «قد تقرأ قيمة فاسدة» ── إنّه سلوك غير معرَّف. ألّا تترك وصولاً قابلاً للتغيير مشتركاً غير متزامن واحداً في الكود شرط مطلق، أكثر ممّا في اللغات الأخرى.1
  • لا تستخدم std::thread عارياً. إن جرى مدمِّر std::thread ومؤشّر الترابط ما يزال قابلاً للالتحام، يقتل std::terminate العمليّة فوراً. std::jthread في C++20 يلتحم تلقائيّاً في مدمِّره وفيه آليّة طلب توقّف (stop_token) مبنيّة.23
  • أمسك الأقفال دائماً عبر RAII. توقّف عن كتابة mtx.lock() يدوياً؛ استخدم lock_guard / scoped_lock. يحرِّر المدمِّر القفل بموثوقيّة حتّى إن رُمي استثناء. عند اكتساب عدّة أقفال معاً، يتولّى scoped_lock الأمر بخوارزميّة تجنّب الانسداد التامّ.4
  • volatile ليس أداة مزامنة. استخدم std::atomic للأعلام والعدّادات المشتركة، وstd::mutex لحماية عدّة متغيّرات معاً. يوفِّر std::atomic الذرّيّة والترتيب القائم على memory_order.5
  • أدِّ انتظار اللقاء بشكل المحمول لـ wait لدى condition_variable. متغيّرات الشرط معرَّضة للإيقاظ الزائف (الاستيقاظ بلا إشعار)، لذا فإنّ استدعاء wait بلا محمول مرتع للأخطاء.6
  • jthread + stop_token ‏(C++20) هو الشكل الأساسيّ لكيف توقِف مؤشّر ترابط. في البيئات السابقة ابنِ التوقّف التعاونيّ يدوياً بـ std::atomic<bool> زائد متغيّر شرط. عامل الإنهاء القسريّ لمؤشّر الترابط كأنّه شيء لا يوجد أصلاً في عالم C++.3
  • اعرف أنّ مدمِّر future قد يحجب قبل أن تستخدم std::async. أهمل القيمة المُرجَعة فتحصل على أثر التنفيذ التسلسليّ نفسه.7
  • كائنات تزامن Win32 تستحقّ مكانها فقط لسيناريوهات «العمل مع واجهات انتظار Win32» و«عبر العمليّات». في ما عدا ذلك، الكتابة على المكتبة القياسيّة الخيار الأفضل لقابليّة النقل والصيانة.8

2. لماذا تعدّد مؤشّرات الترابط صعب ── حالات السباق والانسداد التامّ والسلوك غير المعرَّف

مختصراً، المشاكل التي يجلبها تعدّد مؤشّرات الترابط نوعان، بغضّ النظر عن اللغة.

حالة السباق (race condition) خطأ يعتمد فيه الناتج على الترتيب الذي تصل به عدّة مؤشّرات ترابط إلى قطعة كود معيّنة. المثال الكلاسيكيّ عدّاد مشترك: التعبير الواحد ++count ينقسم على مستوى لغة الآلة إلى ثلاث خطوات ── قراءة، جمع، كتابة راجعة. إن دخل مؤشّرا ترابط تلك الخطوات الثلاث في الوقت نفسه، يُكتَب فوق جمع أحدهما ويُفقَد بكتابة الآخر. يتغيّر الناتج من تشغيل إلى تشغيل، وأيّ ناتج ستحصل عليه لا يمكن توقّعه.

مؤشّر الترابط Bالمتغيّر المشترك countمؤشّر الترابط Aمؤشّر الترابط Bالمتغيّر المشترك countمؤشّر الترابط Acount = 10حدثت زيادتان،ومع ذلك count = 11 ── فُقد جمع مؤشّر الترابط Aقراءة (10)قراءة (10)جمع محلّيّ (11)جمع محلّيّ (11)كتابة راجعة (11)كتابة راجعة (11)

الشكل 1: حالة سباق كلاسيكيّة تُفقَد فيها زيادة على عدّاد مشترك. إن تداخل مؤشّر ترابط آخر أثناء خطوات ++count الثلاث، فإنّ الكتابة الراجعة التي تحدث آخراً تكتب فوق الأخرى

الانسداد التامّ (deadlock) حالة ينتظر فيها مؤشّرا ترابط كلٌّ منهما قفلاً يمسكه الآخر، فلا يستطيع أيّ منهما التقدّم. يمسك مؤشّر الترابط A القفل 1 وينتظر القفل 2؛ ويمسك مؤشّر الترابط B القفل 2 وينتظر القفل 1 ── وهذا وحده يكفي ليتوقّفا إلى الأبد.

ينتظر اكتساب القفل 2ينتظر اكتساب القفل 1مؤشّر الترابط Aيمسك القفل 1مؤشّر الترابط Bيمسك القفل 2

الشكل 2: انتظار دائريّ للانسداد التامّ. في اللحظة التي تشكِّل فيها أسهم الانتظار حلقة، يتوقّف كلّ مؤشّر ترابط في تلك الحلقة إلى الأبد

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

2.1. في C++، سباق البيانات سلوك غير معرَّف مباشرة

فوق ذلك، لـ C++ طبقة أخرى لا تملكها اللغات الأخرى. بموجب معيار C++، إن وصل عدّة مؤشّرات ترابط إلى موقع الذاكرة نفسه بلا مزامنة وكان أحدها على الأقلّ يكتب، فذلك سباق بيانات، وهو سلوك غير معرَّف. يضع فصل التزامن في C++ Core Guidelines ‏(CP.2، «Avoid data races») هذا أوّل قاعدة مطلقة.1 السلوك غير المعرَّف ليس القصّة اللطيفة «قد تقرأ القيمة القديمة أو الجديدة». يحسِّن المترجم على مقدّمة أنّ سباق بيانات لا يوجد، لذا يحدث سلوك لا يمكن توقّعه من الشيفرة المصدر ── اختفاء فحص شرط من حلقة، إعادة ترتيب الكتابات أو دمجها ── على نحو مشروع. الحادث الكلاسيكيّ الذي «يفشل فيه علم توقّف volatile bool في إصدارات الإطلاق فقط» حالة كتاب مدرسيّ لهذا تماماً.

2.2. RAII هو الأساس

مقدّمة أخرى خاصّة بـ C++ هي الاستثناءات وإدارة الموارد. ليس لـ C++ finally؛ بدل ذلك لديه RAII (تحرير تلقائيّ عبر المدمِّرات)، وأدوات تعدّد مؤشّرات الترابط مصمَّمة على افتراض أنّك ستستخدمه. «أدِر الأقفال عبر عمر الكائن»؛ «اضمن التحام مؤشّر الترابط عبر عمر الكائن أيضاً» ── السير مع هذا الاصطلاح هو أساس كتابة C++ متعدّد مؤشّرات الترابط بأمان.

3. كيف تبدأ مؤشّر ترابط ── فخّ thread، وjthread

3.1. مدمِّر std::thread «مصمَّم ليسبِّب حوادث»

لـ std::thread فخّ معروف. إن جرى مدمِّره ومؤشّر الترابط ما يزال قابلاً للالتحام (لم يُلتحَم به ولم يُفصَل)، يُستدعى std::terminate وتموت العمليّة فوراً.9

void process()
{
    std::thread worker([]{ HeavyWork(); });
    DoSomething();      // ← إن رُمي استثناء هنا...
    worker.join();      // ← لا يُبلَغ join أبداً؛ مدمِّر worker يستدعي terminate
}

جعل هذا آمناً من الاستثناءات كان يتطلّب ضمان الالتحام بـ try/catch ── حالة مشوَّهة في لغة RAII كان على مؤشّرات الترابط وحدها أن تُدار يدوياً. يحلّ std::jthread في C++20 هذا. لأنّ مدمِّره يصدر طلب توقّف تلقائيّاً ثمّ يلتحم، يصير الكود أعلاه آمناً من الاستثناءات بمجرّد التحويل إلى std::jthread.2 على MSVC، يتوفّر <stop_token> وjthread من Visual Studio 2019 16.9 فصاعداً.3

std::threadلم يُلتحَم به ولم يُفصَلstd::threadأُلحم به أصلاًstd::jthread - C++20أُطلق مؤشّر الترابطماذا يحدثعندما يخرج النطاق؟std::terminateتموت العمليّة فوراًالتحام آمنrequest_stop + join تلقائيّانآمن حتّى إن رُمي استثناء

الشكل 3: عمر كائن مؤشّر الترابط وكيف ينتهي. std::thread محدَّد ليموت فوراً إن نسيت الالتحام، لذا من C++20 فصاعداً اجعل jthread هو الافتراضيّ

كقاعدة، لا تستخدم detach(). مؤشّر ترابط فقد أيّ وسيلة التحام يصير سبباً كلاسيكيّاً لانهيارات الإغلاق، يتسابق مع تدمير المتغيّرات الساكنة والكومة عند خروج العمليّة.

3.2. أدوات «فوق مستوى مؤشّر الترابط» ── async وfuture والخوارزميّات المتوازية

مبدأ نسخة .NET «لا تنشئ مؤشّرات الترابط بنفسك» يُسقَط في C++ على الأدوات التالية.

  • std::async + std::future: لمهمّة لاتزامنيّة لمرّة واحدة واستلام نتيجتها. غير أنّ ثمّة خصوصيّة مهمّة: future (أو آخر shared_future) المرتبط بمهمّة أُطلقت عبر std::async يحجب حتّى الاكتمال إن جرى مدمِّره والمهمّة ما تزال غير مكتملة.7 للعمل المُطلَق فعلاً بـ std::launch::async، إهمال future المُرجَع يعادل التنفيذ المتزامن في تلك النقطة. وأسوأ، إن لم تحدِّد سياسة إطلاق، فحرٌّ للتنفيذ أن يختار deferred (تنفيذاً كسولاً) افتراضيّاً، وفي تلك الحالة إن لم يستدعِ أحد get() / wait() فإنّ العمل لا يُنفَّذ أصلاً ويختفي صامتاً. إن أردت ضمان التنفيذ المتزامن، حدِّد std::launch::async صراحة، وليُدِر المالك عمر future.
  • PPL ── Parallel Patterns Library ── concurrency::parallel_for / parallel_for_each: تطبيق العمل بالتوازي عبر كلّ عنصر في مجموعة. غير أنّ العمل في تكرار واحد إن كان أصغر ممّا ينبغي، فإنّ كلفة fork/join تلتهم المكاسب، لذا كقاعدة وازِ على الحلقة الخارجيّة.10
  • خوارزميّات C++17 المتوازية ── std::execution::par: على MSVC تُوازى الخوارزميّات الرئيسة (لا كلّها).11 لاحظ أنّ استثناءً إن أفلت من معالجة عنصر تحت سياسة تنفيذ، يُستدعى std::terminate. وضع حدّ استثناء خاصّ بك (try/catch) داخل ردّ النداء يتبع التفكير نفسه لحدّ مؤشّر الترابط في القسم 6.

الخطّ المرسوم في موضع آخر ── «انتظار الإدخال/الإخراج ليس شيئاً تحلّه بإضافة مؤشّرات ترابط» ── ما يزال سارياً بلا تغيير. لكود Windows الأصليّ، فإنّ إدخال/إخراج OVERLAPPED وIOCP هما الأداتان اللتان تستقبلان ذلك العمل (للآليّات، انظر «أعماق إدخال/إخراج Windows، الجزء 2»).

4. تقليل الحالة القابلة للتغيير المشتركة ── التقسيم، التمرير بالقيمة، const، والطوابير

لا ينشأ التنازع إلّا عندما يحضر «عدّة مؤشّرات ترابط» و«بيانات قابلة للتغيير مشتركة» معاً. عدد مؤشّرات الترابط تفرضه المتطلّبات، لذا ما يستطيع التصميم قطعه هو المشاركة. الوسائل تقع في ثلاث عائلات ── التقسيم، جعلها غير قابلة للتغيير، وتسليم البيانات ── وإليك كيف تكتب كلّاً في C++.

قسِّم. في عمل مثل التجميع المتوازي، بدل أن يكتب كلّ مؤشّر ترابط في مجموع مشترك، أعطِ كلّ مؤشّر ترابط مجموعه الفرعيّ المحلّيّ وادمجها مرّة واحدة في النهاية. تهبط الكتابات إلى القيمة المشتركة من «كلّ تكرار» إلى «مرّة لكلّ مؤشّر ترابط»، فتقطع كلفة المزامنة ونافذة التنازع بمراتب. خطوة الدمج الواحدة تلك يمكن أن تتمّ بـ std::mutex أو بـ fetch_add على std::atomic ── كلاهما حسن.

مرِّر بالقيمة. إن سلّمت البيانات التي يحتاجها مؤشّر الترابط إليه بنسخ (أو نقل) عند البدء، صارت تلك البيانات حصراً له، ولا حاجة إلى مزامنة. التقاط اللامدا بالمرجع ([&]) ثمّ لمس متغيّر انتهى عمره حادث شائع، لذا ينبغي للامدا الممرَّرة إلى مؤشّرات الترابط أن تستخدم التقاطاً صريحاً، بالنسخ أو النقل كقاعدة. مع ذلك، «نُسخ، إذن حصريّ» لا يصمد إلّا عندما تكون القيمة رسماً بيانيّاً عميقاً للقيم لا يحتوي أسماء مستعارة مثل المؤشّرات أو shared_ptr. نسخ بنية تحتوي مؤشّراً خاماً يترك ما يشير إليه مشتركاً مع ذلك.

شارك كـ const. البيانات التي تُقرأ فقط آمنة للقراءة من أيّ عدد من مؤشّرات الترابط معاً. قيم الإعداد والبيانات الرئيسة ومدخلات الحساب وما شابه يمكن مشاركتها بلا مزامنة إن جعلتها مشاركة const لا تُعاد كتابتها بعد البناء (std::shared_ptr<const Config> مثلاً). تحذير واحد: ما يمنعه shared_ptr<const T> هو التغيير عبر ذلك المقبض بعينه فقط. إن بقي اسم مستعار غير const في مكان آخر، أو أُعيدت كتابة عضو mutable، يبقى التنازع ── فصمِّم لذلك أيضاً، حتّى «بعد انتهاء البناء، أطلق المرجع غير const ولا يكتب أحد بعده». مجرّد القرار أنّ «عندما يلزم تغيير، ابنِ كائناً جديداً واستبدله، بدل التغيير في المكان» يزيل قطعة حالة قابلة للتغيير كان عليك حمايتها (لإدارة عمر الاستبدال نفسه، انظر التنبيه في القسم 5.2).

سلِّم عبر طابور. وجِّه تدفّق البيانات بين مؤشّرات الترابط عبر طابور منتج/مستهلك بدل متغيّر مشترك. ليس في معيار C++ نوع قناة، لذا كتابة طابور صغير بـ std::mutex + std::condition_variable هو النمط المستقرّ.

template <typename T>
class BlockingQueue {
public:
    explicit BlockingQueue(std::size_t capacity) : capacity_(capacity)
    {
        if (capacity == 0)                          // السعة 0 فخّ ينتظر فيه كلّ Push إلى الأبد
            throw std::invalid_argument("capacity must be positive");
    }

    // ينتظر حتّى يتوفّر مكان (أو طلب توقّف) إن امتلأ. false يعني طلب توقّف.
    bool Push(T item, std::stop_token st)
    {
        {
            std::unique_lock lock(mtx_);
            if (!not_full_.wait(lock, st, [this]{ return queue_.size() < capacity_; }))
                return false;                       // أُوقظ بطلب توقّف
            if (st.stop_requested())                // إن اجتمع المكان والتوقّف، فضِّل التوقّف،
                return false;                       // وارفض الدفع بعد بدء التوقّف
            queue_.push(std::move(item));
        }
        not_empty_.notify_one();   // أشعِر خارج القفل
        return true;
    }

    // ينتظر طلب توقّف (stop_token) أو وصول عنصر. nullopt عند التوقّف.
    std::optional<T> Pop(std::stop_token st)
    {
        std::optional<T> item;
        {
            std::unique_lock lock(mtx_);
            if (!not_empty_.wait(lock, st, [this]{ return !queue_.empty(); }))
                return std::nullopt;                // أُوقظ بطلب توقّف
            if (st.stop_requested())                // إن اجتمع عنصر والتوقّف، فضِّل التوقّف،
                return std::nullopt;                // ولا تبدأ عملاً جديداً بعد بدء التوقّف
            item = std::move(queue_.front());
            queue_.pop();
        }
        not_full_.notify_one();
        return item;
    }

private:
    const std::size_t capacity_;
    std::mutex mtx_;
    std::condition_variable_any not_empty_;   // condition_variable_any، لاستخدام wait الواعي بـ stop_token
    std::condition_variable_any not_full_;
    std::queue<T> queue_;
};

هنا نقطتان تصميميّتان. الأولى، حدّ السعة واجعل جانب المنتج ينتظر عند الامتلاء. طابور بلا حدّ أعلى يصير قنبلة موقوتة في الترتيبات التي يسبق فيها الإنتاج الاستهلاك: «يواصل العمل»، لكن الذاكرة تكبر. حجب Push عند الامتلاء يعمل كضغط خلفيّ طبيعيّ، ينشر الحمل الزائد إلى أعلى المسار آليّاً. الثانية، لأنّ متغيّرات الشرط معرَّضة للإيقاظ الزائف (الاستيقاظ بلا إشعار)، استدعِ wait دائماً بمحمول. شكل المحمول لـ wait ينفِّذ لك داخليّاً منطق «الحلقة حتّى يكون الشرط صادقاً».6

5. انضباط الأقفال ── RAII وscoped_lock

حتّى بعد تقليل الحالة القابلة للتغيير المشتركة، كثيراً ما لا تصل إلى الصفر. استخدم الإقصاء لما بقي مشتركاً، لكن القفل بلا انضباط يخفي التنازع فحسب.

أوّلاً، فكِّر وحدة القفل لا كـ«قطعة كود» بل كـ«بيانات». خصِّص كائناً للحصر (mutex) لكلّ مجموعة بيانات قابلة للتغيير تريد حمايتها (اجعله عضواً private، غير مكشوف للخارج)، وخذ ذلك الكائن نفسه في كلّ موضع يمسّ تلك البيانات ── النسخة المكسورة من جدول التقابل هذا هي ما تكونه معظم أخطاء السباق فعلاً. والشيء الوحيد الذي يجوز فعله وأنت تمسك قفلاً هو قراءة البيانات التي يحميها وكتابتها. إدخال/إخراج الملفّات واستدعاءات الشبكة وردود النداء (الاستدعاء إلى كود خارجيّ) وأنت تمسك قفلاً لا تمدّد زمن الإمساك فحسب ── تفتح مساراً يحاول فيه المستدعى أخذ قفل آخر فينسدّ. جهِّز خارج القفل، وداخل القفل لا تفعل سوى الاستبدال هو الشكل الأساسيّ.

5.1. كتابة lock()/unlock() يدوياً محظورة

الكود الذي يستدعي lock() / unlock() لدى std::mutex مباشرة ينتهي إلى الفشل في تحرير القفل عند الاستثناءات أو العودة المبكِّرة. اترك دائماً اكتساب القفل وتحريره لغلاف RAII.

الغلاف الاستخدام
std::lock_guard يمسك كائناً واحداً للحصر طوال نطاق بالضبط ── الشكل الأكثر أساسيّة
std::scoped_lock ‏(C++17) يكتسب عدّة كائنات حصر معاً. يحلّ مشكلة الترتيب بخوارزميّة تجنّب الانسداد التامّ4
std::unique_lock عندما تريد الفتح وإعادة القفل في المنتصف، أو تحتاج إلى تمريره إلى condition_variable::wait

عندما يكون ثمّة قفلان أو أكثر، تبديل ترتيب الاكتساب حسب مؤشّر الترابط هو نمط الانسداد التامّ الكلاسيكيّ (الانتظار الدائريّ في الشكل 2 يولد هكذا تماماً). الإصلاح وضع قاعدة أنّ «كلّ مؤشّر ترابط يكتسب الأقفال بالترتيب نفسه»، لكن عندما تكتسبها في الوقت نفسه، لدى C++ جواب أفضل: سلِّم عدّة كائنات حصر إلى std::scoped_lock معاً وتضمن المكتبة ترتيب اكتساب خالٍ من الانسداد التامّ.4 في مواقف مثل تحويل بين كائنين تريد فيها «كليهما مقفولاً»، لا تأخذهما فرداً أبداً ── خذهما معاً دائماً.

void Transfer(Account& from, Account& to, int amount)
{
    if (&from == &to) return;                  // لا تفعل شيئاً للحساب نفسه (انظر الملاحظة أدناه)
    std::scoped_lock lock(from.mtx, to.mtx);   // كلاهما معاً؛ تحلّ المكتبة الترتيب
    from.balance -= amount;
    to.balance   += amount;
}

فحص الهويّة في الأعلى ليس زينة. إن مُرِّر Account نفسه كـ from وto، تنتهي إلى تمرير كائن الحصر غير التكراريّ نفسه إلى scoped_lock مرّتين، فيسبِّب تعليقاً أو سلوكاً غير معرَّف. أرفق دائماً استثناء الكائن نفسه بأيّ دالّة «تقفل كليهما».

للبيانات «كثيرة القراءة، نادرة الكتابة»، يمكنك استخدام std::shared_mutex ‏(C++17) كقفل قراءة/كتابة.12 recursive_mutex نوع مصمَّم بحيث «لا ينكسر إعادة اكتساب مؤشّر الترابط نفسه»، لكن تصميماً يحتاج اكتساباً تكراريّاً غالباً علامة على أنّ حدّ مسؤوليّة القفل قد تشوّش ── انظر أوّلاً في مراجعة البنية.

5.2. الموضع الصحيح لـ atomic

يوفِّر std::atomic عمليّات ذرّيّة على متغيّر واحد، زائد ترتيباً قائماً على memory_order.5 يستحقّ مكانه في المواقف نفسها لـ Interlocked في نسخة .NET: تحديث متغيّر واحد، مثل عدّاد أو علم. لا يستطيع الإبقاء على عدّة متغيّرات متّسقة معاً، لذا لذلك تعود إلى std::mutex.

استبدال مؤشّر خام (std::atomic<T*>) له فخّه الخاصّ. حتّى إن كان الاستبدال نفسه ذرّيّاً، لا أحد يحمي عمر الكائن القديم بعد استبداله. إن حمّل قارئ المؤشّر القديم قبيل أن يستبدله الكاتب وdelete، تحصل على وصول إلى ذاكرة محرَّرة. إن أردت تصميم «استبدل وشارك كائناً غير قابل للتغيير» في C++، اختر وسيلة تأتي مقترنة بإدارة العمر ── استبدال std::shared_ptr<const T> محميّ بقفل، أو std::atomic<std::shared_ptr<T>> في C++20.

وكراراً: volatile ليس أداة مزامنة بين مؤشّرات الترابط. البرمجة بلا قفل حيث تحدِّد memory_order بنفسك إقليم خبراء، يتطلّب سبباً مشروعاً لإرخائه عن الافتراضيّ (seq_cst) ووسيلة للتحقّق من أنّك فعلت ذلك صحّة. في تطبيقات الأعمال، إمّا استخدم الافتراضيّ أو اكتبه بـ mutex من الأصل.

6. تصميم كيف تتوقّف ── stop_token والتوقّف التعاونيّ

أوّل سؤال تسأله عند مراجعة تصميم متعدّد مؤشّرات الترابط هو «كيف يتوقّف هذا؟». وليس لـ C++ وسيلة لإيقاف مؤشّر ترابط بأمان من الخارج (مدى خطورة TerminateThread في Win32 مفصَّل في نسخة C). لذا كيف يتوقّف مؤشّر ترابط يجب أن يُبنى بأدوات C++ حول التوقّف التعاونيّ ── جانب التوقيف يصدر طلباً فقط؛ مؤشّر الترابط نفسه يقرِّر متى وكيف ينتهي، في نقطة تترك الأمور مرتَّبة؛ واكتمال الالتحام هو ما يُعدّ «متوقِّفاً».

في C++20، لـ std::jthread آليّة التوقّف مبنيّة. استدعاء request_stop() يرفع طلب التوقّف على std::stop_token الذي تلقّته دالّة مؤشّر الترابط، وتستطلع الحلقة ذلك. يستطيع wait لدى condition_variable_any أخذ stop_token مباشرة، لذا «مؤشّر ترابط ينتظر وصول العمل» يمكن أيضاً إيقاظه فوراً بطلب توقّف (BlockingQueue::Pop في القسم 4 يأخذ هذا الشكل تماماً).

class Worker {
public:
    void Start()
    {
        if (thread_.joinable())                       // ارفض Start مزدوجاً وهو يعمل أصلاً.
            throw std::logic_error("already running"); // إن عيّنّا بدل الرفض، سيبدأ مؤشّر ترابط
                                                       // جديد بالعمل، وبينما ينتظر
                                                       // توقّف القديم، ينتهي عاملان
                                                       // يعملان جنباً إلى جنب
        thread_ = std::jthread([this](std::stop_token st) {
            try {
                while (!st.stop_requested()) {
                    if (auto item = queue_.Pop(st)) {   // يستيقظ على طلب التوقّف أيضاً
                        try {
                            Process(*item, st);          // مرِّر st إلى العمل الذي قد يحجب داخليّاً أيضاً
                        } catch (...) {
                            ReportError(std::current_exception());  // سجِّل فشلاً واحداً وتابع
                        }
                    }
                }
            } catch (...) {
                // خطّ الدفاع الأخير عند حدّ مؤشّر الترابط (يلتقط أيضاً إخفاقات
                // Pop أو النقل). إن أفلت استثناء من هنا، يُسقط std::terminate
                // العمليّة كلّها، لذا تأكّد أنّ ReportError نفسه لا يرمي أبداً
                ReportError(std::current_exception());
            }
        });
    }
    // لا حاجة إلى Stop صريح:
    // مدمِّر Worker -> مدمِّر jthread -> request_stop() + join()
private:
    BlockingQueue<WorkItem> queue_{100};   // سعة محدودة (القسم 4)
    std::jthread thread_;
};
طلب توقّفجانب التوقيف- مدمِّر jthread، أو request_stopstop_tokenحلقة الحساب:تستطلع stop_requested()مؤشّر ترابط منتظر:condition_variable_any::wait(lock, st, pred)يستيقظ فوراًينظِّف ويعود بنفسهيكمل join اللقاءالآن فقط يمكن أن نسمّيه متوقِّفاً

الشكل 4: التوقّف التعاونيّ في C++20. جانب التوقيف يصدر الطلب فقط؛ مؤشّر الترابط نفسه يقرِّر كيف ينتهي؛ اكتمال الالتحام هو ما يُعدّ متوقِّفاً

نقطة أخرى: لا يمكن إسقاط try/catch داخل العامل. ما يجعله jthread آمناً من الاستثناءات هو الالتحام، والالتحام فقط ── إن أفلت استثناء من دالّة مؤشّر الترابط، يُسقط std::terminate العمليّة، كما مع std::thread. قرِّر صراحة، عند حدّ مؤشّر الترابط، كيف تعالج فشل قطعة عمل واحدة (سجِّلها وتابع، أو بلِّغ المالك عبر قناة أخطاء).

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

في البيئات السابقة لـ C++17، تبني الشكل نفسه يدوياً بعلم توقّف std::atomic<bool> زائد notify_all لدى condition_variable. النقطة الأساسيّة هنا طيّ فحص علم التوقّف في محمول متغيّر الشرط ── إن رفعت العلم فقط ونسيت الإشعار، فلن يستيقظ مؤشّر ترابط منتظر أبداً.

7. شواغل خاصّة بـ Windows ── الحدّ مع واجهة Win32

7.1. الاختيار بين المكتبة القياسيّة وكائنات تزامن Win32

توصي وثائق Microsoft بـ std::mutex / std::shared_mutex لكود C++ الذي يقدِّم قابليّة النقل، وتضع مكان كائنات تزامن Win32 عند «الحاجة إلى واجهة انتظار Win32» و«التزامن عبر العمليّات».8

الموقف الاختيار
إقصاء عاديّ داخل العمليّة std::mutex + RAII (الافتراضيّ)
قراءات كثيرة، كتابات نادرة std::shared_mutex
انتظار عدّة كائنات معاً بـ WaitForMultipleObjects كائنات نواة Win32 مثل الأحداث وكائنات الحصر
إقصاء / إشعار عبر العمليّات كائنات حصر وأحداث وإشارات مسمّاة
قفل داخل العمليّة باستخدام واجهة Win32 مباشرة أقفال SRW ‏(CRITICAL_SECTION فقط عندما تلزم التكراريّة)8

للتصميم الملموس لإقصاء الوصول إلى الذاكرة المشتركة عبر العمليّات، انظر «المزالق وأفضل الممارسات عند استخدام shared memory».

7.2. لا تلمس مؤشّرات الترابط داخل DllMain

قيد جدّيّ عند كتابة DLL هو قفل المحمِّل. يُستدعى DllMain وقفل المحمِّل ممسوك، لذا فإنّ عمليّات داخله مثل المزامنة مع مؤشّر ترابط آخر، أو انتظار انتهاء مؤشّر ترابط، أو استدعاء LoadLibrary تسبِّب انسداداً تامّاً أو سلوكاً لا يمكن توقّعه. انقل أيّ تهيئة تبدأ مؤشّرات ترابط أو تلتحم بها إلى خارج DllMain، إلى دالّة تهيئة صريحة.13

7.3. مؤشّر ترابط الواجهة وشقق COM

لتطبيقات سطح مكتب Windows قيد قويّ يسري بغضّ النظر عن اللغة: مؤشّر الترابط الذي أنشأ نافذة أو تحكّماً ── مؤشّر ترابط الواجهة ── وحده يجوز له لمسه. يسلِّم Windows رسائل النافذة إلى طابور رسائل مؤشّر الترابط الذي أنشأ تلك النافذة، لذا يجب تركيز إنشاء الواجهة ومعالجتها على ذلك المؤشّر. عندما تريد تحديث الشاشة من مؤشّر ترابط عامل، لا تلمسه مباشرة ── اطلب من مؤشّر ترابط الواجهة بـ PostMessage (لاتزامنيّ)، وعالجه في إجراء النافذة من جانب مؤشّر ترابط الواجهة. استدعاء الشكل المتزامن، SendMessage، بينما ينتظر مؤشّر ترابط الواجهة انتهاء ذلك العامل يسبِّب انسداداً تامّاً ينتظر فيه كلٌّ الآخر، لذا اجعل الشكل اللاتزامنيّ الافتراضيّ لإشعارات العامل. STA/MTA، حيث يدخل COM، مشروح في «أساسيات COM STA/MTA - نماذج مؤشّرات الترابط وكيفية تجنّب حالات التعليق». لاحظ أيضاً أنّ في كود C++/CLI المترجَم بـ /clr تُحجب ترويسات مؤشّرات الترابط القياسيّة مثل <thread> و<mutex>.14

8. التحقّق والتنقيح ── الاستعداد على افتراض أنّه لن يُعاد إنتاجه

لا يمكنك الاعتماد على الاختبار لإيجاد أخطاء السباق، لأنّ اختباراً عاديّاً يحسب تشغيلاً «صادف أنّه لم يتسابق» نجاحاً. فكِّر الاستعداد في ثلاث طبقات.

خطّ الدفاع الأوّل هو مبادئ التصميم المعروضة حتّى الآن، كما هي تماماً. في المراجعة، أكِّد بجدول: أيّ بيانات قابلة للتغيير مشتركة، أيّ كائن حصر يحمي كلّ قطعة، هل ترتيب اكتساب الأقفال المتعدّدة فريد (أو تُؤخَذ معاً بـ scoped_lock)، وأين مسار التوقّف. تصميم لا تستطيع كتابة هذا الجدول له لم يكتمل، مهما حسن عمله الآن.

ثانياً، اجعل الحالات الشاذّة قابلة للملاحظة بدل إخفائها. أرفق مهلة بـ try_lock_for لدى timed_mutex أو wait_for لدى condition_variable بأيّ قفل ينبغي ألّا يفشل اكتسابه أبداً، وسجِّل المهلة كأنّها شذوذ ── ذلك يحوِّل تعليقاً أبديّاً إلى فشل قابل للكشف. سجِّل دائماً الاستثناءات الملتقطة في try/catch عند حدّ مؤشّر الترابط (القسم 6). عندما يحدث تعليق أو انهيار في الميدان، التقط تفريغاً، افحص مكدّس كلّ مؤشّر ترابط، وانظر إن كانت انتظارات أقفالها تشكِّل دورة. إعداد التفريغات والتسجيل مشروح في «كيف نُبقي سجلّات الأعطال في تطبيقات Windows».

ثالثاً، هزّ الأشياء تحت الحمل. التشغيل طويلاً بتوازي أكثر من عدد الأنوية، وعشوائية ترتيب المعالجة، وحقن تأخيرات اصطناعيّة تقنيّات اختبار إجهاد عمليّة تسهِّل إصابة «جائزة» السباق على آلة التطوير. أخطاء تختفي في بناء التنقيح كثيراً ما تُعاد بسهولة في بناء إطلاق محسَّن تحت حمل ثقيل.

9. الخلاصة ── قائمة C++

ركِّب فحوصات خاصّة بـ C++ فوق المبادئ المشتركة لكلّ لغة: لا تنشئ مؤشّرات الترابط مباشرة، قلِّل الحالة القابلة للتغيير المشتركة، تقابل واحد لواحد بين الأقفال والبيانات، والتوقّف التعاونيّ.

  1. هل يُستخدم std::thread عارياً (هل يمكن أن يكون jthread؟ هل الالتحام مضمون حتّى على مسار الاستثناء؟)
  2. هل لا يُستخدم detach()؟
  3. هل التقاطات اللامدا صريحة، وهل يعيش أيّ متغيّر مُلتقَط بالمرجع أطول من مؤشّر الترابط؟
  4. هل تستطيع القول بثقة أنّه لا يوجد وصول قابل للتغيير مشترك غير متزامن واحد (= سلوك غير معرَّف) في أيّ مكان؟
  5. هل لا توجد كتابة يدويّة لـ lock() / unlock()، وهل تُؤخَذ الأقفال المتعدّدة معاً بـ scoped_lock؟
  6. هل كلّ condition_variable::wait مستخدَم بمحمول؟
  7. هل لا يُستخدم volatile لعلم مشترك (هل هو std::atomic بدل ذلك)؟
  8. هل مسار التوقّف مصمَّم حول stop_token (أو علم ذرّيّ زائد إشعار)، واكتمال الالتحام يؤكِّد اللقاء؟
  9. هل لا يُهمَل future من std::async؟
  10. هل DllMain خالٍ من بدء مؤشّرات الترابط أو مزامنتها أو الالتحام بها؟

C++ متعدّد مؤشّرات الترابط عمل يُنجَز بالمشي بمحاذاة حافّة السلوك غير المعرَّف، لكن اقلبه فيعني أنّ السير الصادق مع RAII واصطلاحات المكتبة القياسيّة يضع مسافة حقيقيّة بينك وبين تلك الحافّة. jthread وscoped_lock وwait بشكل المحمول وatomic ── اختيار الافتراضيّات الصحيحة بين هذه الأدوات هو، في C++، ممارسة مبادئ التصميم نفسها.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع مراجعات تصميم تعدّد مؤشّرات الترابط لتطبيقات ومكتبات DLL المكتوبة بـ C++، والتحقيق في السبب الجذريّ (تحليل التفريغ) لأخطاء حالات السباق مثل «ينهار أحياناً» أو «يسلك سلوكاً خاطئاً في إصدارات الإطلاق فقط»، والاستشارة حول ترحيل كود مؤشّرات الترابط القديم إلى C++ الحديث.

روابط مرجعيّة

  1. ISO C++, C++ Core Guidelines - CP: Concurrency and parallelism. حول وضع CP.1 (افترض أنّ كودك سيعمل كجزء من برنامج متعدّد مؤشّرات الترابط) وCP.2 (تجنّب سباقات البيانات) كقواعد افتتاح فصل التزامن والتوازي؛ وحول انعدام أيّ ضمان بعد وجود سباق بيانات؛ وحول تنظيم قواعد تصميم الكود المتزامن ── نطاق إمساك الأقفال، واستخدام RAII، وما إلى ذلك ── هناك.  2

  2. cppreference.com, std::jthread. حول اختلاف jthread في C++20 عن std::thread في أنّ مدمِّره يستدعي request_stop() تلقائيّاً ثمّ يلتحم؛ وحول إمكان تلقّي std::stop_token وسيطاً أوّل لدالّة مؤشّر الترابط؛ وحول ضمان ذلك لكلٍّ من الالتحام وطلب التوقّف حتّى عند رمي استثناء.  2

  3. Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. حول دعم P0660R10 ‏(<stop_token> وjthread) وP1135R6 (مكتبة تزامن C++20) من Visual Studio 2019 16.9؛ وحول حالة الدعم حسب الإصدار لميزات مكتبة C++ القياسيّة.  2 3

  4. Microsoft Learn, scoped_lock Class. حول اكتساب scoped_lock في C++17 كائن حصر واحد أو أكثر عند البناء وتحريرها في المدمِّر؛ وحول اكتساب عدّة كائنات حصر، عند تمريرها معاً، بخوارزميّة تجنّب انسداد تامّ مكافئة لـ std::lock؛ وحول التحرير الموثوق حتّى إن رُمي استثناء؛ وحول كون lock_guard/unique_lock خياراً أيضاً عندما يتدخّل كائن حصر واحد فقط.  2 3

  5. Microsoft Learn, <atomic>. حول كون العمليّات الذرّيّة غير قابلة للانقسام، فلا تستطيع مؤشّرات الترابط الأخرى ملاحظة إلّا الحالة قبل العمليّة أو بعدها؛ وحول إقامة، بناء على وسيط memory_order، متطلّبات ترتيب على رؤية العمليّات الذرّيّة الأخرى، وقمع تحسينات المترجم التي تنتهكها؛ وحول كون atomic_flag حرّاً من القفل دائماً؛ وحول حجب هذه الترويسة تحت /clr:pure.  2

  6. Microsoft Learn, <condition_variable>. حول احتياج انتظار متغيّر شرط إلى كائن حصر، مع تحرير القفل طوال مدّة الانتظار؛ وحول وجود الإيقاظ الزائف ── الاستيقاظ بلا إشعار ── لذا ينبغي لجانب الانتظار إعادة فحص الشرط صراحة عند العودة، وشكل المحمول wait(lock, pred) ينفِّذ تلك الحلقة عنك؛ وحول إمكان جمع condition_variable_any مع أيّ نوع كائن حصر.  2

  7. Microsoft Learn, <future>. حول كون مدمِّري future وshared_future لا يحجبان كقاعدة، مع الاستثناء الوحيد أنّ future (أو آخر shared_future) المرتبط بمهمّة أُطلقت بـ std::async يحجب حتّى تصير الحالة المشتركة جاهزة إن جرى مدمِّره والمهمّة ما تزال غير مكتملة ── سلوك منصوص عليه صراحة في المعيار.  2

  8. Microsoft Learn, About Synchronization. حول إرشاد اختيار بدائيّات تزامن Win32: يُوصى بـ std::mutex / std::shared_mutex وRAII لكود C++ الذي يقدِّم قابليّة النقل؛ تُستخدم كائنات تزامن Win32 عند الحاجة إلى واجهة انتظار Win32 أو تزامن عبر العمليّات؛ الافتراضيّ للكود الجديد داخل العمليّة قفل SRW، مع حفظ CRITICAL_SECTION لحين الحاجة إلى الاكتساب التكراريّ؛ واستخدام Mutex للمزامنة داخل العمليّة «خطأ شائع» لأنّه يتضمّن دائماً انتقالاً إلى النواة.  2 3

  9. cppreference.com, std::thread::~thread. حول استدعاء مدمِّر std::thread لـ std::terminate إن استُدعي ومؤشّر الترابط ما يزال قابلاً للالتحام (لم يُلتحَم به ولم يُفصَل) ── أي أنّ قرار الالتحام أو الفصل يجب أن يُحسَم قبل تدمير كائن مؤشّر الترابط، بلا استثناء. 

  10. Microsoft Learn, Best Practices in the Parallel Patterns Library. حول التعبير عن التوازي مثاليّاً في أعلى مستوى ممكن (الحلقة الخارجيّة)؛ وحول إمكان تفوّق كلفة جدولة fork/join على مكاسب التنفيذ المتوازي في الحلقات المتوازية التي يكون عمل كلّ تكرار فيها صغيراً أو غير متوازن؛ وحول اشتداد ذلك الميل مع ازدياد عدد المعالجات. 

  11. Microsoft Learn, Microsoft C/C++ language conformance by Visual Studio version. حول اكتمال مكتبة خوارزميّات C++17 المتوازية، بينما «اكتمال» لا يعني موازاة كلّ خوارزميّة في كلّ حالة؛ وحول سياسة التنفيذ بموازاة أهمّ الخوارزميّات مع توفير توقيعات سياسة التنفيذ لتلك التي لا تُوازى. 

  12. Microsoft Learn, C++ standard library header files. حول ترتيب الترويسات القياسيّة المتعلّقة بتعدّد مؤشّرات الترابط كـ <atomic> ‏(C++11)، و<mutex> ‏(C++11)، و<shared_mutex> ‏(C++14)، و<condition_variable> ‏(C++11)، و<future> ‏(C++11)، و<stop_token> / <semaphore> / <latch> / <barrier> ‏(C++20)، و<thread> ‏(C++11). 

  13. Microsoft Learn, Dynamic-Link Library Best Practices. حول استدعاء DllMain وقفل المحمِّل ممسوك، بما يفرض قيوداً جدّيّة على الواجهات التي يجوز استدعاؤها؛ وحول إمكان انسداد المزامنة مع مؤشّر ترابط آخر داخل DllMain؛ وحول كون استدعاء LoadLibrary أو انتظار انتهاء مؤشّر ترابط أفعالاً محظورة نموذجيّة؛ وحول تأخير التهيئة مثاليّاً أبعد ما يمكن ونقلها خارج DllMain؛ وحول تعريف تراتب أقفال يوضع فيه قفل المحمِّل في القمّة. 

  14. Microsoft Learn, <thread>. حول تعريف ترويسة <thread> لصنف thread ودوالّ مساعدة مثل sleep_for؛ وحول حجب هذه الترويسة في الكود المترجَم بـ /clr؛ وحول إمكان تحديد دعم مؤشّرات الترابط من عدمه بماكرو STDCPP_THREADS

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

كيف أختار بين std::mutex وCRITICAL_SECTION / أقفال SRW في Win32؟
لكود C++ العاديّ الذي يقدِّم قابليّة النقل، فإنّ std::mutex / std::shared_mutex مع أغلفة RAII ‏(lock_guard / scoped_lock) هما الخيار الأوّل. لجأ إلى كائنات تزامن Win32 عندما تحتاج إلى جمعها مع واجهة انتظار Win32 مثل WaitForMultipleObjects، أو عندما تحتاج تزامناً عبر العمليّات عبر كائن مسمًّى. إن استخدمت واجهة Win32 مباشرة داخل العمليّة، فالافتراضيّ للكود الجديد هو قفل SRW، ولا تستخدم CRITICAL_SECTION إلّا عندما يحتاج مؤشّر الترابط نفسه إلى اكتسابه تكراريّاً. استخدام Mutex من Win32 للإقصاء داخل العمليّة خطأ كلاسيكيّ، لأنّه يتضمّن دائماً انتقالاً إلى النواة فيكون بطيئاً تبعاً لذلك.
هل يجوز استخدام detach() الخاصّ بـ std::thread؟
كقاعدة، تجنّبه. مؤشّر ترابط فُصل يفقد أيّ وسيلة للالتحام (join)، وتفقد التحكّم في ما إذا كان ما يزال يعمل عند خروج العمليّة. حادث كلاسيكيّ: مؤشّر ترابط مفصول يواصل العمل بعد تدمير المتغيّرات الساكنة أو الكومة، فيسبِّب انهياراً عند الإغلاق. القدرة على انتظار انتهاء مؤشّر الترابط شرط أساسيّ لتصميم مؤشّرات الترابط، لذا استخدم jthread (الذي يلتحم تلقائيّاً)، أو إن استخدمت thread فابنِ الكود ليلتحم دائماً قبل نهاية النطاق. لا يُباح detach إلّا في الموقف الضيّق الذي يمكن فيه لمؤشّر الترابط أن يشارك مصير العمليّة وتضمن أنّه لا يمسّ الحالة المشتركة أبداً.
هل يمكن استخدام volatile للمزامنة بين مؤشّرات الترابط في C++؟
لا. volatile في C++ مؤهِّل للقراءات والكتابات التي لا تريد للمترجم أن يحسِّنها بعيداً ── إدخال/إخراج مُسقَط على الذاكرة مثلاً ── ولا يضمن الرؤية ولا الترتيب بين مؤشّرات الترابط. إن وصل عدّة مؤشّرات ترابط إلى المتغيّر نفسه بلا مزامنة، فذلك سباق بيانات، وهو سلوك غير معرَّف. استخدم std::atomic للأعلام والعدّادات المشتركة بين مؤشّرات الترابط، وstd::mutex عندما تحتاج إلى حماية عدّة متغيّرات معاً. يوفِّر std::atomic كلاً من ذرّيّة العمليّة والترتيب القائم على memory_order.
يبدو std::async مريحاً، فهل فيه فخاخ؟
أكبر فخّ هو مدمِّر future. فإنّ future (أو آخر shared_future) المرتبط بمهمّة أُطلقت بـ std::async يحجب حتّى الاكتمال إن جرى مدمِّره والمهمّة ما تزال غير مكتملة. إن أهملت future المُرجَع دون الإمساك به، صار ذلك معادلاً للتنفيذ المتزامن في المكان ── حادث قصدت فيه اللاتزامن فانتهيت إلى التسلسل. كذلك، إن لم تحدِّد سياسة إطلاق، فإنّ ما إذا كان العمل يجري فعلاً على مؤشّر ترابط منفصل يُترَك لتقدير التنفيذ. إن استخدمته، أدِر عمر future صراحةً، وحدِّد std::launch::async حيثما تحتاج إلى ضمان التنفيذ المتزامن.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة