विषय पर बढ़ें

अंतिम अद्यतन:2026-10-13

C++ सिंगलटन पैटर्न

0. प्रस्तावना

इंटरव्यू में डिज़ाइन पैटर्न के विषय से शायद कोई नहीं बच पाता। यह खंड इंटरव्यू में सबसे अधिक पूछे जाने वाले डिज़ाइन पैटर्न (सिंगलटन पैटर्न) पर चर्चा करता है — इसके वर्गीकरण, थ्रेड सुरक्षा, तथा C++11 मानक पर आधारित न होने और आधारित होने, दोनों दृष्टिकोणों से थ्रेड-सुरक्षित सिंगलटन के समाधान। यह लेख ध्यान से पढ़ लें, फिर भविष्य के इंटरव्यू में चिंता करने की ज़रूरत नहीं होगी।

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 विधि द्वारा किया जाता है, जो उस क्लास का एकमात्र इंस्टेंस लौटाती है।

बेशक यह कोड केवल सिंगल-थ्रेड के लिए उपयुक्त है; कई थ्रेड होने पर यह सुरक्षित नहीं है। सोचिए कि दो थ्रेड एक साथ पहली बार instance विधि को कॉल करते हैं और दोनों को p nullptr दिखता है: तब दोनों थ्रेड एक साथ p के लिए इंस्टेंस बना देंगे, जो सिंगलटन के नियम का उल्लंघन करता है।

2. लेज़ी और ईगर इनिशियलाइज़ेशन

सिंगलटन दो तरीकों से कार्यान्वित किया जाता है:

  • लेज़ी (आलस्य)
  • क्लास इंस्टेंस के पहली बार उपयोग होने पर ही इंस्टेंटिएट होता है; ऊपर दिया गया कोड लेज़ी कार्यान्वयन है।
  • ईगर (उत्सुक)
  • सिंगलटन क्लास की परिभाषा के समय ही इंस्टेंटिएट हो जाता है।

यहाँ ईगर कार्यान्वयन भी दिया गया है:

class singleton {
private:
    singleton() {}
    static singleton *p;
public:
    static singleton *instance();
};

singleton *singleton::p = new singleton();
singleton* singleton::instance() {
    return p;
}

बेशक यह थ्रेड-सुरक्षित है। सामान्यतः हम जिसे थ्रेड-असुरक्षित बताते हैं वह लेज़ी शैली है; नीचे हम लेज़ी पैटर्न के थ्रेड-सुरक्षित कोड अनुकूलन की चर्चा करेंगे।

3. कई थ्रेड के लिए लॉकिंग

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() को कॉल करे, तो B अवश्य ही लॉक प्राप्त करने की जगह अवरुद्ध होगा और थ्रेड A के निष्पादन समाप्त होकर लॉक मुक्त होने की प्रतीक्षा करेगा। इस प्रकार यह थ्रेड-सुरक्षित है।

लेकिन इस शैली का प्रदर्शन बहुत ख़राब है, क्योंकि instance() के प्रत्येक कॉल पर लॉक प्राप्त और मुक्त होता है, जबकि यह चरण केवल पहले new Singleton() के लिए आवश्यक है। एक बार p बन जाने के बाद, चाहे कितने ही थ्रेड एक साथ पहुँचें, if (p == nullptr) से निर्णय करना पर्याप्त है (यह केवल पढ़ने का ऑपरेशन है, लॉक की ज़रूरत नहीं); थ्रेड-सुरक्षा की कोई समस्या नहीं है, और लॉक लगाने से इसके बदले प्रदर्शन की समस्या आ जाती है।

इसी से DCL का जन्म हुआ।

4. डबल-चेक्ड लॉकिंग पैटर्न

ऊपर की शैली हर स्थिति में लॉक प्राप्त करके मुक्त करती है, जबकि पढ़ने के ऑपरेशन में थ्रेड-सुरक्षा की कोई समस्या नहीं होती; इसलिए लॉक केवल पहली बार इंस्टेंस बनते समय चाहिए, बाद में कभी नहीं। पहले DCLP का कार्यान्वयन देखते हैं:

singleton* singleton::instance() {
    if(p == nullptr) {  // 第一次检查
        Lock lock;
        if(p == nullptr){ // 第二次检查
            p = new singleton;
        }
    }
    return p;
}

उपरोक्त आधार पर, हम डबल-चेक्ड लॉकिंग + स्वचालित रीकवरी लिख सकते हैं

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 को इनिशियलाइज़ करती है:

p = new singleton;

यह कथन तीन चीज़ें घटित करता है:

  1. singleton ऑब्जेक्ट को संग्रहीत करने में सक्षम मेमोरी आवंटित करना;
  2. आवंटित मेमोरी में एक singleton ऑब्जेक्ट का निर्माण करना;
  3. p को उस आवंटित मेमोरी की ओर इशारा कराना।

हो सकता है कि आप मानें कि ये तीनों चरण क्रम से निष्पादित होते हैं, लेकिन वास्तव में केवल इतना निश्चित है कि चरण 1 सबसे पहले निष्पादित होता है; चरण 2 और 3 के बारे में निश्चितता नहीं है। समस्या यहीं है।

  • थ्रेड A instance को कॉल करता है, p का पहला परीक्षण निष्पादित करता है, लॉक प्राप्त करता है, चरण 1 और 3 निष्पादित करता है, फिर निलंबित हो जाता है। इस समय p गैर-शून्य है, लेकिन p द्वारा इंगित मेमोरी में अभी तक कोई Singleton ऑब्जेक्ट निर्मित नहीं हुआ है।
  • थ्रेड B instance को कॉल करता है, p को गैर-शून्य पाता है, और उसे instance के कॉलर को लौटा देता है। कॉलर सिंगलटन प्राप्त करने के लिए पॉइंटर को डिरेफ़रेंस करता है — अरे, एक ऐसा ऑब्जेक्ट जो अभी निर्मित ही नहीं हुआ। बग यहीं पैदा हो जाता है।

DCLP तभी ठीक से काम करता है जब चरण एक और दो, चरण तीन से पहले निष्पादित हों, लेकिन C या C++ में इस नियम को व्यक्त करने का कोई तरीका नहीं है। यह DCLP के दिल में घोंपी गई छुरी की तरह है: हमें निर्देशों के सापेक्ष क्रम पर प्रतिबंध परिभाषित करने की आवश्यकता है, लेकिन हमारी भाषा ऐसे प्रतिबंध व्यक्त करने का कोई तरीका नहीं देती।

5. memory barrier निर्देश

DCLP की समस्या C++11 में हल हो गई।

चूँकि नए C++11 ने नया मेमोरी मॉडल निर्धारित किया, यह गारंटी दी जाती है कि उपरोक्त 3 चरणों के निष्पादन के दौरान थ्रेड स्विच नहीं होता; यह इनिशियलाइज़ेशन प्रक्रिया "परमाणु" (atomic) ऑपरेशन के समान है, और DCL को दोबारा सही ढंग से उपयोग किया जा सकता है। हालाँकि, C++11 के अंतर्गत मल्टी-थ्रेड सिंगलटन लिखने की और भी संक्षिप्त विधि मौजूद है, जिसे आगे प्रस्तुत करेंगे।

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 को सीधे एम्बेड करने पर आधारित; 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 को इस निर्देश से पहले वाले निर्देशों को इसके बाद विनिमय (swap) करने से रोकना है; इसीलिए इस निर्देश को सामान्यतः barrier कहा जाता है। ऊपर के कोड में asm बताता है कि यह एक असेंबली निर्देश है; volatile वैकल्पिक है — यदि इसका उपयोग किया जाए, तो इसका अर्थ है कि कंपाइलर को घोषित किया जा रहा है कि इस असेंबली निर्देश का अनुकूलन करने की अनुमति नहीं है। lwsync POWERPC द्वारा प्रदान किया गया barrier निर्देश है।

6. स्टैटिक लोकल वेरिएबल

स्कॉट मेयर्स ने «Effective C++» में सिंगलटन लिखने की एक संक्षिप्त शैली प्रस्तावित की

singleton &singleton::instance() {
    static singleton p;
    return p;
}
  • सिंगल-थ्रेड में, सही।
  • C++11 और बाद के संस्करणों (जैसे C++14) की मल्टी-थ्रेड स्थिति में, सही।
  • C++11 से पहले की मल्टी-थ्रेड स्थिति में, ज़रूरी नहीं कि सही हो।

कारण यह है कि C++11 से पहले के मानकों में लोकल static वेरिएबल के लिए मेमोरी मॉडल निर्धारित नहीं था, इसलिए वह थ्रेड-सुरक्षित नहीं था। लेकिन C++11 में यह थ्रेड-सुरक्षित है, क्योंकि नए C++ मानक ने नियम दिया है कि जब एक थ्रेड किसी वेरिएबल को इनिशियलाइज़ कर रहा हो, तो अन्य थ्रेडों को उस इनिशियलाइज़ेशन के पूरा होने की प्रतीक्षा करनी होगी, तभी वे उस तक पहुँच सकते हैं।

ऊपर उपयोग किए गए मेमोरी क्रम:

  • memory_order_relaxed: शिथिल मेमोरी क्रम, केवल यह सुनिश्चित करता है कि परमाणु ऑब्जेक्ट पर ऑपरेशन परमाणु हों
  • memory_order_acquire: प्राप्ति (acquire) ऑपरेशन; किसी परमाणु ऑब्जेक्ट को पढ़ते समय, वर्तमान थ्रेड का कोई भी बाद का पढ़ना-लिखना इस ऑपरेशन से पहले पुनर्क्रमित नहीं किया जा सकता, और अन्य थ्रेडों द्वारा उसी परमाणु ऑब्जेक्ट की रिहाई (release) से पहले की सभी मेमोरी लेखन वर्तमान थ्रेड को दिखाई देती हैं
  • memory_order_release: रिहाई (release) ऑपरेशन; किसी परमाणु ऑब्जेक्ट पर लिखते समय, वर्तमान थ्रेड का कोई भी पहले का पढ़ना-लिखना इस ऑपरेशन के बाद पुनर्क्रमित नहीं किया जा सकता, और वर्तमान थ्रेड की सभी मेमोरी लेखन उसी परमाणु ऑब्जेक्ट को प्राप्त (acquire) करने वाले अन्य थ्रेडों को दिखाई देती हैं

7. Atomic

C++11 से पहले के संस्करणों में, लॉक से थ्रेड-सुरक्षित Singleton बनाने के अलावा, विभिन्न कंपाइलरों में अंतर्निहित atomic operation का भी उपयोग किया जा सकता है।

java और c# ने पुनर्क्रमण की समस्या की खोज के बाद एक कीवर्ड volatile जोड़ा: p वेरिएबल की घोषणा करते समय volatile क्वालिफ़ायर लगाना ज़रूरी है, और कंपाइलर इसे देखकर समझ जाता है कि यहाँ पुनर्क्रमण (reorder) नहीं हो सकता (पहले मेमोरी आवंटित हो, फिर कंस्ट्रक्टर निष्पादित हो, और सब पूरा होने के बाद ही असाइनमेंट हो)।

लेकिन c++ मानक में यह लंबे समय तक सुधारा नहीं गया, इसलिए VC++ ने अपने 2005 संस्करण में यह कीवर्ड जोड़ा, पर यह क्रॉस-प्लेटफ़ॉर्म नहीं है (केवल माइक्रोसॉफ़्ट प्लेटफ़ॉर्म का समर्थन)।

फिर c++ 11 संस्करण में, इन खामियों को जड़ से मिटाने के लिए मल्टी-थ्रेड के उपयुक्त मेमोरी मॉडल पेश किया गया। अंततः ऐसा तंत्र मिला जो हमें क्रॉस-प्लेटफ़ॉर्म समाधान कार्यान्वित करने में मदद करता है।

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 और release प्रायः जोड़ी में आते हैं, जिसका उद्देश्य यह सुनिश्चित करना है कि यदि उसी परमाणु ऑब्जेक्ट पर release, acquire से पहले होता है, तो release से पहले हुए मेमोरी परिवर्तन acquire के बाद के मेमोरी पठन को पूरी तरह दिखाई दें।

8. pthread_once

unix प्लेटफ़ॉर्म पर, atomic operation के अलावा, यदि C++11 का उपयोग नहीं कर रहे हैं, तो pthread_once से भी Singleton कार्यान्वित किया जा सकता है।

प्रोटोटाइप इस प्रकार है:

int pthread_once(pthread_once_t once_control, void (init_routine) (void));

कार्यान्वयन:

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. सारांश

इस लेख में कई सिंगलटन पैटर्न समझाए गए, साथ ही थ्रेड-सुरक्षित सिंगलटन पैटर्न और C++11 का उपयोग किए बिना थ्रेड-सुरक्षित सिंगलटन के कई कार्यान्वयन: memory barrier, स्टैटिक लोकल वेरिएबल, pthread_once विधि, C++11 का atomic कार्यान्वयन आदि।

अंत में ध्यान देने योग्य है कि उपरोक्त सिंगलटन क्लासों के डिस्ट्रक्टर के लिए कृपया डबल-चेक्ड लॉकिंग + स्वचालित रीकवरी कार्यान्वयन देखें: क्लास के भीतर एक स्टैटिक लोकल वेरिएबल घोषित करना अनिवार्य है। स्टैटिक लोकल वेरिएबल को ग्लोबल वेरिएबल की तरह समझा जा सकता है; प्रोग्राम समाप्त होने पर उस स्टैटिक लोकल वेरिएबल का डिस्ट्रक्टर स्वचालित रूप से कॉल होता है। इसीलिए क्लास के भीतर नेस्टेड क्लास घोषित और परिभाषित की जाती है, न कि सिंगलटन का डिस्ट्रक्टर सीधे लिखा जाता है।

10. संदर्भ

https://thorns.cn/2019/03/09/DCLP.html

https://www.cnblogs.com/liyuan989/p/4264889.html

https://segmentfault.com/a/1190000015950693

पुनः प्रयोग योग्य थ्रेड-सुरक्षित सिंगलटन टेम्पलेट क्लास

issue #303 की माँग के जवाब में: boost::container::singleton_default के विचार को देखते हुए, एक CRTP टेम्पलेट बेस क्लास किसी भी क्लास को एक पंक्ति में थ्रेड-सुरक्षित सिंगलटन बना सकती है (पूरा कोड singleton_default.hpp में देखें, जिसमें कंकरेंसी टेस्ट शामिल हैं):

template <class T>
class SingletonDefault {
public:
    static T& instance() {
        static T inst;   // C++11 起局部静态变量初始化线程安全
        return inst;
    }
    SingletonDefault(const SingletonDefault&) = delete;
    SingletonDefault& operator=(const SingletonDefault&) = delete;
protected:
    SingletonDefault() = default;
};

// 用法:继承即可
class MyConfig : public SingletonDefault<MyConfig> {
    friend class SingletonDefault<MyConfig>;
private:
    MyConfig() = default;   // 构造私有,交由基类创建
public:
    int value = 42;
};

// MyConfig::instance().value

टिप्पणियाँ