العمليات الذرية وترتيب الذاكرة
Understand lock-free programming with std::atomic. Master memory orderings (relaxed, acquire, release, seq_cst), implement a spinlock with atomic_flag, and learn the compare-and-swap (CAS) pattern.
ما هي العمليات الذرية؟
العملية الذرية غير قابلة للتجزئة — فهي تكتمل بالكامل أو لا تكتمل على الإطلاق، مع عدم وجود حالة وسيطة مرئية للخيوط الأخرى. يوفرstd::atomicوصولاً ذريًا إلى قيمة من النوعTدون الحاجة إلى كائن المزامنة (mutex).
فكر في++counterبسيط علىintعادي مشترك بين سلاسل الرسائل. هذه في الواقع ثلاث خطوات: التحميل والزيادة والتخزين. يمكن أن يتداخل مؤشر ترابط آخر بين هذه الخطوات، مما يتسبب في حدوث سباق بيانات (سلوك غير محدد). معstd::atomic، الزيادة هي عملية ذرية واحدة - لا يوجد تشذير ولا UB.
لكن العلوم الذرية تذهب إلى ما هو أعمق من مجرد "العمليات غير القابلة للتجزئة". كما أنها تتحكم في ترتيب الذاكرة — القواعد التي تحكم عندما تصبح عمليات الكتابة بواسطة أحد المواضيع مرئية لسلاسل الرسائل الأخرى. هذا هو المكان الذي تصبح فيه الذرات قوية ودقيقة.
الأمراض المنقولة جنسيا::أساسيات الذرية
العمليات الذرية الأكثر شيوعًا هيload()وstore()وexchange()وfetch_add()/fetch_sub(). يمكنك أيضًا استخدام عوامل التشغيل++، و--، و+=، وما إلى ذلك.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> counter{0}; // atomic integer, initialized to 0
void increment(int n) {
for (int i = 0; i < n; ++i) {
counter.fetch_add(1, std::memory_order_relaxed);
// or simply: ++counter; (uses seq_cst by default)
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment, 100'000);
}
for (auto& t : threads) t.join();
std::cout << "Counter: " << counter.load() << "\n";
// Always prints 1000000 — no data race, no mutex needed
return 0;
}شرح ترتيب الذاكرة
تعمل وحدات المعالجة المركزية والمترجمات الحديثة على إعادة ترتيب عمليات الذاكرة من أجل الأداء. على نظام التشغيل x86، يعد هذا معتدلًا نسبيًا، ولكنه عدواني على ARM/RISC-V. تخبر طلبات الذاكرة المترجم ووحدة المعالجة المركزية (CPU) عن عمليات إعادة الترتيب المسموح بها.
memory_order_seq_cst (افتراضي) — الترتيب الأقوى. ترى جميع سلاسل الرسائل جميع عملياتseq_cstبنفس الترتيب الإجمالي **. بسيطة وآمنة ولكنها قد تحد من التحسين.
memory_order_acquire — يُستخدم عند الأحمال. لا يمكن إعادة ترتيب أي ذاكرة للقراءة/الكتابة في مؤشر الترابط الحالي قبل هذا التحميل. فكر في الأمر على النحو التالي: "كل ما أقرأه بعد هذه النقطة يرى على الأقل ما كتبه موضوع الإصدار."
memory_order_release — يُستخدم في المتاجر. لا يمكن إعادة ترتيب أي ذاكرة للقراءة/الكتابة في الموضوع الحالي بعد هذا المخزن. فكر في الأمر على النحو التالي: "كل ما كتبته قبل هذه النقطة يكون مرئيًا للخيط المكتسب."
memory_order_acq_rel — يجمع بين الاكتساب والإصدار. يُستخدم في عمليات القراءة والتعديل والكتابة مثلcompare_exchange.
memory_order_relaxed — لا توجد ضمانات للطلب تتجاوز الذرية. العملية ذرية ولكن يمكن إعادة ترتيبها بحرية. استخدم فقط للعدادات أو الإحصائيات المستقلة حيث لا يهم الطلب.
الحصول على الإصدار في الممارسة العملية
يعد نمط الاكتساب والإصدار هو العمود الفقري للبرمجة الخالية من القفل. يتزامن مخزن الإصدارات في أحد الخيوط مع تحميل الاكتساب في سلسلة رسائل أخرى، مما يؤدي إلى إنشاء علاقة تحدث قبل ذلك.
#include <iostream>
#include <atomic>
#include <thread>
#include <cassert>
std::atomic<bool> data_ready{false};
int payload = 0; // non-atomic — protected by acquire-release
void producer() {
payload = 42; // write payload BEFORE the release store
data_ready.store(true, std::memory_order_release);
// release: all writes before this store are visible
// to a thread that does an acquire load of data_ready
}
void consumer() {
while (!data_ready.load(std::memory_order_acquire)) {
// spin — acquire: sees all writes before the release store
}
// guaranteed to see payload == 42
assert(payload == 42); // never fires
std::cout << "Payload: " << payload << "\n";
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}نمط المقارنة والمبادلة (CAS).
المقارنة والمبادلة (compare_exchange_weak/compare_exchange_strong) هي لبنة البناء الأساسية لهياكل البيانات الخالية من القفل. فهو يتحقق تلقائيًا مما إذا كانت القيمة تساوي قيمة متوقعة، وإذا كان الأمر كذلك، يستبدلها بالقيمة المطلوبة.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<int> max_value{0};
void update_max(int new_val) {
int current = max_value.load(std::memory_order_relaxed);
// CAS loop: keep trying until we succeed or new_val is not bigger
while (new_val > current) {
if (max_value.compare_exchange_weak(
current, // expected (updated on failure)
new_val, // desired
std::memory_order_relaxed)) {
break; // success
}
// On failure, 'current' is updated with the actual value
// Loop re-checks if new_val > current (maybe another thread won)
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 100; ++i) {
threads.emplace_back(update_max, i);
}
for (auto& t : threads) t.join();
std::cout << "Max: " << max_value.load() << "\n"; // always 99
return 0;
}Spinlock مع std::atomic_flag
std::atomic_flagهو النوع الذري الأكثر بدائية - وهو مضمون بدون قفل على كل منصة. وهو يدعم فقطtest_and_set()وclear()، مما يجعله مثاليًا لقفل الدوران البسيط.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
class SpinLock {
std::atomic_flag flag_ = ATOMIC_FLAG_INIT;
public:
void lock() {
while (flag_.test_and_set(std::memory_order_acquire)) {
// spin — consider adding a pause/yield hint:
// __builtin_ia32_pause() or std::this_thread::yield()
}
}
void unlock() {
flag_.clear(std::memory_order_release);
}
};
SpinLock spin;
int shared_data = 0;
void work(int n) {
for (int i = 0; i < n; ++i) {
spin.lock();
++shared_data; // protected by spinlock
spin.unlock();
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.emplace_back(work, 100'000);
}
for (auto& t : threads) t.join();
std::cout << "shared_data: " << shared_data << "\n";
// prints 400000
return 0;
}يوفرmemory_order_relaxed لا توجد ضمانات للطلب خارج نطاق الذرية نفسها. يعد استخدامه لحماية البيانات غير الذرية المشتركة (مثل العلم الذي يحرس الحمولة) خطأً:
// BUG: relaxed store/load does not synchronize payload
payload = 42;
ready.store(true, memory_order_relaxed); // might be reordered BEFORE payload=42!
// Consumer:
while (!ready.load(memory_order_relaxed)) {}
assert(payload == 42); // MAY FAIL on ARM/POWERUse memory_order_relaxed only when the atomic variable stands alone and doesn't need to synchronize access to other data. For publish-consume patterns, use release/acquire.
ابدأ بـmemory_order_seq_cst(الافتراضي) واسترخِ فقط مع الطلبات الأضعف عندما يثبت التنميط أنه ضروري. تؤدي طلبات الذاكرة غير الصحيحة إلى ظهور أخطاء يكاد يكون من المستحيل إعادة إنتاجها على x86 ولكنها تظهر على ARM. تفضل كائنات المزامنة (mutexes) للحالة المشتركة المعقدة - فالذرات مخصصة للمتغيرات المشتركة البسيطة وبناء أوليات خالية من القفل. تحقق دائمًا من الافتراضات الخالية من القفل باستخدامstd::atomic.
- يوفر
std::atomicعمليات غير قابلة للتجزئة بدون كائنات المزامنة - وهي آمنة للقراءة والكتابة المتزامنة - يتحكم ترتيب الذاكرة في كيفية ظهور عمليات الكتابة في أحد المواضيع لسلاسل الرسائل الأخرى - فالأمر لا يتعلق بالذرية فقط
- يعد الحصول على الإصدار هو النمط الأساسي: ينشر متجر الإصدار البيانات، ويستهلكها الحصول على التحميل
compare_exchange_weak/strong(CAS) هو حجر الأساس للخوارزميات الخالية من القفلstd::atomic_flagهو النوع الوحيد المضمون بدون قفل على جميع الأنظمة الأساسية- الافتراضي هو
seq_cstولا يضعف إلا عندما يتطلب التوصيف ذلك - من الصعب للغاية تصحيح الأخطاء الدقيقة
اختبار — اختبر معرفتك
(25 نقطة خبرة)1. ماذا يضمن`memory_order_acquire`عند عملية التحميل؟
2. لماذا يتم استخدام`compare_exchange_weak`عادةً في حلقة؟
3. متى يكون`memory_order_relaxed`آمنًا للاستخدام؟