एटॉमिक ऑपरेशन और मेमोरी ऑर्डरिंग
std::atomic के साथ लॉक-फ़्री प्रोग्रामिंग को समझें। मेमोरी ऑर्डरिंग (relaxed, acquire, release, seq_cst) में महारत हासिल करें, atomic_flag से स्पिनलॉक बनाएँ, और compare-and-swap (CAS) पैटर्न सीखें।
एटॉमिक ऑपरेशन क्या हैं?
एटॉमिक ऑपरेशन (atomic operation) अविभाज्य होता है — वह या तो पूरी तरह पूरा होता है या बिल्कुल नहीं, और दूसरे थ्रेड को कोई मध्यवर्ती स्थिति दिखाई नहीं देती। std::atomic प्रकार T के मान तक एटॉमिक एक्सेस देता है, और इसके लिए म्यूटेक्स की ज़रूरत नहीं पड़ती।
थ्रेड के बीच साझा किए गए एक सामान्य int पर एक साधारण ++counter पर विचार करें। यह असल में तीन चरण हैं: लोड, इंक्रीमेंट, स्टोर। कोई दूसरा थ्रेड इन चरणों के बीच घुस सकता है, जिससे डेटा रेस (अपरिभाषित व्यवहार) पैदा होती है। std::atomic के साथ, इंक्रीमेंट एक ही एटॉमिक ऑपरेशन होता है — न कोई बीच में घुसपैठ, न UB।
लेकिन एटॉमिक सिर्फ़ "अविभाज्य ऑपरेशन" से कहीं आगे जाते हैं। वे मेमोरी ऑर्डरिंग (memory ordering) को भी नियंत्रित करते हैं — यानी वे नियम जो तय करते हैं कि एक थ्रेड के लिखे गए मान दूसरे थ्रेड को कब दिखाई देने लगते हैं। यहीं एटॉमिक शक्तिशाली भी हो जाते हैं और जटिल भी।
std::atomic की बुनियादी बातें
सबसे आम एटॉमिक ऑपरेशन 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;
}मेमोरी ऑर्डरिंग की व्याख्या
आधुनिक CPU और कंपाइलर प्रदर्शन के लिए मेमोरी ऑपरेशन को रीऑर्डर (reorder) करते हैं। x86 पर यह अपेक्षाकृत हल्का होता है, लेकिन ARM/RISC-V पर यह आक्रामक होता है। मेमोरी ऑर्डरिंग कंपाइलर और CPU को बताती हैं कि कौन-सी रीऑर्डरिंग की अनुमति है।
memory_order_seq_cst (डिफ़ॉल्ट) — सबसे मज़बूत ऑर्डरिंग। सभी थ्रेड सभी seq_cst ऑपरेशन को एक ही कुल क्रम (same total order) में देखते हैं। सरल और सुरक्षित, लेकिन ऑप्टिमाइज़ेशन को सीमित कर सकती है।
memory_order_acquire — लोड पर इस्तेमाल होती है। मौजूदा थ्रेड का कोई भी मेमोरी रीड/राइट इस लोड से पहले रीऑर्डर नहीं हो सकता। इसे ऐसे समझें: "इस बिंदु के बाद मैं जो कुछ भी पढ़ता हूँ, वह कम से कम वह सब देखता है जो रिलीज़ करने वाले थ्रेड ने लिखा था।"
memory_order_release — स्टोर पर इस्तेमाल होती है। मौजूदा थ्रेड का कोई भी मेमोरी रीड/राइट इस स्टोर के बाद रीऑर्डर नहीं हो सकता। इसे ऐसे समझें: "इस बिंदु से पहले मैंने जो कुछ भी लिखा, वह acquire करने वाले थ्रेड को दिखाई देता है।"
memory_order_acq_rel — acquire और release को जोड़ती है। compare_exchange जैसे read-modify-write ऑपरेशन पर इस्तेमाल होती है।
memory_order_relaxed — एटॉमिसिटी से आगे कोई ऑर्डरिंग गारंटी नहीं। ऑपरेशन एटॉमिक होता है, लेकिन उसे खुलकर रीऑर्डर किया जा सकता है। केवल स्वतंत्र काउंटर या सांख्यिकी के लिए इस्तेमाल करें, जहाँ ऑर्डरिंग मायने नहीं रखती।
व्यवहार में Acquire-Release
acquire-release पैटर्न लॉक-फ़्री प्रोग्रामिंग का मुख्य आधार है। एक थ्रेड में release store दूसरे थ्रेड में acquire load के साथ सिंक्रोनाइज़ होता है, और इस तरह happens-before संबंध स्थापित करता है।
#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;
}Compare-and-Swap (CAS) पैटर्न
Compare-and-swap (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;
}std::atomic_flag के साथ स्पिनलॉक
std::atomic_flag सबसे बुनियादी एटॉमिक प्रकार है — हर प्लेटफ़ॉर्म पर लॉक-फ़्री होने की गारंटी वाला। इसके मूल ऑपरेशन केवल test_and_set() और clear() हैं (C++20 में test() और wait()/notify जैसे ऑपरेशन भी जुड़े), जो इसे एक सरल स्पिनलॉक (spinlock) के लिए एकदम उपयुक्त बनाता है।
#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/POWERmemory_order_relaxed का उपयोग केवल तभी करें जब एटॉमिक वेरिएबल अकेला हो और उसे अन्य डेटा के एक्सेस को सिंक्रोनाइज़ करने की ज़रूरत न हो। publish-consume पैटर्न के लिए release/acquire का उपयोग करें।
memory_order_seq_cst (डिफ़ॉल्ट) से शुरू करें और कमज़ोर ऑर्डरिंग की ओर तभी जाएँ जब प्रोफ़ाइलिंग इसे ज़रूरी साबित करे। ग़लत मेमोरी ऑर्डरिंग ऐसे बग पैदा करती है जिन्हें x86 पर दोहराना लगभग असंभव है, लेकिन जो ARM पर प्रकट होते हैं। जटिल साझा स्टेट के लिए म्यूटेक्स को प्राथमिकता दें — एटॉमिक सरल साझा वेरिएबल और लॉक-फ़्री प्रिमिटिव बनाने के लिए हैं। लॉक-फ़्री से जुड़ी धारणाओं को हमेशा std::atomic से जाँचें।
std::atomicम्यूटेक्स के बिना अविभाज्य ऑपरेशन देता है — समवर्ती रीड और राइट के लिए सुरक्षित- मेमोरी ऑर्डरिंग नियंत्रित करती है कि एक थ्रेड के लिखे मान दूसरे थ्रेड को कैसे दिखाई देते हैं — यह केवल एटॉमिसिटी की बात नहीं है
- Acquire-release मुख्य पैटर्न है: release-store डेटा को प्रकाशित करता है, acquire-load उसे ग्रहण करता है
compare_exchange_weak/strong(CAS) लॉक-फ़्री एल्गोरिदम का निर्माण-खंड हैstd::atomic_flagएकमात्र प्रकार है जिसके सभी प्लेटफ़ॉर्म पर लॉक-फ़्री होने की गारंटी है- डिफ़ॉल्ट रूप से
seq_cstरखें और केवल तब कमज़ोर करें जब प्रोफ़ाइलिंग की माँग हो — सूक्ष्म बग को डीबग करना बेहद कठिन होता है
क्विज़ — अपना ज्ञान परखें
(25 XP)1. लोड ऑपरेशन पर `memory_order_acquire` क्या गारंटी देता है?
2. `compare_exchange_weak` को आमतौर पर लूप में क्यों इस्तेमाल किया जाता है?
3. `memory_order_relaxed` का उपयोग कब सुरक्षित है?