Deadlocks و Livelocks - كيفية تجنب في العالم الحقيقي التزامن؟

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

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

فيما يلي بعض الحلول النظرية لـ Deadlocks ، واحدها (الثاني) هو السبب الرئيسي لـ Livelocks

المناهج النظرية

لا تستخدم الأقفال

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

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

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

دع المواضيع تطلب دائمًا الأقفال بترتيب صارم

قال أسهل من القيام به ، على سبيل المثال. إذا كنا نكتب وظيفة لتحويل الأموال من الحساب أ إلى ب ، فيمكننا كتابة شيء من هذا القبيل

/ / في وقت الترجمة ، نأخذ قفل أول وسيطة ثم الثانية
نقل الفراغ العام (الحساب أ ، الحساب ب ، الأموال الطويلة) {
  متزامن (A) {
    متزامن (B) {
      A.add (مبلغ)؛
      B.subtract (مبلغ)؛
    }
  }
}
/ / في وقت التشغيل ، لا يمكننا تتبع كيفية استدعاء أساليبنا
تشغيل الفراغ العام () {
  مؤشر ترابط جديد (() -> this.transfer (X، Y، 10000)). start ()؛
  مؤشر ترابط جديد (() -> this.transfer (Y، X، 10000)). start ()؛
}
/ / هذا المدى () سيخلق حالة توقف تام
// أقفال مؤشر الترابط الأول على X ، ينتظر Y
// أقفال مؤشر الترابط الثاني على Y ، ينتظر X

حل العالم الحقيقي

يمكننا الجمع بين أساليب ترتيب الأقفال والأقفال الموقوتة للتوصل إلى حل حقيقي للكلمة

تحديد الأعمال قفل الطلب

يمكننا تحسين نهجنا من خلال التمييز بين A و B بناءً على رقم حسابه أكبر أو أصغر.

/ / في وقت التشغيل ، نأخذ قفل الحساب بمعرف أصغر أولاً
نقل الفراغ العام (الحساب أ ، الحساب ب ، الأموال الطويلة) {
  الحساب الختامي أولاً = A.id 
  متزامن (الأول) {
    متزامن (الثاني) {
      first.add (مبلغ)؛
      second.subtract (مبلغ)؛
    }
  }
}
/ / في وقت التشغيل ، لا يمكننا تتبع كيفية استدعاء أساليبنا
تشغيل الفراغ العام () {
  مؤشر ترابط جديد (() -> this.transfer (X، Y، 10000)). start ()؛
  مؤشر ترابط جديد (() -> this.transfer (Y، X، 10000)). start ()؛
}

على سبيل المثال ، إذا كان X.id = 1111 ، و Y.id = 2222 ، نظرًا لأننا نأخذ في الحساب الأول كواحد مع معرف حساب أصغر ، ترتيب تأمين لتنفيذ عمليات النقل (Y ، X ، 10000) ونقل (X ، Y ، 10000) سيكون نفسه. X له رقم حساب أقل من Y ، سيحاول كلتا القفلتين قفل X قبل Y وسينجح واحد منهم فقط ويستمر في قفل النهاية Y وإقفال الأقفال على X و Y قبل أن يكتسب خيط المناقشة الآخران القفل ويستطيع المتابعة.

الأعمال المحددة زمن الانتظار انتظر طلبات تأمين قفل / مزامنة

لا يعمل حل استخدام أمر التأمين المحدد للأعمال إلا في حالة العلاقات الترابطية حيث ينقل منطق في مكان واحد (....) ، كما هو الحال في أسلوبنا ، كيفية تنسيق الموارد.

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

// تفترض Account # getLock () يمنحنا قفل الحساب (java.util.concurrent.locks.Lock)
// يمكن للحساب تغليف القفل ، وتوفير القفل () / إلغاء القفل ()
getWait العام ()
/// تقوم بإرجاع المتوسط ​​المتحرك لأوقات النقل لآخر عمليات نقل n + ملح صغير عشوائي بالمللي ، لذا لا تستيقظ جميع الخيوط التي تنتظر قفلها في نفس الوقت.
}
نقل الفراغ العام (Lock lockF ، و Lock locks ، ومبلغ int) {
  الحساب الختامي أولاً = A.id 
  منطقية عمله = كاذبة.
  فعل {
    محاولة {
      محاولة {
        if (lockF.tryLock (getWait ()، MILLISECONDS)) {
          محاولة {
            if (lockS.tryLock (getWait () ، المليارات)) {
              عمله = صحيح ؛
            }
          } أخيرا {
            lockS.unlock ()؛
          }
        }
      } catch (InterruptedException e) {
        رمي RuntimeException الجديد ("تم الإلغاء") ؛
      }
    } أخيرا {
      lockF.unlock ()؛
    }
  } بينما (! فعلت) ؛

}
/ / في وقت التشغيل ، لا يمكننا تتبع كيفية استدعاء أساليبنا
تشغيل الفراغ العام () {
    مؤشر ترابط جديد (() -> this.transfer (X، Y، 10000)). start ()؛
    مؤشر ترابط جديد (() -> this.transfer (Y، X، 10000)). start ()؛
}