1. 程式人生 > 實用技巧 >NOIP普及 導彈攔截 題解

NOIP普及 導彈攔截 題解

單例模式可以說得上最簡單的模式,我記得我本科畢業時找工作,很多公司問到設計模式都是談單例模式,mvc之類的。
單例模式說簡單也簡單,說可研究點也很多。
怎麼說呢?有時候我們想儲存一個全域性只需要一個變數或者物件例項的時候(比如這個物件建立很複雜但是實際使用物件例項中狀態基本不會變化),會怎麼做呢?
可能首先想到的是全域性變數,但是從第一門 程式設計課開始,就有人時不時提醒自己,不要要使用全域性變數,不要使用全域性變數,不要使用全域性變數。
那麼在面嚮物件語言中應該怎樣做到一個物件例項只有一個訪問點呢?

其中關鍵的做法就是將建構函式與複製建構函式私有化,就可以達到這樣的目的。

Singleton執行緒不安全版本

class Singleton {
private:
 Singleton();
 Singleton(const Single& singleton);
 static Singleton* GetInstance() {
   if (instance_ == nullptr) {
    instance_ = new Singleton();
    return instance_;
   }
 }
private:
 static Singleton* instance_;
};  // class Singleton

其實哈我覺得如果不是在多執行緒使用,其實這樣寫的已經夠了。但是可能出現多執行緒下有多個執行緒同時通過判斷if(instance_ == nullptr),從而導致單例失效。

Singleton多執行緒加鎖

#include <mutex>

class Singleton {
private:
 Singleton();
 Singleton(const Single& singleton);
 static Singleton* GetInstance() {
   std::lock_guard<std::mutex> guard(single_mutex_);
   if (instance_ == nullptr) {
    instance_ = new Singleton();
    return instance_;
   }
 }
private:
 static Singleton* instance_;
 static std::mutex single_mtx_;
};  // class Singleton

這個版本呢,大家都說加鎖的資源消耗大,例如其實這個過程中其實就是“寫”呼叫建構函式的時候需要加鎖,其他的時候都是“讀”操作,其實是不用加鎖的。而且加鎖在高併發的時候很很費時間的。
但是哈,其實如果沒有高併發,這樣寫也沒問題。
上面遇到什麼問題呢,“高併發的時候,不合適的加鎖”,所以說加鎖是一個技術活,有時候正確的地方加鎖,會明顯的提高效能。

雙檢鎖

#include <mutex>

class Singleton {
private:
 Singleton();
 Singleton(const Single& singleton);
 static Singleton* GetInstance() {
   if (instance_ == nullptr) {
      std::lock_guard<std::mutex> guard(single_mutex_);
      if (instance_ == nullptr) {
        instance_ = new Singleton();
      }
   }
      return instance_;
 }
private:
 static Singleton* instance_;
 static std::mutex single_mtx_;
};  // class Singleton

"雙檢鎖"顧名思義就是兩次檢查 if判斷,一次鎖 ,或者我們可以讀作”檢鎖檢“。
這個優化在哪裡呢?就是如果已經建立了,直接只進行一次if判斷就可以了,同時避免了多執行緒同時進入第一個if判斷的情況。

本來覺得這個已經可以了,但是呢,實際生成中又檢查出了新的問題。編譯優化導致的reorder問題。
reorder是哥啥問題呢。我們看下這段程式碼

instance_ = new Singleton();
return instance_;

我們預想的指令呼叫順序是 先申請例項的記憶體空間,然後呼叫建構函式,最後返回申請的地址。
但是編譯器做了優化順序變成了 先申請例項的記憶體空間,然後返回申請地址,最後呼叫建構函式。
這兩種順序會導致什麼問題呢?
假設有非常多的執行緒呼叫GetInstance其中有一個進入第2個if,當它申請好地址後,直接先返回地址給instance_,那麼其他的執行緒看到
instance_不為空,直接就使用instance_,而instance_並沒有初始化,這不就出錯了嗎。

解決方案很簡單,原子化和設定memory order
如何理解 C++11 的六種 memory order?
原子化很簡單,atomic就是一個操作不能再細分。使用資料庫的人應該明白這個基礎概念。
c++11中就是 atomic
那麼我們寫程式碼吧

std::atomic<Singleton*> Single::instance_;
std::mutex Singleton::gingle_mtx_;
Singleton* Singleton::GetInstance() {
  Singleton* tmp = instance_.load(std::memory_order_relaxed);
  std::atomic_thread_fence(std::memory_order_acquire);
  if (tmp == nullptr) {
    std::lock_guard<std::mutex>  guard(single_mtx_);
    tmp = instance_.load(std::memory_order_relaxed);
    if (tmp == nullptr) {
      tmp = new Singleton();
      std::atomic_thread_fence(std::memory_order_release);
      instance_.store(tmp, std::memory_order_relaxed);
    }
  }
  return tmp;
}

std::atomic_thread_fence解釋

總結

  • Singleton模式中例項的構造器可以設定為protected以允許子類派生。
  • 一般不使用拷貝建構函式和原型模式。