C / المتقدم: حدود اللغة والأنظمة / الدرس 16
التزامن والذريات في C11
حلل الكائنات المشتركة بالمزامنة وعلاقات السبق، لا بالتوقيت أو volatile أو سلوك آلة شوهد مرة.
في هذه الصفحة
أهداف الدرس
- تحديد الوصولات المتعارضة وسباقات البيانات.
- اختيار حماية القفل أو العملية الذرية.
- شرح العد المرخي وحدود النشر بالإطلاق والاكتساب.
فحص المرافق وتحديد المشاركة
أضافت C11 الترويستين threads.h وstdatomic.h. قد تحذف تنفيذات C17 هذه المرافق الاختيارية وتعلن __STDC_NO_THREADS__ أو __STDC_NO_ATOMICS__. تحقق من التنفيذ وخيارات الربط قبل استخدام الأمثلة الكاملة التي تفترض توافر المرافق المطلوبة. تختار -pthread إعداد الخيوط اللازم في أدوات يونكس كثيرة؛ والخيار ليس صياغة ISO C.
يتضمن سباق البيانات فعلين متعارضين في خيطين مختلفين، أحدهما على الأقل غير ذري، دون علاقة السبق المطلوبة. يسبب سباق C سلوكا غير معرّف. ولا يعني مجرد احتمال ضياع تحديث للعداد. حدد مالك كل كائن أو المزامنة الحامية لكل وصول مشترك.
استخدام عملية واحدة للتحديث
يوفر الكائن الذري عمليات لا تتجزأ كالوصول العادي المتعارض. لكن قراءة ثم كتابة منفصلة ليستا تحديث قراءة وتعديل وكتابة ذري واحد. تنفذ atomic_fetch_add الزيادة كعملية ذرية واحدة وتعيد القيمة السابقة للزيادة.
يعزل البرنامج الأول هذه الدلالة في خيط واحد. يكفي الترتيب المرخي لعداد عددي مستقل عندما لا ينشر بيانات أخرى عبره. ليست الأنواع الذرية خالية من الأقفال دائما؛ استعلم عن التنفيذ إذا همت الخاصية، ولا تخلط غياب الأقفال بضمان انتهاء محدود لكل خيط.
#include <stdatomic.h>
#include <stdio.h>
int main(void)
{
atomic_int counter;
atomic_init(&counter, 5);
int previous = atomic_fetch_add_explicit(&counter, 2, memory_order_relaxed);
int current = atomic_load_explicit(&counter, memory_order_relaxed);
printf("before=%d after=%d\n", previous, current);
return 0;
}
before=5 after=7
تعيد عملية الزيادة الواحدة خمسة وتخزن سبعة؛ ولا يلزم ترتيب لنشر بيانات أخرى.
الانضمام قبل الاعتماد على الاكتمال
يبدأ المثال المتزامن عاملا واحدا بينما تجري main ألف زيادة أخرى. كل وصول إلى counter ذري، ومجموع ألفين يلائم حتى أدنى مدى int مطلوب. للكائن المشترك مدة تخزين ساكنة، فلا يستعير خيط سياقا محليا انتهى عمره.
قد تفشل thrd_create ويجب فحص thrd_join قبل الاعتماد على الاكتمال الناجح. تقع القراءة الأخيرة بعد انضمام ناجح، فتكون الحلقتان قد اكتملتا. قد تتداخل الزيادات بطرائق عديدة بحسب الجدولة، لكن المجموع في مسار النجاح يبقى ألفين. هذه نتيجة مبررة بالعقد وليست توقع توقيت.
#include <stdatomic.h>
#include <stdio.h>
#include <threads.h>
static atomic_int counter;
static int worker(void *unused)
{
(void)unused;
for (int i = 0; i < 1000; ++i)
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
return 0;
}
int main(void)
{
thrd_t thread;
int result;
atomic_init(&counter, 0);
if (thrd_create(&thread, worker, NULL) != thrd_success) return 1;
for (int i = 0; i < 1000; ++i)
atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
if (thrd_join(thread, &result) != thrd_success || result != 0) return 1;
printf("counter=%d\n", atomic_load_explicit(&counter, memory_order_relaxed));
return 0;
}
counter=2000
في تنفيذ مدعوم ومع نجاح عمليات الخيوط تُحفظ الزيادات الألفان جميعها.
مزامنة البيانات لا العلم وحده
يمكن لكتابة إطلاق إلى علم ذري نشر الكتابات السابقة عندما تقرأ عملية اكتساب في خيط آخر القيمة المنشورة المناسبة. تنشأ بذلك علاقة مزامنة لتلك الكتابات؛ ولا يحقق علم مرخى وحده ذلك. يحتاج النشر أيضا بروتوكول عمر وقاعدة تمنع تعديلا لاحقا متزامنا للبيانات المفترض نشرها.
يمنح القفل غالبا ثابتا أوضح لعدة حقول مترابطة من ذريات مستقلة. احم كل وصول بسياسة القفل نفسها وافحص التهيئة ونتائج القفل. تتطلب الأقفال المتعددة ترتيب اقتناء متسقا لمنع الجمود. ويجب أن يكون انتظار متغير الشرط داخل حلقة تعيد فحص المسند المحمي بعد الاستيقاظ.
عدم استبدال المزامنة بالأمل
تضبط volatile وصولات ملحوظة معينة؛ ولا تجعل العداد المشترك ذريا ولا تنشئ ترتيبا بين الخيوط. ولا ينشئ النوم لمدة مخمنة مزامنة. وقد تمتلك شيفرة ذرية صحيحة سباقا منطقيا أعلى، مثل فحص رصيد ثم التصرف لاحقا دون حماية المعاملة كاملة.
استخدم ThreadSanitizer عند دعمه في بناء فحوص منفصل للتحقيق في السباقات المنفذة. لا يثبت تكرار النجاح غياب السباق. راجع علاقات السبق وملكية الأقفال وعمر الكائن ومسارات الإنهاء. ابدأ بذريات متسقة تسلسليا أو قفل إذا كان التصميم غامضا، ولا تضعف الترتيب إلا بحجة صحة مكتوبة.
الأخطاء الشائعة وإصلاحها
تُستخدم volatile لإصلاح زيادة مشتركة.
- لماذا يحدث؟
- اختلط الوصول الملحوظ بالمزامنة الذرية.
- كيف تصلحه؟
- استخدم تحديثا ذريا أو قفلا حول العملية كاملة.
- كيف تتجنب تكراره؟
- راجع سياسة الوصول لكل كائن مشترك.
يصبح العلم الذري صحيحا لكن الوصول للحمولة غير صالح.
- لماذا يحدث؟
- لم يغط بروتوكول النشر الترتيب أو العمر أو التعديل اللاحق.
- كيف تصلحه؟
- أنشئ علاقة إطلاق واكتساب مناسبة وسياسة عمر للبيانات.
- كيف تتجنب تكراره؟
- وثق الكتابة التي تلاحظها عملية الاكتساب بالضبط.
يحرر الإنهاء الحالة والعامل يستخدمها.
- لماذا يحدث؟
- لم ينتظر المالك انتهاء الخيوط المستعيرة.
- كيف تصلحه؟
- أشر للإنهاء بمزامنة وانضم قبل التحرير.
- كيف تتجنب تكراره؟
- أدرج مسارات الفشل والإلغاء في تصميم العمر.
اختبر فهمك
1. لماذا لا تكفي atomic_load ثم atomic_store للقيمة old + 1 لعداد مشترك دقيق؟
اعرض الحل المشروح
قد يقرأ خيطان القيمة السابقة نفسها ثم يكتبان الزيادة نفسها. الوصولات الفردية ذرية فلا يلزم أن يكون ذلك سباق بيانات، لكن الخوارزمية تضيع زيادة. استخدم atomic_fetch_add لتحديث واحد أو احم المعاملة كاملة بقفل. لا تعني ذرية الأجزاء ذرية العملية المركبة.
2. يتلقى عامل مؤشرا إلى إعداد محلي. حدد بروتوكولا آمنا للعمر والإنهاء.
اعرض الحل المشروح
أبق الإعداد حيا ودون تعديل وفق سياسة المشاركة حتى انتهاء العامل. افحص الإنشاء؛ وعند نجاحه رتب إنهاء متزامنا عند الحاجة وانضم بنجاح قبل مغادرة نطاق الإعداد. إذا احتفظ العامل بمؤشرات متداخلة فتحتاج أهدافها الضمان نفسه. لا يمدد نسخ المؤشر الأعلى عمر أي هدف.
أهم ما تعلمته
- يحتاج غياب السباق حجة مزامنة لا جدولة ناجحة.
- لا ينشر العد الذري المرخي بيانات غير مرتبطة.
- تحفظ قواعد الانضمام والملكية حياة الكائنات أثناء الإنهاء.