智能指針shared_ptr的線程安全、互斥鎖

                                        智能指針和線程安全的問題


<1>  智能指針shared_ptr本身(底層實現原理是引用計數)是線程安全的

智能指針的引用計數在手段上使用了atomic原子操作,只要shared_ptr在拷貝或賦值時增加引用,析構時減少引用就可以了。首先原子是線程安全的,所有智能指針在多線程下引用計數也是安全的,也就是說智能指針在多線程下傳遞使用時引用計數是不會有線程安全問題的。

<2>  智能指針指向的對象的線程安全問題,智能指針沒有做任何保障

  • 遇到的問題

對於智能指針shared_ptr的引用計數本身是安全且無鎖的,但對象的讀寫則不是,因爲 shared_ptr 有兩個數據成員,一個是指向的對象的指針,還有一個就是我們上面看到的引用計數管理對象,當智能指針發生拷貝的時候,標準庫的實現是先拷貝智能指針,再拷貝引用計數對象(拷貝引用計數對象的時候,會使use_count加一),這兩個操作並不是原子操作,隱患就出現在這裏。兩個線程中智能指針的引用計數同時++或--,這個操作不是原子的,假設引用計數原來是1,++了兩次,可能還是2,這樣引用計數就錯亂了,違背了原子性。

 

  • 下多線程編程中的三個核心概念,可以作爲面試中原因分析的講解

 

        (1)原子性的舉例

這一點,跟數據庫事務的原子性概念差不多,即一個操作(有可能包含有多個子操作)要麼全部執行(生效),要麼全部都不執行(都不生效)。

關於原子性,一個非常經典的例子就是銀行轉賬問題:比如A和B同時向C轉賬10萬元。如果轉賬操作不具有原子性,A在向C轉賬時,讀取了C的餘額爲20萬,然後加上轉賬的10萬,計算出此時應該有30萬,但還未來及將30萬寫回C的賬戶,此時B的轉賬請求過來了,B發現C的餘額爲20萬,然後將其加10萬並寫回。然後A的轉賬操作繼續——將30萬寫回C的餘額。這種情況下C的最終餘額爲30萬,而非預期的40萬。

     (2)可見性的舉例

可見性是指,當多個線程併發訪問共享變量時,一個線程對共享變量的修改,其它線程能夠立即看到。可見性問題是好多人忽略或者理解錯誤的一點。

CPU從主內存中讀數據的效率相對來說不高,現在主流的計算機中,都有幾級緩存。每個線程讀取共享變量時,都會將該變量加載進其對應CPU的高速緩存裏,修改該變量後,CPU會立即更新該緩存,但並不一定會立即將其寫回主內存(實際上寫回主內存的時間不可預期)。此時其它線程(尤其是不在同一個CPU上執行的線程)訪問該變量時,從主內存中讀到的就是舊的數據,而非第一個線程更新後的數據。

這一點是操作系統或者說是硬件層面的機制,所以很多應用開發人員經常會忽略。

   (3)順序性舉例

順序性指的是,程序執行的順序按照代碼的先後順序執行。處理器爲了提高程序整體的執行效率,可能會對代碼進行優化,其中的一項優化方式就是調整代碼順序,按照更高效的順序執行代碼。講到這裏,有人要着急了——什麼,CPU不按照我的代碼順序執行代碼,那怎麼保證得到我們想要的效果呢?實際上,大家大可放心,CPU雖然並不保證完全按照代碼順序執行,但它會保證程序最終的執行結果和代碼順序執行時的結果一致。

  • 解決辦法——加入互斥鎖

使用互斥鎖對多線程讀寫同一個shared_ptr進行加鎖操作(多個線程訪問同一資源時,爲了保證數據的一致性,最簡單的方式就是使用 mutex(互斥鎖))

一旦一個線程獲得了鎖對象,那麼在臨界區時一直是受保護的,具體表現爲該線程一直佔着資源不放。

     臨界區的說明

 

         有時我們會遇到兩個進/線程共同使用同一個資源的情況,這個資源就稱爲臨界區。臨界區是指某一時間只能有一個線程執行的一個代碼段

  • 加入互斥鎖的代碼展示
    • 方法1:直接操作 mutex,即直接調用 mutex 的 lock / unlock 函數

 

#include <iostream>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  mutex.lock();

  int i = ++count;
  std::cout << "count == " << i << std::endl;

  // 前面代碼如有異常,unlock 就調不到了。
  mutex.unlock();
}

int main() {
  // 創建一組線程。
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  // 等待所有線程結束。
  threads.join_all();
  return 0;
}
  • 方法2:使用 lock_guard 自動加鎖、解鎖。原理是 RAII,和智能指針類似

C++利用了一個非常好的特性:當一個對象初始化時自動調用構造函數,當一個對象到達其作用域結尾時,自動調用析構函數。所以我們可以利用這個特性解決鎖的維護問題:把鎖封裝在對象內部!此時,在構造函數時獲得鎖,在語句返回前自動調用析構函數釋放鎖。其實這種做法有個專有的名稱,叫做RAII

 

#include <iostream>
#include <boost/thread/lock_guard.hpp>
#include <boost/thread/mutex.hpp>
#include <boost/thread/thread.hpp>

boost::mutex mutex;
int count = 0;

void Counter() {
  // lock_guard 在構造函數里加鎖,在析構函數裏解鎖。
  boost::lock_guard<boost::mutex> lock(mutex);

  int i = ++count;
  std::cout << "count == " << i << std::endl;
}

int main() {
  boost::thread_group threads;
  for (int i = 0; i < 4; ++i) {
    threads.create_thread(&Counter);
  }

  threads.join_all();
  return 0;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章