Atomics.wait()تم حظرها ، فلا يمكن استدعاؤها في السلسلة الرئيسية (إذا حاولت القيام بذلك ، فسيتم طرح خطأ TypeError).
يدعم محرك V8 ، منذ الإصدار 8.7 ، خيارًا بدون حظر
Atomics.wait()يسمى Atomics.waitAsync () . يمكن استخدام هذه الطريقة الجديدة على الخيط الرئيسي.
سنعرض لك اليوم كيفية استخدام واجهات برمجة التطبيقات ذات المستوى المنخفض لإنشاء كائن المزامنة (mutex) الذي يمكن تشغيله بشكل متزامن (في مؤشرات ترابط العاملين) وغير متزامن (في مؤشرات ترابط العاملين أو في مؤشر ترابط رئيسي).
Atomics.wait () و Atomics.wait Async ()
طرق
Atomics.wait()و Atomics.waitAsync()تأخذ المعلمات التالية:
buffer: مصفوفة من النوعInt32ArrayأوBigInt64Arrayالتي تستند إلىSharedArrayBuffer.index: الفهرس الفعلي للعنصر في المصفوفة.expectedValue: القيمة التي نتوقع تمثيلها في الذاكرة ، في الموقع الموصوف بـbufferوindex.timeout: المهلة بالمللي ثانية (اختياري ، الافتراضي إلىInfinity).
Atomics.wait()إرجاع سلسلة. إذا لم يتم العثور على القيمة المتوقعة في موقع الذاكرة المحدد ، يتم Atomics.wait()الخروج منها على الفور ، وإرجاع سلسلة not-equal. خلاف ذلك ، يتم حظر الخيط. يجب أن يقع أحد الأحداث التالية حتى يتم تحرير القفل. الأول هو استدعاء من خيط آخر لطريقة Atomics.notify()مع الإشارة إلى المكان في الذاكرة الذي تهتم به الطريقة Atomics.wait(). والثاني هو انتهاء المهلة. في الحالة الأولى ، ستعيد Atomics.wait()سلسلة ok، في الحالة الثانية - قيمة سلسلة timed-out. تأخذ
الطريقة
Atomics.notify()المعلمات التالية:
typedArray: مصفوفة من النوعInt32ArrayأوBigInt64Arrayالتي تستند إلىSharedArrayBuffer.index: الفهرس الفعلي للعنصر في المصفوفة.count: عدد الوكلاء الذين ينتظرون الإعلام (معلمة اختيارية ، مضبوطة افتراضيًاInfinity).
طريقة
Atomics.notify()تخطر وصف عدد محدد من وكلاء انتظار الإعلام في عنوان typedArrayو indexتجاوزها من أجل FIFO. إذا تم إجراء عدة مكالمات Atomics.wait()أو Atomics.waitAsync()مشاهدة نفس المكان في الذاكرة ، فسينتهي بهم الأمر جميعًا في نفس قائمة الانتظار.
على عكس الطريقة
Atomics.wait()، Atomics.waitAsync()ترجع الطريقة على الفور قيمة في موقع الاستدعاء. يمكن أن تكون إحدى القيم التالية:
{ async: false, value: 'not-equal' }- إذا كان موقع الذاكرة المحدد لا يحتوي على القيمة المتوقعة.{ async: false, value: 'timed-out' }- فقط عند ضبط المهلة على 0.{ async: true, value: promise }- في حالات أخرى.
الوعد ، بعد مرور بعض الوقت ، يمكن حله بنجاح من خلال قيمة سلسلة
ok(إذا تم استدعاء طريقة Atomics.notify()، تم تمرير معلومات حول المكان الذي تم تمريره في الذاكرة Atomics.waitAsync()). يمكن حلها بقيمة timed-out. هذا الوعد لم يتم رفضه.
يوضح المثال التالي أساسيات الاستخدام
Atomics.waitAsync():
const sab = new SharedArrayBuffer(16);
const i32a = new Int32Array(sab);
const result = Atomics.waitAsync(i32a, 0, 0, 1000);
// | | ^ - ()
// | ^
// ^
if (result.value === 'not-equal') {
// SharedArrayBuffer .
} else {
result.value instanceof Promise; // true
result.value.then(
(value) => {
if (value == 'ok') { /* */ }
else { /* - */ }
});
}
// :
Atomics.notify(i32a, 0);
الآن دعنا نتحدث عن كيفية إنشاء كائن المزامنة (mutex) الذي يمكن استخدامه في كلا الوضعين المتزامن وغير المتزامن. وتجدر الإشارة إلى أن تنفيذ الإصدار المتزامن من كائن المزامنة (mutex) قد تمت مناقشته مسبقًا. على سبيل المثال - في هذه المادة.
في هذا المثال ، لن نستخدم المعامل
timeoutعند استدعاء Atomics.wait()و Atomics.waitAsync(). يمكن استخدام هذه المعلمة لتنفيذ الشروط ذات الصلة بالمهلة.
طبقتنا
AsyncLockيمثل مزامنة تعمل مع العازلة SharedArrayBufferوالأدوات والأساليب التالية:
lock(): يحظر الخيط حتى تتاح لنا الفرصة لالتقاط كائن المزامنة (لا ينطبق إلا في مؤشر ترابط العامل).unlock(): يحرر كائن المزامنة (هذا هو عكس ذلكlock()).executeLocked(callback): يحاول الحصول على القفل دون حجب الخيط. يمكن استخدام هذه الطريقة على الخيط الرئيسي. يخطط لتنفيذ رد الاتصال في الوقت الذي يمكننا فيه الحصول على القفل.
دعونا نلقي نظرة على كيفية تنفيذ هذه الأساليب. يتضمن إعلان الفئة ثوابت ومنشئًا يأخذ مخزنًا مؤقتًا
SharedArrayBuffer.
class AsyncLock {
static INDEX = 0;
static UNLOCKED = 0;
static LOCKED = 1;
constructor(sab) {
this.sab = sab;
this.i32a = new Int32Array(sab);
}
lock() {
/* … */
}
unlock() {
/* … */
}
executeLocked(f) {
/* … */
}
}
هنا
i32a[0]يحتوي العنصر على القيمة LOCKEDأو UNLOCKED. هو ، بالإضافة إلى ذلك ، يمثل المكان في الذاكرة الذي يهم Atomics.wait()و Atomics.waitAsync(). AsyncLockيوفر الفصل القدرات الأساسية التالية:
i32a[0] == LOCKEDوالخيط في حالة انتظار (بعد استدعائهAtomics.wait()أوAtomics.waitAsync()) ، ومشاهدتهi32a[0]، سيتم إعلامه في النهاية.- بعد إخطار الخيط ، سيحاول الحصول على القفل. إذا نجحت ، فعندما تقوم بتحرير القفل ، فسوف تستدعي
Atomics.notify().
التقاط وإطلاق القفل المتزامن
ضع في اعتبارك رمز الطريقة
lock()التي لا يمكن استدعاؤها إلا من مؤشر ترابط عامل.
lock() {
while (true) {
const oldValue = Atomics.compareExchange(this.i32a, AsyncLock.INDEX,
/* >>> */ AsyncLock.UNLOCKED,
/* >>> */ AsyncLock.LOCKED);
if (oldValue == AsyncLock.UNLOCKED) {
return;
}
Atomics.wait(this.i32a, AsyncLock.INDEX,
AsyncLock.LOCKED); // <<< ,
}
}
عندما يتم استدعاء طريقة من مؤشر ترابط
lock()، فإنها تحاول أولاً الحصول على القفل ، واستخدامها Atomics.compareExchange()لتغيير حالة القفل من UNLOCKEDإلى LOCKED. Atomics.compareExchange()تحاول الطريقة إجراء عملية ذرية لتغيير حالة القفل ، وتقوم بإرجاع القيمة الأصلية الموجودة في منطقة الذاكرة المحددة. إذا كانت القيمة الأصلية UNLOCKED، فهذا سيخبرنا أن تغيير الحالة كان ناجحًا وأن الخيط قد حصل على القفل. لا تحتاج إلى فعل أي شيء آخر.
إذا
Atomics.compareExchange()لم يتمكن من تغيير حالة القفل ، فهذا يعني أن هناك خيطًا آخر يحمل القفل. نتيجة لذلك ، lock()يحاول الخيط الذي يتم استدعاء الطريقة منه استخدام الطريقةAtomics.wait()من أجل الانتظار حتى يتم تحرير القفل بواسطة مؤشر ترابط آخر. إذا كانت القيمة المتوقعة لا تزال مخزنة في منطقة الذاكرة ذات الأهمية (في حالتنا - AsyncLock.LOCKED) ، فستقوم المكالمة Atomics.wait()بحظر الخيط. Atomics.wait()سيحدث العودة من فقط عندما يستدعى موضوع آخر Atomics.notify().
تقوم الطريقة
unlock()بتحرير القفل عن طريق ضبطه في الحالة UNLOCKEDواستدعائه Atomics.notify()لإخطار الوكلاء الذين ينتظرون تحرير القفل. من المفترض أن تنجح عملية تغيير حالة القفل دائمًا. وذلك لأن مؤشر الترابط الذي يقوم بهذه العملية يحتفظ بقفل. لذلك ، لا شيء آخر يجب أن يستدعي الطريقة في هذا الوقت unlock().
unlock() {
const oldValue = Atomics.compareExchange(this.i32a, AsyncLock.INDEX,
/* >>> */ AsyncLock.LOCKED,
/* >>> */ AsyncLock.UNLOCKED);
if (oldValue != AsyncLock.LOCKED) {
throw new Error('Tried to unlock while not holding the mutex');
}
Atomics.notify(this.i32a, AsyncLock.INDEX, 1);
}
في حالة نموذجية ، يحدث كل شيء على النحو التالي: القفل مجاني ويلتقطه الخيط T1 ، ويغير حالته باستخدام
Atomics.compareExchange(). يحاول Thread T2 الحصول على القفل عن طريق الاتصال Atomics.compareExchange()، لكن لا يمكنه تغيير حالته. ثم مكالمات T2 Atomics.wait()، ستحظر هذه المكالمة الموضوع. بعد مرور بعض الوقت ، يقوم مؤشر الترابط T1 بتحرير القفل والمكالمات Atomics.notify(). يؤدي هذا استدعاء Atomics.wait()T2 للعودة okو T2 للخروج من القفل. ثم يحاول T2 الحصول على القفل مرة أخرى. هذه المرة نجح.
هناك نوعان من الحالات الخاصة هنا. يهدف تحليلهم إلى توضيح أسباب ذلك
Atomics.wait()، Atomics.waitAsync()والتحقق من قيمة محددة في الفهرس المحدد لعنصر المصفوفة. هذه هي الحالات:
- T1 , T2 . T2 ,
Atomics.compareExchange(), . T1 , T2Atomics.wait(). T2Atomics.wait(),not-equal. T2 . - T1 , T2
Atomics.wait(). T1 , T2 (Atomics.wait())Atomics.compareExchange(). , T3, . .Atomics.compareExchange()T2 . T2Atomics.wait(), T3 .
توضح الحالة الخاصة الأخيرة حقيقة أن كائن المزامنة الخاص بنا لا يعمل بشكل عادل. قد يحدث أن الخيط T2 كان ينتظر تحرير القفل ، لكن T3 تمكن من الحصول عليه فور تحريره. يمكن أن يستخدم تطبيق القفل الأكثر ملاءمة للاستخدام في العالم الحقيقي العديد من حالات القفل الموجودة للتمييز بين المواقف التي تم فيها "اكتساب" القفل ببساطة والتي "حدث فيها تعارض أثناء الاستحواذ".
التقاط قفل غير متزامن
يمكن استدعاء طريقة غير مانعة
executeLocked()، على عكس الطريقة lock()، من الخيط الرئيسي. يتلقى ، باعتباره المعلمة الوحيدة ، رد اتصال ، ويقوم بجدولة رد الاتصال بعد الحصول على قفل ناجح.
executeLocked(f) {
const self = this;
async function tryGetLock() {
while (true) {
const oldValue = Atomics.compareExchange(self.i32a, AsyncLock.INDEX,
/* >>> */ AsyncLock.UNLOCKED,
/* >>> */ AsyncLock.LOCKED);
if (oldValue == AsyncLock.UNLOCKED) {
f();
self.unlock();
return;
}
const result = Atomics.waitAsync(self.i32a, AsyncLock.INDEX,
AsyncLock.LOCKED);
// ^ ,
await result.value;
}
}
tryGetLock();
}
tryGetLock()تحاول
الوظيفة الداخلية أولاً الحصول على القفل باستخدام Atomics.compareExchange(). إذا أدى استدعاء هذه الطريقة إلى تغيير حالة القفل بنجاح ، يمكن للوظيفة استدعاء رد اتصال ، ثم تحرير القفل والخروج.
إذا
Atomics.compareExchange()لم تسمح المكالمة بالحصول على القفل ، فنحن بحاجة إلى محاولة القيام بذلك مرة أخرى ، في الوقت الذي سيكون فيه القفل مجانيًا على الأرجح. لكن لا يمكننا حظر الخيط وانتظار تحرير القفل. بدلاً من ذلك ، نقوم Atomics.waitAsync()بجدولة محاولة جديدة للحصول على القفل باستخدام الطريقة والوعد الذي سيعود به.
إذا نجحنا في تنفيذ الطريقة
Atomics.waitAsync()، فسيتم حل الوعد الذي تم إرجاعه بواسطة هذه الطريقة عندما استدعى مؤشر الترابط الذي يحتوي على قفلAtomics.notify()... بعد ذلك ، يحاول الخيط الذي أراد الحصول على القفل ، كما كان من قبل ، القيام بذلك مرة أخرى.
هنا ، من الممكن أن تكون هذه الحالات الخاصة مميزة للإصدار المتزامن (يتم تحرير القفل بين المكالمات
Atomics.compareExchange()و Atomics.waitAsync()؛ يتم التقاط القفل بواسطة مؤشر ترابط آخر ، يتم القيام بذلك بين لحظات حل الوعد والمكالمة Atomics.compareExchange()). لذلك ، في قانون مماثل ينطبق في المشاريع الحقيقية ، يجب أن يؤخذ ذلك في الاعتبار.
النتيجة
في هذه المقالة ، تحدثنا عن أساسيات المزامنة منخفضة المستوى
Atomics.wait()، Atomics.waitAsync()و Atomics.notify(). لقد قمنا بتحليل مثال على إنشاء كائن المزامنة (mutex) بناءً عليه ، والذي يمكن استخدامه في كلٍ من السلسلة الرئيسية وفي سلاسل العمليات.
هل ستكون Atomics.wait () و Atomics.waitAsync () و Atomics.notify () مفيدة في مشروعاتك؟