अंतिम अद्यतन:2026-10-12
C++ सिंगलटन पैटर्न¶
0.प्रस्तावना¶
विश्वास है कि इंटरव्यू में सभी से डिज़ाइन पैटर्न की चर्चा अनिवार्यतः होती है। यह अनुभाग इंटरव्यू में सर्वाधिक प्रयुक्त डिज़ाइन पैटर्न (सिंगलटन पैटर्न) की व्याख्या करता है — वर्गीकरण, thread safety, तथा C++11 से पहले तथा बाद के दृष्टिकोण से thread-safe सिंगलटन पैटर्न की कौन-कौन सी योजनाएँ हैं। विश्वास है यह लेख ध्यान से पढ़ लेने के बाद, आगे के इंटरव्यू में चिंता करने की आवश्यकता नहीं रहेगी।
1.सर्वविदित सिंगलटन¶
सामान्य पुस्तकों में या जिसे सभी भली-भाँति जानते हैं, सिंगलटन पैटर्न नीचे जैसा होता है:
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
if (p == nullptr)
p = new singleton();
return p;
}
यह बहुत सरल कार्यान्वयन है — कंस्ट्रक्टर को private या protected घोषित कर बाहरी फंक्शन्स द्वारा इंस्टैंशिएट होने से रोका गया है; भीतर एक स्टैटिक क्लास पॉइंटर अद्वितीय इंस्टेंस सहेजता है, और इंस्टेंस का कार्यान्वयन एक public मेथड से होता है जो उस क्लास का अद्वितीय इंस्टेंस लौटाता है।
बेशक यह कोड केवल single-thread में उपयुक्त है; multi-thread में यह असुरक्षित है। मान लीजिए दो थ्रेड एक साथ पहली बार instance मेथड कॉल करते हैं और एक साथ पाते हैं कि p, nullptr है — तो दोनों थ्रेड एक साथ p के लिए एक इंस्टेंस बना देंगे, जो सिंगलटन के नियम के विरुद्ध है।
2.लेज़ी (lazy) तथा ईगर (eager)¶
सिंगलटन के दो कार्यान्वयन तरीके हैं:
- लेज़ी (lazy)
- क्लास इंस्टेंस की पहली आवश्यकता पड़ने पर ही इंस्टैंशिएट होता है; उपरोक्त उदाहरण यही लेज़ी कार्यान्वयन है।
- ईगर (eager)
- सिंगलटन क्लास की परिभाषा के समय ही इंस्टैंशिएट हो जाता है।
यहाँ ईगर का कार्यान्वयन भी देते हैं:
class singleton {
private:
singleton() {}
static singleton *p;
public:
static singleton *instance();
};
singleton *singleton::p = new singleton();
singleton* singleton::instance() {
return p;
}
बेशक यह thread-safe है; जिसे हम सामान्यतः thread-unsafe कहते हैं वह लेज़ी मोड है — आगे लेज़ी मोड के thread-safe कोड अनुकूलन की व्याख्या होगी।
3.मल्टी-थ्रेड में लॉक (lock)¶
C++ में लॉकिंग के लिए एक क्लास का कार्यान्वयन सिद्धांत RAII पर आधारित है, जिसमें unlock का हाथ से प्रबंधन नहीं करना पड़ता — वह है lock_guard; यहाँ उसी से लॉक किया गया है।
class singleton {
private:
singleton() {}
static singleton *p;
static mutex lock_;
public:
static singleton *instance();
};
singleton *singleton::p = nullptr;
singleton* singleton::instance() {
lock_guard<mutex> guard(lock_);
if (p == nullptr)
p = new singleton();
return p;
}
इस लिखावट में ऊपर जैसी स्थिति (दोनों थ्रेड p==nullptr के भीतर पहुँच जाएँ) नहीं आ सकती। जब थ्रेड A p = new Singleton() निष्पादित कर रहा हो और थ्रेड B उस समय instance() कॉल करे, तो वह अवश्य ही लॉक प्राप्त करने के स्थान पर अवरुद्ध होगा और थ्रेड A के निष्पादन समाप्त होकर लॉक मुक्त करने की प्रतीक्षा करेगा। इस प्रकार यह thread-safe है।
परंतु इस लिखावट का प्रदर्शन बहुत निम्न है, क्योंकि हर बार instance() कॉल पर लॉक लगता और छूटता है — जबकि यह चरण केवल पहली बार new Singleton() के समय ही आवश्यक है। जब तक p बन गया है, तब कितने भी थ्रेड एक साथ आएँ, केवल if (p == nullptr) से निर्णय पर्याप्त है (केवल पठन ऑपरेशन, लॉक की आवश्यकता नहीं) — कोई thread-safety समस्या नहीं; लॉक लगाने से उलटा प्रदर्शन की समस्या आ जाती है।
इसी से DCL की उत्पत्ति हुई।
4.डबल-चेक लॉक पैटर्न (DCLP)¶
उपरोक्त लिखावट में हर स्थिति में लॉक लगता और छूटता है, जबकि पठन ऑपरेशन में thread-safety की समस्या होती ही नहीं — अतः केवल पहली बार इंस्टेंस बनते समय लॉक चाहिए, बाद में नहीं। पहले DCLP का कार्यान्वयन देखिए:
singleton* singleton::instance() {
if(p == nullptr) { // 第一次检查
Lock lock;
if(p == nullptr){ // 第二次检查
p = new singleton;
}
}
return p;
}
उपरोक्त के आधार पर, हम डबल-चेक लॉक + स्वतः रीसाइकल (auto-recycle) लिख सकते हैं
class singleton {
private:
singleton() {}
static singleton *p;
static mutex lock_;
public:
static singleton *instance();
// 实现一个内嵌垃圾回收类
class CGarbo
{
public:
~CGarbo()
{
if(singleton::p)
delete singleton::p;
}
};
static CGarbo Garbo; // 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
};
singleton *singleton::p = nullptr;
singleton::CGarbo Garbo;
std::mutex singleton::lock_;
singleton* singleton::instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
if (p == nullptr)
p = new singleton();
}
return p;
}
DCLP का मर्म यह है कि instance के अधिकांश कॉल पाएँगे कि p गैर-शून्य है, इसलिए उसे आरंभीकृत करने का प्रयास भी नहीं करते। अतः DCLP लॉक प्राप्त करने से पहले जाँचता है कि p शून्य है या नहीं। केवल जब जाँच सफल हो (अर्थात् p अभी आरंभीकृत नहीं हुआ) तभी लॉक लेता है, फिर पुनः जाँचता है कि p अभी भी शून्य है या नहीं (इसी से नाम पड़ा — डबल-चेक लॉक)। दूसरी जाँच आवश्यक है, क्योंकि — जैसा अभी देखा — बहुत संभव है कि कोई अन्य थ्रेड पहली जाँच के बाद तथा लॉक प्राप्त करने से पहले ही p को आरंभीकृत कर दे।
देखने में उपरोक्त कोड बहुत सिद्ध लगता है, परंतु काफी समय बाद यह छिद्र पकड़ में आया; कारण है: मेमोरी पठन-लेखन का क्रम-विहीन निष्पादन (कंपाइलर समस्या)।
पुनः p को आरंभीकृत करने वाली पंक्ति पर विचार करें:
यह कथन तीन काम करता है:
singletonऑब्जेक्ट सहेजने योग्य मेमोरी आवंटित करना;- आवंटित मेमोरी में एक
singletonऑब्जेक्ट का निर्माण; pको उस आवंटित मेमोरी की ओर इशारा कराना।
सोचा जा सकता है कि ये तीनों चरण क्रम से चलते हैं, परंतु वास्तव में केवल इतना निश्चित है कि चरण 1 सबसे पहले होता है — चरण 2 तथा 3 का क्रम निश्चित नहीं। समस्या यहीं है।
- थ्रेड A, instance कॉल करता है, p की पहली जाँच निष्पादित करता है, लॉक प्राप्त करता है, 1, 3 क्रम से निष्पादित करता है, फिर निलंबित हो जाता है। इस समय p गैर-शून्य है, परंतु p की ओर इशारा की मेमोरी में अभी कोई Singleton ऑब्जेक्ट बना नहीं है।
- थ्रेड B, instance कॉल करता है, पाता है कि p गैर-शून्य है, और उसे instance के कॉलर को लौटा देता है। कॉलर पॉइंटर को dereference करके singleton लेता है — अरे, एक ऐसा ऑब्जेक्ट जो अभी बना ही नहीं! बग आ गया।
DCLP तभी भली-भाँति काम करता है जब चरण एक तथा दो, चरण तीन से पहले निष्पादित हों; परंतु C या C++ में यह प्रतिबंध व्यक्त करने का कोई तरीका नहीं था। यह जैसे DCLP के हृदय में घोंपी गई छुरी है: हमें निर्देशों के सापेक्ष क्रम पर प्रतिबंध परिभाषित करने हैं, परंतु हमारी भाषा ऐसा प्रतिबंध व्यक्त करने का कोई उपाय नहीं देती।
5.memory barrier निर्देश¶
C++11 में DCLP की यह समस्या हल हो गई।
क्योंकि नए C++11 ने नया मेमोरी मॉडल निर्धारित किया, जो गारंटी देता है कि उपरोक्त 3 चरणों के निष्पादन के दौरान थ्रेड-स्विच नहीं होगा — मानो यह आरंभीकरण प्रक्रिया "atomic" ऑपरेशन हो। इससे DCL पुनः सही ढंग से उपयोग हो सकता है; तथापि C++11 में multi-thread singleton लिखने की और भी संक्षिप्त विधि है, जिसका परिचय आगे है।
C++11 से पहले का समाधान barrier निर्देश था। सही निष्पादन के लिए चरण 2, 3 के बीच एक memory barrier लगाना पड़ता है — CPU को बाध्य करके कि 1, 2, 3 चरणों के क्रम में चले।
पहला कार्यान्वयन:
operator new + placement new पर आधारित — 1, 2, 3 निष्पादन क्रम के अनुसार क्रमवार कोड।
// method 1 operator new + placement new
singleton *instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
if (p == nullptr) {
singleton *tmp = static_cast<singleton *>(operator new(sizeof(singleton)));
new(tmp)singleton();
p = tmp;
}
}
return p;
}
दूसरा कार्यान्वयन:
ASM असेंबली निर्देश mfence को सीधे अंतःस्थापित (embed) करने पर आधारित; unix का barrier मैक्रो भी इसी निर्देश से बना है।
#define barrier() __asm__ volatile ("lwsync")
singleton *singleton::instance() {
if (p == nullptr) {
lock_guard<mutex> guard(lock_);
barrier();
if (p == nullptr) {
p = new singleton();
}
}
return p;
}
सामान्यतः CPU द्वारा दिया गया एक निर्देश कॉल किया जाता है; इस निर्देश का कार्य है — CPU को रोकना कि वह इस निर्देश से पहले के निर्देशों को इसके बाद न बदले; इसी निर्देश को प्रायः barrier कहा जाता है। उपरोक्त कोड में asm यह दर्शाता है कि यह एक असेंबली निर्देश है; volatile वैकल्पिक है — इसके उपयोग पर कंपाइलर को घोषित किया जाता है कि इस असेंबली निर्देश का अनुकूलन (optimize) करने की अनुमति नहीं। lwsync, POWERPC द्वारा दिया गया barrier निर्देश है।
6.स्टैटिक लोकल वेरिएबल¶
Scott Meyer ने “Effective C++” में सिंगलटन की एक संक्षिप्त लिखावट प्रस्तुत की
- single-thread में, सही।
- C++11 तथा बाद के संस्करणों (जैसे C++14) के multi-thread में, सही।
- C++11 से पहले के multi-thread में, हो सकता है सही न हो।
कारण यह है कि C++11 से पहले के स्टैंडर्ड में local static वेरिएबल का मेमोरी मॉडल निर्धारित नहीं था, इसलिए वह thread-safe नहीं था। परंतु C++11 में यह thread-safe है, क्योंकि नए C++ स्टैंडर्ड ने नियम दिया कि जब एक थ्रेड किसी वेरिएबल का आरंभीकरण कर रहा हो, तो अन्य थ्रेड्स को उस आरंभीकरण पूरा होने तक प्रतीक्षा करनी होगी।
उपरोक्त में उपयोग हुए मेमोरी ऑर्डर (memory order):
- memory_order_relaxed: शिथिल (relaxed) मेमोरी ऑर्डर — केवल इतना सुनिश्चित करता है कि atomic ऑब्जेक्ट पर ऑपरेशन atomic हो
- memory_order_acquire: acquire ऑपरेशन — किसी atomic ऑब्जेक्ट को पढ़ते समय, वर्तमान थ्रेड के उसके बाद के कोई भी पठन-लेखन इस ऑपरेशन से पहले reorder नहीं हो सकते, तथा उसी atomic ऑब्जेक्ट पर release से पहले अन्य थ्रेड्स के सभी मेमोरी-लेखन वर्तमान थ्रेड को दिखाई देते हैं
- memory_order_release: release ऑपरेशन — किसी atomic ऑब्जेक्ट पर लिखते समय, वर्तमान थ्रेड के उसके पहले के कोई भी पठन-लेखन इस ऑपरेशन के बाद reorder नहीं हो सकते, तथा वर्तमान थ्रेड के सभी मेमोरी-लेखन उसी atomic ऑब्जेक्ट को acquire करने वाले अन्य थ्रेड्स को दिखाई देते हैं
7.Atomic¶
C++11 से पहले के संस्करणों में, लॉक से thread-safe Singleton बनाने के अतिरिक्त, विभिन्न कंपाइलरों के अंतर्निहित atomic operation से भी बनाया जा सकता है।
java तथा c# ने reorder समस्या पकड़ने पर volatile कीवर्ड जोड़ दिया — p वेरिएबल घोषित करते समय volatile लगाना होता है; कंपाइलर देखकर समझ जाता है कि यहाँ reorder नहीं हो सकता (पहले मेमोरी आवंटित, फिर कंस्ट्रक्टर निष्पादित, सब पूरा होने पर असाइनमेंट)।
परंतु c++ स्टैंडर्ड में यह लंबे समय तक सुधार नहीं हुआ, इसलिए VC++ ने भी 2005 संस्करण में यह कीवर्ड जोड़ दिया — परंतु यह cross-platform नहीं है (केवल Microsoft प्लेटफ़ॉर्म)।
और c++ 11 संस्करण में, इन छिद्रों को जड़ से मिटाने के लिए multi-thread के उपयुक्त मेमोरी मॉडल पेश किया गया। अंततः ऐसी कार्यप्रणाली मिली जो cross-platform समाधान बनाने में सहायक है।
mutex singleton::lock_;
atomic<singleton *> singleton::p;
/*
* std::atomic_thread_fence(std::memory_order_acquire);
* std::atomic_thread_fence(std::memory_order_release);
* 这两句话可以保证他们之间的语句不会发生乱序执行。
*/
singleton *singleton::instance() {
singleton *tmp = p.load(memory_order_relaxed);
atomic_thread_fence(memory_order_acquire);
if (tmp == nullptr) {
lock_guard<mutex> guard(lock_);
tmp = p.load(memory_order_relaxed);
if (tmp == nullptr) {
tmp = new singleton();
atomic_thread_fence(memory_order_release);
p.store(tmp, memory_order_relaxed);
}
}
return p;
}
ध्यान देने योग्य है कि उपरोक्त कोड दो अपेक्षाकृत महत्वपूर्ण शब्द उपयोग करता है — acquire तथा release:
- acquire मेमोरी पर एक पठन ऑपरेशन है; वर्तमान थ्रेड के उसके बाद के कोई भी पठन-लेखन इस ऑपरेशन से पहले reorder नहीं हो सकते।
- release मेमोरी पर एक लेखन ऑपरेशन है; वर्तमान थ्रेड के उसके पहले के कोई भी पठन-लेखन इस ऑपरेशन के बाद reorder नहीं हो सकते।
acquire तथा release प्रायः जोड़े में आते हैं; उद्देश्य यह सुनिश्चित करना है कि यदि उसी atomic ऑब्जेक्ट पर release, acquire से पहले हुआ हो, तो release से पहले की गई मेमोरी-मरम्मतें (modifications) acquire के बाद के मेमोरी-पठन को संपूर्ण रूप से दिखाई दें।
8.pthread_once¶
यदि unix प्लेटफ़ॉर्म हो, तो atomic operation के अतिरिक्त, C++11 उपयोग न करते हुए भी pthread_once से Singleton बनाया जा सकता है।
प्रोटोटाइप इस प्रकार है:
कार्यान्वयन:
class singleton {
private:
singleton(); //私有构造函数,不允许使用者自己生成对象
singleton(const singleton &other);
//要写成静态方法的原因:类成员函数隐含传递this指针(第一个参数)
static void init() {
p = new singleton();
}
static pthread_once_t ponce_;
static singleton *p; //静态成员变量
public:
singleton *instance() {
// init函数只会执行一次
pthread_once(&ponce_, &singleton::init);
return p;
}
};
9.सारांश¶
इस लेख में कुछ सिंगलटन पैटर्न समझाए गए, साथ ही thread-safe सिंगलटन पैटर्न तथा C++11 उपयोग किए बिना बनाए जाने वाले कुछ thread-safe सिंगलटन: memory barrier, स्टैटिक लोकल वेरिएबल, pthread_once तरीका, C++11 का atomic कार्यान्वयन आदि।
अंत में ध्यान देने योग्य है कि उपरोक्त सिंगलटन क्लास के डिस्ट्रक्टर के लिए डबल-चेक लॉक पैटर्न + स्वतः रीसाइकल कार्यान्वयन देखें — क्लास में अवश्य ही एक स्टैटिक लोकल वेरिएबल घोषित करना चाहिए। स्टैटिक लोकल वेरिएबल को ग्लोबल वेरिएबल समझा जा सकता है — प्रोग्राम समाप्त होने पर उस स्टैटिक लोकल वेरिएबल का डिस्ट्रक्टर स्वतः कॉल होता है; इसीलिए क्लास में नेस्टेड क्लास घोषित-परिभाषित की जाती है, न कि सिंगलटन का डिस्ट्रक्टर सीधे लिखा जाता।
10.संदर्भ सामग्री¶
https://thorns.cn/2019/03/09/DCLP.html