多線程單例模式


多線程單例模式

原文:https://blog.csdn.net/u011726005/article/details/82356538 

 

1. 餓漢模式
使用餓漢模式實現單例是十分簡單的,並且有效避免了線程安全問題,因為將該單例對象定義為static變量,程序啟動即將其構造完成了。代碼實現:

 

class Singleton {
public:
  static Singleton* GetInstance() {
    return singleton_;
  }

  static void DestreyInstance() {
    if (singleton_ != NULL) {
      delete singleton_;
    }
  }

private:
  // 防止外部構造。
  Singleton() = default;

  // 防止拷貝和賦值。
  Singleton& operator=(const Singleton&) = delete;
  Singleton(const Singleton& singleton2) = delete;

private:
  static Singleton* singleton_;
};

Singleton* Singleton::singleton_ = new Singleton;

int main() {
  Singleton* s1 = Singleton::GetInstance();
  std::cout << s1 << std::endl;

  Singleton* s2 = Singleton::GetInstance();
  std::cout << s2 << std::endl;

  Singleton.DestreyInstance();

  return 0;
}

 

2.懶漢模式
餓漢方式不論是否需要使用該對象都將其定義出來,可能浪費了內存,或者減慢了程序的啟動速度。所以使用懶漢模式進行優化,懶漢模式即延遲構造對象,在第一次使用該對象的時候才進行new該對象。

而懶漢模式會存在線程安全問題,最出名的解決方案就是Double-Checked Locking Pattern (DCLP)。使用兩次判斷來解決線程安全問題並且提高效率。代碼實現:

 

#include <iostream>
#include <mutex>

class Singleton {
public:
  static Singleton* GetInstance() {
    if (instance_ == nullptr) {
      std::lock_guard<std::mutex> lock(mutex_);
      if (instance_ == nullptr) {
        instance_ = new Singleton;
      }
    }

    return instance_;
  }

  ~Singleton() = default;

  // 釋放資源。
  void Destroy() {
    if (instance_ != nullptr) {
      delete instance_;
      instance_ = nullptr;
    }
  }

  void PrintAddress() const {
    std::cout << this << std::endl;
  }

private:
  Singleton() = default;

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static Singleton* instance_;
  static std::mutex mutex_;
};

Singleton* Singleton::instance_ = nullptr;
std::mutex Singleton::mutex_;

int main() {
  Singleton* s1 = Singleton::GetInstance();
  s1->PrintAddress();

  Singleton* s2 = Singleton::GetInstance();
  s2->PrintAddress();

  return 0;
}

 

3. 懶漢模式優化
上述代碼有一個問題,當程序使用完該單例,需要手動去調用Destroy()來釋放該單例管理的資源。如果不去手動釋放管理的資源(例如加載的文件句柄等),雖然程序結束會釋放這個單例對象的內存,但是並沒有調用其析構函數去關閉這些管理的資源句柄等。解決辦法就是將該管理的對象用智能指針管理。代碼如下:

#include <iostream>
#include <memory>
#include <mutex>

class Singleton {
public:
  static Singleton& GetInstance() {
    if (!instance_) {
      std::lock_guard<std::mutex> lock(mutex_);
      if (!instance_) {
        instance_.reset(new Singleton);
      }
    }

    return *instance_;
  }

  ~Singleton() = default;

  void PrintAddress() const {
    std::cout << this << std::endl;
  }

private:
  Singleton() = default;

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static std::unique_ptr<Singleton> instance_;
  static std::mutex mutex_;
};

std::unique_ptr<Singleton> Singleton::instance_;
std::mutex Singleton::mutex_;

int main() {
  Singleton& s1 = Singleton::GetInstance();
  s1.PrintAddress();

  Singleton& s2 = Singleton::GetInstance();
  s2.PrintAddress();

  return 0;
}

 

4. Double-Checked Locking Pattern存在的問題
Double-Checked Locking Pattern (DCLP)實際上也是存在嚴重的線程安全問題。Scott Meyers and 和Alexandrescu寫的一篇文章里面專門分析了這種解決方案的問題C++ and the Perils of Double-Checked Locking。文章截圖:

 

比如剛剛實現方式很容易發現其存在線程安全問題。

    if (instance_ == nullptr) {  \\ 語句1
      std::lock_guard<std::mutex> lock(mutex_);
      if (instance_ == nullptr) {
        instance_ = new Singleton;  \\ 語句2
      }
    }

線程安全問題產生的原因是多個線程同時讀或寫同一個變量時,會產生問題。
如上代碼,對於語句2是一個寫操作,我們用mutex來保護instance_這個變量。但是語句1是一個讀操作,if (instance_ == nullptr),這個語句是用來讀取instance_這個變量,而這個讀操作是沒有鎖的。所以在多線程情況下,這種寫法明顯存在線程安全問題。
《C++ and the Perils of Double-Checked Locking》這篇文章中提到:

instance_ = new Singleton;

 

這條語句實際上做了三件事,第一件事申請一塊內存,第二件事調用構造函數,第三件是將該內存地址賦給instance_。

但是不同的編譯器表現是不一樣的。可能先將該內存地址賦給instance_,然后再調用構造函數。這是線程A恰好申請完成內存,並且將內存地址賦給instance_,但是還沒調用構造函數的時候。線程B執行到語句1,判斷instance_此時不為空,則返回該變量,然后調用該對象的函數,但是該對象還沒有進行構造。

 

5. 使用std::call_once實現單例
在C++11中提供一種方法,使得函數可以線程安全的只調用一次。即使用 std::call_once 和 std::once_flag 。std::call_once是一種lazy load的很簡單易用的機制。實現代碼如下:

 

#include <iostream>
#include <memory>
#include <mutex>

class Singleton {
public:
  static Singleton& GetInstance() {
    static std::once_flag s_flag;
    std::call_once(s_flag, [&]() {
      instance_.reset(new Singleton);
    });

    return *instance_;
  }

  ~Singleton() = default;

  void PrintAddress() const {
    std::cout << this << std::endl;
  }

private:
  Singleton() = default;

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;

private:
  static std::unique_ptr<Singleton> instance_;
};

std::unique_ptr<Singleton> Singleton::instance_;

int main() {
  Singleton& s1 = Singleton::GetInstance();
  s1.PrintAddress();

  Singleton& s2 = Singleton::GetInstance();
  s2.PrintAddress();

  return 0;
}

 

6.使用局部靜態變量實現懶漢
使用C++局部靜態變量也可解決上述問題。

 

#include <iostream>

class Singleton {
public:
  static Singleton& GetInstance() {
    static Singleton intance;
    return intance;
  }

  ~Singleton() = default;

private:
  Singleton() = default;

  Singleton(const Singleton&) = delete;
  Singleton& operator=(const Singleton&) = delete;
};

int main() {
  Singleton& s1 = Singleton::GetInstance();
  std::cout << &s1 << std::endl;

  Singleton& s2 = Singleton::GetInstance();
  std::cout << &s2 << std::endl;

  return 0;
}

 

局部靜態變量可以延遲對象的構造,等到第一次調用時才進行構造。
C++11中靜態變量的初始化時線程安全的。通過調試,在進行局部靜態變量初始化的時候,確實會執行以下代碼來保證線程安全。

 

================= End

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM