الغرض من البرمجة المتزامنة هو جعل البرنامج يعمل بشكل أسرع ، ولكن استخدام التزامن قد لا يجعل البرنامج يعمل بشكل أسرع بالضرورة. لا يمكن أن تنعكس مزايا البرمجة المتزامنة إلا عندما يصل عدد البرامج المتزامنة إلى ترتيب معين. لذلك ، من المجدي فقط التحدث عن البرمجة المتزامنة عندما يكون هناك تزامن كبير. على الرغم من عدم تطوير أي برامج ذات حجم متزامن عالي حتى الآن ، إلا أن التعلم التزامن هو فهم بعض البنى الموزعة بشكل أفضل. ثم عندما لا يكون حجم التزامن للبرنامج مرتفعًا ، مثل برنامج واحد متخلف ، فإن كفاءة التنفيذ لخيط واحد أعلى من برنامج متعدد الخيوط. لماذا هذا؟ يجب على أولئك الذين يعرفون نظام التشغيل أن يعلموا أن وحدة المعالجة المركزية تنفذ متعدد الخيوط عن طريق تخصيص شرائح زمنية لكل موضوع. وبهذه الطريقة ، عندما تتحول وحدة المعالجة المركزية من مهمة إلى أخرى ، سيتم حفظ حالة المهمة السابقة. عند تنفيذ المهمة ، ستستمر وحدة المعالجة المركزية في تنفيذ حالة المهمة السابقة. وتسمى هذه العملية تبديل السياق.
في Java Multithreading ، تلعب الكلمة الرئيسية المتزامنة للكلمة الرئيسية المتطايرة دورًا مهمًا. يمكنهم جميع تنفيذ مزامنة الخيوط ، ولكن كيف يتم تنفيذها في الأسفل؟
متقلب
يمكن أن يضمن التقلب فقط رؤية المتغيرات لكل مؤشر ترابط ، ولكن لا يمكن أن تضمن الذرة. لن أقول الكثير عن كيفية استخدام لغة جافا المتطايرة. اقتراحي هو استخدامه في أي موقف آخر باستثناء مكتبة الفصل في حزمة java.util.concurrent.atomic. انظر هذا المقال لمزيد من التفسيرات.
مقدمة
انظر الرمز التالي
package org.go ؛ class public go {folatile int i = 0 ؛ private void inc () {i ++ ؛ } public static void main (string [] args) {go go = new go () ؛ لـ (int i = 0 ؛ i <10 ؛ i ++) {new thread (() -> {for (int j = 0 ؛ j <1000 ؛ j ++) go.inc () ؛}). start () ؛ } بينما (thread.activecount ()> 1) {thread.yield () ؛ } system.out.println (go.i) ؛ }} تختلف نتيجة كل تنفيذ للرمز أعلاه ، ودائمًا ما يكون رقم الإخراج أقل من 10000. هذا لأنه عند أداء INC () ، فإن I ++ ليس عملية ذرية. ربما يقترح بعض الأشخاص استخدام المزامنة لمزامنة INC () ، أو استخدام القفل ضمن الحزمة java.util.concurrent.locks للتحكم في مزامنة مؤشر الترابط. لكنها ليست جيدة مثل الحلول التالية:
package org.go ؛ import java.util.concurrent.atomic.atomicinteger ؛ class public go {AtomicInteger i = new AtomicInteger (0) ؛ private void inc () {i.getandincrement () ؛ } public static void main (string [] args) {go go = new go () ؛ لـ (int i = 0 ؛ i <10 ؛ i ++) {new thread (() -> {for (int j = 0 ؛ j <1000 ؛ j ++) go.inc () ؛}). start () ؛ } بينما (thread.activecount ()> 1) {thread.yield () ؛ } system.out.println (go.i) ؛ }} في هذا الوقت ، إذا لم تفهم تنفيذ الذرة ، فستشك بالتأكيد في أنه قد يتم تنفيذ Atomicinteger الأساسي باستخدام الأقفال ، لذلك قد لا يكون فعالًا. إذن ما هو بالضبط ، دعنا نلقي نظرة.
التنفيذ الداخلي للفصول الذرية
سواء كان ذلك هو concurrentLinkedQueue atomicinteger أو concurrentLinkedQueue.
Sun.Misc.Misc.Miscafe غير آمن ؛ ، هذا الفصل عبارة عن تغليف Java of Sun :: Misc :: Unfafe الذي ينفذ الدلالات الذرية. أريد أن أرى التنفيذ الأساسي. صادف أن لدي رمز المصدر لـ GCC4.8 في متناول اليد. بالمقارنة مع المسار المحلي ، من المريح للغاية العثور على المسار إلى جيثب. انظر هنا.
خذ مثال التنفيذ في واجهة getandincrement ()
Atomicinteger.java
Finial Static Final Unfafe غير آمن = unfafe.getunsafe () ؛ public final int getAndIncrement () {for (؛؛) {int current = get () ؛ int التالي = الحالي + 1 ؛ if (CompareAndSet (الحالي ، التالي)) إرجاع التيار ؛ }} Public Final Boolean CompareAndset (int region ، int update) {return unsafe.compareandswapint (this ، valueffset ، توقع ، تحديث) ؛ } انتبه إلى هذا للحصول على حلقة ، لن يعود إلا إذا نجحت ConperteAndset. خلاف ذلك ، سوف يقارن دائما.
يسمى تنفيذ المقارنة. هنا ، لاحظت أن تنفيذ Oracle JDK مختلف قليلاً. إذا نظرت إلى SRC تحت JDK ، فيمكنك أن ترى أن Oracle JDK يستدعي GetAndincrement () ، لكنني أعتقد أنه عندما تنفذ Oracle JDK. جافا ، يجب أن تستدعي فقط المقارنة ، لأن المقارنة يمكن أن تنفذ العمليات الذرية للزيادة والتقليل وتعيين القيم.
غير آمن
Public Native Boolean CompareAndswapint (Object OBJ ، إزاحة طويلة ، INT توقع ، int update) ؛
تنفيذ C ++ دعا من خلال JNI.
natunsafe.cc
jBooleansun :: Misc :: Unfafe :: CompareAndSwapint (Jobject OBJ ، Jlong Offset ، jint توقع ، jint update) {jint *addr = (jint *) ((char *) obj + offset) ؛ Return CompareAndswap (Addr ، توقع ، تحديث) ؛} ثابتة ثابتة boolcompareanswap (flatile jint *addr ، jint old ، jint new_val) {jboolean result = false ؛ قفل الدوران if ((result = ( *addr == old))) *addr = new_val ؛ نتيجة العودة ؛} غير آمن :: CompareAndswapint يستدعي الوظيفة الثابتة المقارنة. يستخدم CompareAndswap Spinlock كقفل. يتمتع Spinlock هنا بمعنى LockGuard ، الذي تم قفله أثناء البناء ويطلقه أثناء التدمير.
نحن بحاجة إلى التركيز على الدوران. فيما يلي ضمان أن Spinlock هو تطبيق حقيقي للعمليات الذرية قبل إصداره.
ما هو spinlock
Spinlock ، نوع من مشغول في انتظار الحصول على قفل المورد. على عكس حظر MUTEX الخيط الحالي وإطلاق موارد وحدة المعالجة المركزية لانتظار الموارد المطلوبة ، لن يدخل Spinlock عملية التعليق ، في انتظار الوفاء بالظروف ، وإعادة تجميع وحدة المعالجة المركزية. هذا يعني أن Spinlock أفضل من Mutex فقط إذا كانت تكلفة انتظار القفل أقل من تكلفة مفتاح سياق تنفيذ مؤشر الترابط.
natunsafe.cc
Class Spinlock {Static Folatile OBJ_ADDR_T LOCK ؛ public: spinlock () {بينما (! compare_and_swap (& lock ، 0 ، 1)) _JV_THEREADIEFRY () ؛ } ~ spinlock () {refere_set (& lock ، 0) ؛ }} ؛ استخدام متغير ثابت ثابت متقلبة OBJ_ADDR_T LOCK ؛ كبخل العلم ، يتم تنفيذ الحارس من خلال C ++ RAII ، وبالتالي فإن ما يسمى القفل هو في الواقع قفل العضو الثابت OBJ_ADDR_T. لا يمكن أن يضمن التقلب في C ++ التزامن. ما هو مضمون هو care_and_swap المسمى في المُنشئ وقفل متغير ثابت. عندما يكون متغير القفل هذا 1 ، تحتاج إلى الانتظار ؛ عندما يكون 0 ، تقوم بتعيينه على 1 من خلال العملية الذرية ، مما يشير إلى أنك قد حصلت على القفل.
إنه حقًا حادث لاستخدام متغير ثابت هنا ، مما يعني أن جميع الهياكل الخالية من القفل تشترك في نفس المتغير (في الواقع size_t) لتمييز ما إذا كان يجب إضافة قفل. عندما يتم ضبط هذا المتغير على 1 ، يجب انتظار Spinlock الأخرى. لماذا لا تضيف متغيرًا خاصًا متغيرًا OBJ_ADDR_T في Sun :: MISC :: Unfafe وتمريره إلى Spinlock كمعلمة مُنشأة؟ هذا يعادل مشاركة بت العلم لكل غير آمن. هل سيكون التأثير أفضل؟
_jv_threadyield في الملف التالي ، يتم التخلي عن مورد وحدة المعالجة المركزية من خلال SCHED_YIELD SCHED_YIELD (MAN 2 SCHED_YIELD). يتم تعريف الماكرو have_sched_yield في التكوين ، مما يعني أنه إذا كان التعريف غير محدد أثناء التجميع ، يطلق على قفل الدوران الحقيقي.
posix-threads.h
inline void_jv_threadyield (void) {#ifdef hy_sched_yield Sched_yield () ؛#endif / * have_sched_yield * /} يحتوي هذا القفل. على تطبيقات مختلفة على منصات مختلفة. نأخذ منصة IA64 (Intel AMD X64) كمثال. يمكن رؤية التطبيقات الأخرى هنا.
IA64/locks.h
typedef size_t obj_addr_t ؛ inline static boolcompare_and_swap (folatile obj_addr_t *addr ، obj_addr_t old ، obj_addr_t new_val) {return __sync_bool_compare_and_swap (addr ، new_val) ؛ OBJ_ADDR_T *ADDR ، OBJ_ADDR_T NEW_VAL) {__asm__ __volatile __ ("" :: "Memory") ؛ *(addr) = new_val ؛}__sync_bool_compare_and_swap هي وظيفة GCC مدمجة ، وتعليمات التجميع "الذاكرة" تكمل حاجز الذاكرة.
باختصار ، يضمن الجهاز مزامنة وحدة المعالجة المركزية متعددة النواة ، وتنفيذ غير آمن فعال قدر الإمكان. يعد GCC-Java فعالًا للغاية ، وأعتقد أن Oracle و OpenJDK لن يكونوا أسوأ.
العمليات الذرية والعمليات الذرية المدمجة في مجلس التعاون الخليجي
العملية الذرية
تعبيرات Java وتعبيرات C ++ ليست عمليات ذرية ، مما يعني أنك في الرمز:
// لنفترض أنني متغير I ++ مشترك بين المواضيع ؛
في بيئة متعددة مؤشرات الترابط ، أنا الوصول غير ذبي ، ويتم تقسيمه فعليًا إلى المعاملات الثلاثة التالية:
يغير المترجم توقيت التنفيذ ، لذلك قد لا يتم توقع نتيجة التنفيذ.
العملية الذرية المدمجة في مجلس التعاون الخليجي
لدى GCC عمليات ذرية مدمجة ، والتي تمت إضافتها من 4.1.2. من قبل ، تم تنفيذها باستخدام التجميع المضمّن.
اكتب __sync_fetch_and_add (type *ptr ، type value ، ...) اكتب __sync_fetch_sub (اكتب *ptr ، نوع القيمة ، ...) اكتب __sync_fetch_and_or (type *ptr ، type type ، type القيمة ، ...) اكتب __sync_fetch_and_xor (اكتب *ptr ، اكتب القيمة ، ...) اكتب __sync_fetch_and_nand (اكتب *ptr ، نوع القيمة ، ...) اكتب __sync_add_and_fetch (اكتب * (اكتب *ptr ، قيمة النوع ، ...) اكتب __sync_and_and_fetch (اكتب *ptr ، قيمة النوع ، ...) اكتب __sync_and_and_fetch (اكتب *ptr ، نوع القيمة ، ...) اكتب __sync_xor_and_fetch (النوع *ptr ، اكتب القيمة ، ...) اكتب __sync_nand_ __sync_bool_compare_and_swap (اكتب *ptr ، اكتب نوع Oldval newval ، ...) اكتب __sync_val_compare_and_swap (النوع *ptr ، اكتب نوع Oldval newVal ، ...) __ sync_synchron __sync_lock_release (اكتب *ptr ، ...)
ما تجدر الإشارة إليه هو:
ملفات OpenJDK ذات الصلة
فيما يلي بعض تطبيقات التشغيل الذرية لـ OpenJDK9 على Github ، على أمل مساعدة أولئك الذين يحتاجون إلى معرفة ذلك. بعد كل شيء ، OpenJDK يستخدم على نطاق واسع من GCC. - ولكن بعد كل شيء ، لا يوجد رمز مصدر لـ Oracle JDK ، على الرغم من أنه يقال إن رمز المصدر بين OpenJdk و Oracle صغير جدًا.
Atomicinteger.java
غير آمن
غير آمن
Oop.inline.hpp :: Oopdesc :: Atomic_compare_exchange_oop
Atomic_linux_x86.hpp :: Atomic :: cmpxchg
Inline Jlong Atomic :: CMPXCHG (Jlong Exchange_value ، folatile Jlong* dest ، Jlong compare_value ، cmmpxchg_memory_order order) {bool mp = os :: is_mp () ؛ __asm__ __volatile__ (lock_if_mp (٪ 4) "cmpxchgq ٪ 1 ، (٪ 3)": "= a" (Exchange_value): "r" (Exchange_value) ، "a" (compare_value) ، "r" (dest) ، "r": "cc" ، "memory") ؛ إرجاع Exchange_value ؛} هنا نحتاج إلى إعطاء موجه لمبرمجي Java الذين ليسوا على دراية بـ C/C ++. تنسيق تعليمات التجميع المضمنة على النحو التالي
__asm__ [__volatile __] (قالب التجميع // قالب التجميع: [قائمة المعاملات الإخراج] // قائمة الإدخال: [قائمة معامل الإدخال] // قائمة الإخراج:
٪ 1 ، ٪ 3 ، ٪ 4 في قالب التجميع يتوافق مع قائمة المعلمة التالية {"r" (Exchange_value) ، "R" (Dest) ، "R" (MP)} ، وفصل قائمة المعلمات بواسطة الفواصل وتصنيفها من 0. تعني "R" وضعها في سجل عام ، "A" تعني تسجيل EAX ، و "=" الوسائل للإخراج (اكتب مرة أخرى). تعليمة CMPXCHG تعني استخدام سجلات EAX ، أي المعلمة ٪ 2.
التفاصيل الأخرى لن يتم سردها هنا. من المقرر تنفيذ مجلس التعاون الخليجي نقل المؤشر المراد تبادله ، وبعد المقارنة الناجحة ، يتم تعيين القيمة مباشرة (تعيين غير آلي) ، وضمان الذرة بواسطة spinlock.
يتم تنفيذ OpenJDK هو تمرير المؤشر المراد تبادله ، وتعيين القيم مباشرة من خلال تعليمات التجميع CMMPXCHGQ ، ويتم ضمان Atomicity من خلال تعليمات التجميع. بطبيعة الحال ، يتم ضمان الطبقة الأساسية من الدوران في GCC من خلال CMMPXCHGQ.
ما سبق هو كل محتوى هذه المقالة. آمل أن يكون ذلك مفيدًا لتعلم الجميع وآمل أن يدعم الجميع wulin.com أكثر.