هل سيتسبب async(launch:: async) في C++ 11 في جعل مجموعات ترابط متقادمة لتجنب إنشاء سلاسل محادثات مكلفة؟



multithreading asynchronous (1)

وترتبط بشكل فضفاض بهذا السؤال: هل يتم تجميع STD :: thread في C ++ 11؟ . على الرغم من أن السؤال يختلف ، فإن النية هي نفسها:

السؤال الأول: هل لا يزال من المنطقي استخدام تجمعات مؤشر ترابط (أو مكتبة الطرف الثالث) الخاصة بك لتجنب إنشاء مؤشر ترابط مكلفة؟

كان الاستنتاج في السؤال الآخر أنه لا يمكنك الاعتماد على std::thread ليتم تجميعها (ربما أو قد لا يكون). ومع ذلك ، يبدو أن std::async(launch::async) تتمتع بفرصة أكبر بكثير لتجميعها.

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

السؤال 2: هذا ما أفكر به ، لكن ليس لدي أي حقائق لإثبات ذلك. قد أكون مخطئا جدا. هل هو تخمين متعلم؟

وأخيرًا ، لقد قدمت هنا بعضًا من نماذج التعليمة البرمجية التي تُظهر أولاً كيف أعتقد أنه يمكن التعبير عن إنشاء سلسلة async(launch::async) خلال async(launch::async) :

مثال 1:

 thread t([]{ f(); });
 // ...
 t.join();

يصبح

 auto future = async(launch::async, []{ f(); });
 // ...
 future.wait();

مثال 2: النار وننسى موضوع

 thread([]{ f(); }).detach();

يصبح

 // a bit clumsy...
 auto dummy = async(launch::async, []{ f(); });

 // ... but I hope soon it can be simplified to
 async(launch::async, []{ f(); });

Questin 3: هل تفضل الإصدارات async إلى إصدارات سلاسل المحادثات؟

الباقي لم يعد جزء من السؤال ، ولكن فقط للتوضيح:

لماذا يجب تعيين قيمة الإرجاع للمتغير الوهمي؟

لسوء الحظ ، يفرض معيار C ++ 11 الحالي الذي قمت بالتقاط قيمة الإرجاع من std::async ، وإلا يتم تنفيذ destructor ، الذي يمنع حتى ينتهي الإجراء. يعتبر من قبل البعض خطأ في المعيار (على سبيل المثال ، عن طريق عشبة Sutter).

يوضح هذا المثال من cppreference.com بشكل cppreference.com :

{
  std::async(std::launch::async, []{ f(); });
  std::async(std::launch::async, []{ g(); });  // does not run until f() completes
}

توضيح آخر:

أعلم أن مجمعات الخيوط قد تكون لها استخدامات مشروعة أخرى ولكن في هذا السؤال ، أنا مهتم فقط بجانب تجنب تكاليف إنشاء سلاسل المحادثات المكلفة .

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

قد تكون متغيرات مؤشر الترابط المحلي أيضًا وسيطة لمجمعات ترابط الخاصة بك ، ولكن لست متأكداً ما إذا كان المتغير في الواقع العملي:

  • إنشاء مؤشر ترابط جديد مع std::thread يبدأ دون متغيرات مؤشر الترابط المحلي. ربما هذا ليس ما تريد.
  • في مؤشرات الترابط التي async من async ، فإنه غير واضح إلى حد ما بالنسبة لي لأنه قد تم إعادة استخدام مؤشر الترابط. من فهمي ، لا يتم ضمان إعادة ضبط متغيرات الصفحات المحلية ، ولكن قد أكون مخطئًا.
  • إن استخدام تجمعات مؤشر الترابط (ذات الحجم الثابت) الخاصة بك ، من ناحية أخرى ، يمنحك التحكم الكامل إذا كنت تحتاج إليها بالفعل.

السؤال الأول :

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

من الواضح تماماً أن مكتبة C ++ القياسية التي تأتي مع g ++ لا تحتوي على تجمعات مؤشر ترابط. لكن يمكنني بالتأكيد رؤية قضية لهم. حتى مع وجود مقدار الاضطرار إلى دفع المكالمة من خلال نوع معين من طوابير الترابط ، من المحتمل أن يكون أرخص من بدء سلسلة جديدة. والمعيار يسمح بذلك.

IMHO ، يجب على الناس نواة لينكس العمل على خلق خلق سلسلة أرخص مما هو عليه حاليا. ولكن ، يجب على مكتبة C ++ القياسية أيضًا مراعاة استخدام تجمع لتنفيذ launch::async | launch::deferred launch::async | launch::deferred .

و OP صحيح ، باستخدام ::std::thread ترابط بالطبع يفرض إنشاء مؤشر ترابط جديد بدلاً من استخدام واحد من تجمع. So ::std::async(::std::launch::async, ...) المفضل.

السؤال الثاني :

نعم ، أساسا هذا 'ضمنا' يطلق موضوع. لكن في الحقيقة ، ما زال من الواضح تمامًا ما يحدث. لذا لا أعتقد أن الكلمة ضمنيًا كلمة جيدة بشكل خاص.

أنا أيضا غير مقتنع بأن إجبارك على انتظار العودة قبل التدمير هو بالضرورة خطأ. لا أعلم أنه يجب عليك استخدام استدعاء async لإنشاء سلاسل عمليات "daemon" غير متوقع إرجاعها. وإذا كان من المتوقع أن يعودوا ، فلا بأس من تجاهل الاستثناءات.

السؤال 3 :

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

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

ولكن في الحقيقة ، يعتمد الأمر على ما تفعله بالضبط.

تجربة أداء

لذا ، فقد اختبرت أداء الطرق المختلفة للاتصال بالأشياء ووصلت إلى هذه الأرقام على جهاز 2 CPU VM يعمل فيدورا 25 تم جمعه مع g ++ 6.3.1:

Do nothing calls per second: 30326536 Empty calls per second: 29348752 New thread calls per second: 15322 Async launch calls per second: 14779 Worker thread calls per second: 1357391

وبلدي ، على جهاز MacBook Retina مع Apple LLVM version 8.0.0 (clang-800.0.42.1) تحت OSX 10.12.3 ، أحصل على هذا:

Do nothing calls per second: 20303610 Empty calls per second: 20222685 New thread calls per second: 40539 Async launch calls per second: 45165 Worker thread calls per second: 2662493

بالنسبة لمؤشر ترابط العاملين ، بدأت في إنشاء سلسلة رسائل ، ثم استخدمت قائمة انتظار بدون قفل لإرسال الطلبات إلى سلسلة محادثات أخرى ، ثم انتظر رد "تم الانتهاء" ليتم إرسالها مرة أخرى.

"لا تفعل شيئًا" هو فقط اختبار الحمل الزائد.

من الواضح أن النفقات العامة لإطلاق خيط ضخم. وحتى مؤشر ترابط العامل بقائمة انتظار ترابط بين العناصر يؤدي إلى إبطاء الأشياء بواسطة عامل 20 أو نحو ذلك على Fedora 25 في VM ، ومن حوالي 8 على OS X الأصلي.

أقوم بإنشاء مشروع Bitbucket يحمل الرمز الذي استخدمته لاختبار الأداء. يمكن العثور عليه هنا: https://bitbucket.org/omnifarious/launch_thread_performance





threadpool