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

تحدث حالة العرق هذه لأنها ليست مجرد "إضافة واحد إلى المتغير" بمعنى مجرد ؛ تقوم وحدة المعالجة المركزية بتحميل قيمة
number
في السجل ، مضيفة واحدًا إلى تلك القيمة ، ثم تخزين النتيجة كقيمة جديدة للمتغير. لا يعرف أنه ، في هذه الأثناء ، كان هناك مؤشر ترابط آخر يحاول أيضًا أن يفعل الشيء نفسه تمامًا ، وقام بتحميل قيمة غير صحيحة قريبًا من
رقم
يتعارض الخيطون ، و في نهاية الحلقة ،
رقمقد لا يساوي 100.
توفر
NET ميزة للمساعدة في إدارة هذا: الكلمة الرئيسية
lock. لا يمنع هذا من إجراء التغييرات بشكل مباشر ، ولكنه يساعد في إدارة التزامن من خلال السماح فقط لخيط واحد في كل مرة بالحصول على القفل. إذا حاول مؤشر ترابط آخر إدخال عبارة قفل أثناء معالجة مؤشر ترابط آخر ، فسوف ينتظر ما يصل إلى 300 مللي ثانية قبل المتابعة.
أنت قادر فقط على قفل أنواع المراجع ، لذا فإن النمط الشائع هو إنشاء كائن قفل مسبقًا ، واستخدامه كبديل لقفل نوع القيمة.

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

هناك أيضًا
Increment
و
Decrement
لـ
++
و
- -
عمليات ، مما سيوفر لك ضغطتين قويتين على المفاتيح. إنهم يلتفون حرفياً
أضف (عدد المرجع ، 1)تحت الغطاء ، لذلك لا يوجد تسريع محدد لاستخدامهم.
يمكنك أيضًا استخدام Exchange ، وهي طريقة عامة ستحدد متغيرًا مساويًا للقيمة التي تم تمريرها إليه. على الرغم من ذلك ، يجب أن تكون حذرًا مع هذا - إذا كنت تقوم بتعيينه على قيمة قمت بحسابها باستخدام القيمة الأصلية ، فهذا ليس مؤشر ترابط آمن ، حيث كان من الممكن تعديل القيمة القديمة قبل تشغيل Interlocked. Exchange.

ستتحقق CompareExchange من قيمتين للمساواة ، وتستبدل القيمة إذا كانت متساوية.
استخدم مجموعات سلاسل الرسائل الآمنة
المجموعات الافتراضية في
System. Collections. Generic
يمكن استخدامها مع تعدد مؤشرات الترابط ، لكنها ليست آمنة تمامًا لسلسلة المحادثات. توفر Microsoft عمليات تنفيذ خيطية آمنة لبعض المجموعات في
System. Collections. Current.
من بينها
ConcurrentBag
، ومجموعة عامة غير مرتبة ، و
ConcurrentDictionary ،
قاموس آمن للخيط. هناك أيضًا قوائم انتظار ومكدسات متزامنة ، و
OrderablePartitioner، والتي يمكنها تقسيم مصادر البيانات القابلة للطلب مثل القوائم إلى أقسام منفصلة لكل مؤشر ترابط.
انظر إلى الحلقات المتوازية
في كثير من الأحيان ، أسهل مكان لتعدد مؤشرات الترابط هو الحلقات الكبيرة والمكلفة. إذا كان بإمكانك تنفيذ عدة خيارات بالتوازي ، يمكنك الحصول على تسريع كبير في وقت التشغيل الكلي.
أفضل طريقة للتعامل مع هذا الأمر مع
System. Threading. Tasks. Parallel
. توفر هذه الفئة بدائل لـ
للحلقات
و
foreachالتي تنفذ أجسام الحلقة على سلاسل منفصلة. إنه سهل الاستخدام ، على الرغم من أنه يتطلب بنية مختلفة قليلاً:

من الواضح أن المهم هنا هو أنك تحتاج إلى التأكد من أن
DoSomething ()
هو مؤشر ترابط آمن ، ولا يتداخل مع أي متغيرات مشتركة.ومع ذلك ، هذا ليس دائمًا سهلاً مثل مجرد استبدال الحلقة بحلقة متوازية ، وفي كثير من الحالات يجب عليك
lockكائنات مشتركة لإجراء تغييرات.
للتخفيف من بعض المشاكل مع الجمود ،
متوازي ، لـ
و
متوازي ، لكليوفر ميزات إضافية للتعامل مع الحالة. في الأساس ، لن يتم تشغيل كل تكرار على سلسلة منفصلة - إذا كان لديك 1000 عنصر ، فلن يتم إنشاء 1000 موضوع ؛ ستقوم بعمل أكبر عدد ممكن من سلاسل الرسائل التي يمكن لوحدة المعالجة المركزية الخاصة بك التعامل معها ، وتشغيل العديد من التكرارات لكل سلسلة محادثات. هذا يعني أنك إذا كنت تحسب إجماليًا ، فلن تحتاج إلى قفل كل تكرار. يمكنك ببساطة تمرير متغير إجمالي فرعي ، وفي النهاية ، قم بإغلاق الكائن وإجراء التغييرات مرة واحدة. هذا يقلل بشكل كبير من النفقات العامة على القوائم الكبيرة جدا.
دعونا نلقي نظرة على مثال. تأخذ الشفرة التالية قائمة كبيرة من الكائنات ، وتحتاج إلى إجراء تسلسل لكل عنصر على حدة إلى JSON ، وينتهي الأمر بـ
Listمن جميع الكائنات.تسلسل JSON عملية بطيئة جدًا ، لذا فإن تقسيم كل عنصر على خيوط متعددة يعد تسريعًا كبيرًا.

هناك مجموعة من الحجج ، والكثير لتفريغها هنا:
- تأخذ الوسيطة الأولى IEnumerable ، والتي تحدد البيانات التي يتم تكرارها. هذه حلقة ForEach ، لكن نفس المفهوم يعمل مع حلقات For الأساسية.
- الإجراء الأول يقوم بتهيئة متغير المجموع الفرعي المحلي. ستتم مشاركة هذا المتغير عبر كل تكرار للحلقة ، ولكن فقط داخل نفس الخيط. المواضيع الأخرى سيكون لها المجاميع الفرعية الخاصة بها. هنا ، نحن بصدد تهيئته إلى قائمة فارغة. إذا كنت تحسب إجماليًا رقميًا ، فيمكنك
إرجاع 0هنا.
- الإجراء الثاني هو جسم الحلقة الرئيسي. الوسيطة الأولى هي العنصر الحالي (أو الفهرس في حلقة For) ، والثانية هي كائن ParallelLoopState يمكنك استخدامه لاستدعاء
. Break ()، والأخير هو المجموع الفرعي متغير.
في هذه الحلقة ، يمكنك العمل على العنصر ، وتعديل المجموع الفرعي. ستحل القيمة التي ترجعها محل الإجمالي الفرعي للحلقة التالية. في هذه الحالة ، نقوم بتسلسل العنصر إلى سلسلة ، ثم نضيف السلسلة إلى المجموع الفرعي ، وهو عبارة عن قائمة
- أخيرًا ، يأخذ الإجراء الأخير "النتيجة" الإجمالي الفرعي بعد انتهاء جميع عمليات التنفيذ ، مما يسمح لك بقفل وتعديل مورد بناءً على الإجمالي النهائي. يتم تشغيل هذا الإجراء مرة واحدة ، في النهاية ، ولكنه لا يزال يعمل على سلسلة منفصلة ، لذلك ستحتاج إلى قفل أو استخدام أساليب Interlocked لتعديل الموارد. هنا ، نسمي
AddRange ()لإلحاق قائمة المجموع الفرعي بالقائمة النهائية.
الوحدة تعدد
ملاحظة أخيرة - إذا كنت تستخدم محرك لعبة Unity ، فستحتاج إلى توخي الحذر عند تعدد مؤشرات الترابط. لا يمكنك استدعاء أي Unity APIs ، وإلا ستتعطل اللعبة. من الممكن استخدامه باعتدال عن طريق إجراء عمليات واجهة برمجة التطبيقات على مؤشر الترابط الرئيسي والتبديل ذهابًا وإيابًا كلما احتجت إلى موازاة شيء ما.
في الغالب ، هذا ينطبق على العمليات التي تتفاعل مع المشهد أو محرك الفيزياء. لا تتأثر الرياضيات في Vector3 ، ولك مطلق الحرية في استخدامها من سلسلة رسائل منفصلة بدون مشاكل. أنت حر أيضًا في تعديل الحقول وخصائص العناصر الخاصة بك ، بشرط ألا تستدعي أي عمليات Unity تحت الغطاء.