1.概要
讀寫鎖的理解
讀的時候,只要是讀的線程都不受限制,但不能寫。
寫的時候,線程獨占,任何寫和讀的線程都不可以。
最初我以為,只有限制寫就可以了,讀完全不受現在,但是有可能讀到不完整的數據,比如寫一半的數據等等待,所以這就是對讀的那部分控制,共享鎖的價值。
2.std::shared_mutex 詳細說明
std::shared_mutex
是C++17中引入的一個同步原語,用于在多線程環境下提升對共享資源的訪問效率。與傳統的互斥鎖(如std::mutex
)不同,std::shared_mutex
允許多個線程以只讀模式共享對資源的訪問,但寫入操作必須獨占資源,以防止同時有其他線程對共享資源進行讀取或寫入。
關鍵特性
- 兩種訪問級別:
- 共享訪問(讀鎖):多個線程可以同時擁有讀鎖,允許它們并行讀取數據。
- 獨占訪問(寫鎖):只有一個線程可以擁有寫鎖,以此來修改數據。當一個線程擁有寫鎖時,其他線程無法獲得讀鎖或寫鎖。
- 適用場景:這種機制非常適合于多讀少寫的場景,因為它能夠最大化讀操作的并發性,同時確保寫操作的安全性。
使用方法
為了安全地使用std::shared_mutex
,你需要理解其兩種鎖模型:
- 獨占鎖(寫鎖):當你需要對共享數據進行寫入操作時,應該使用獨占鎖。獨占鎖確保了在執行寫入操作期間,沒有其它線程對數據進行讀或寫操作。
- 共享鎖(讀鎖):當多個線程需要讀取共享數據但不進行修改時,可以使用共享鎖。多個線程可以同時擁有讀鎖,但如果有線程正在使用獨占鎖(寫鎖),則其他線程無法獲得讀鎖或寫鎖。
示例
下面是一個簡單的示例,展示了如何使用std::shared_mutex
來同步對共享資源的訪問:
#include <iostream>
#include <thread>
#include <vector>
#include <shared_mutex> std::shared_mutex mtx;
int shared_data = 0; void reader(int id) { std::shared_lock<std::shared_mutex> lock(mtx); // 讀取共享數據 std::cout << "Reader " << id << " reads: " << shared_data << std::endl;
} void writer(int value) { std::unique_lock<std::shared_mutex> lock(mtx); // 修改共享數據 shared_data = value; std::cout << "Writer writes: " << shared_data << std::endl;
} int main() { std::vector<std::thread> threads; // 創建多個讀線程 for (int i = 0; i < 5; ++i) { threads.emplace_back(reader, i); } // 創建一個寫線程,稍后進行寫入操作 threads.emplace_back(writer, 42); // 等待所有線程完成 for (auto& t : threads) { t.join(); } return 0;
}
請注意,上述示例中的寫線程在創建后并不會立即執行,因為線程的調度順序是由操作系統決定的。為了更精確地控制讀寫操作的順序,你可能需要使用條件變量或其他同步機制。
3.共享鎖和沒有鎖有啥區別
C++中的共享鎖(如std::shared_lock
配合std::shared_mutex
使用)和沒有鎖(即不使用任何同步機制)之間存在顯著的區別。這些區別主要體現在對多線程環境中共享資源訪問的控制和安全性上。
沒有鎖
如果不使用任何鎖或同步機制,多個線程可以自由訪問和修改共享資源,這可能導致以下問題:
- 數據競爭(Data Race):當兩個或更多的線程并發訪問同一內存位置,且至少有一個線程是寫入操作,并且線程間沒有使用適當的同步來協調這些訪問時,就會發生數據競爭。數據競爭會導致未定義的行為,因為寫入操作的順序是不確定的。
- 不一致的狀態:由于線程之間沒有協調它們的訪問,一個線程可能看到由另一個線程部分修改的變量,導致程序處于不一致的狀態。
- 難以調試:沒有鎖的多線程程序通常很難調試,因為線程的執行順序是不確定的,并且程序的行為可能因環境而異(例如,CPU核心數量、線程調度策略等)。
共享鎖
共享鎖允許多個線程同時讀取共享資源,但阻止任何線程在已有線程讀取或寫入時寫入資源。使用共享鎖可以解決上述問題:
- 防止數據競爭:通過確保在讀取或寫入共享資源時只有一個線程可以執行,共享鎖防止了數據競爭。
- 保持數據一致性:由于寫入操作是獨占的,因此當一個線程正在寫入時,其他線程無法讀取或寫入,從而確保了數據的一致性。
- 提高并發性:與互斥鎖(如
std::mutex
)相比,共享鎖允許多個線程同時讀取共享資源,從而提高了并發性。這對于多讀少寫的場景特別有用。 - 簡化編程:使用共享鎖可以簡化多線程編程,因為程序員不需要手動協調線程之間的訪問,而是可以依靠鎖機制來自動處理。
總結
沒有鎖的多線程程序可能導致數據競爭、不一致的狀態和難以調試的問題。而共享鎖通過確保在讀取或寫入共享資源時只有一個線程可以執行,解決了這些問題,并提高了并發性。因此,在需要多線程訪問共享資源的場景中,使用適當的鎖或同步機制是非常重要的。
4.讀寫鎖和普通鎖的對比試驗
為了對比C++中的讀寫鎖(std::shared_mutex
?或其他第三方讀寫鎖實現)和普通鎖(如?std::mutex
)的性能,我們可以設計一個簡單的試驗。在這個試驗中,我們將創建多個線程,其中一些線程作為讀者(只讀取數據),而另一些線程作為寫者(修改數據)。
以下是一個簡化的示例,展示了如何使用std::mutex
和std::shared_mutex
來模擬這種場景,并進行性能對比。
使用?std::mutex
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
#include <chrono> std::mutex mtx;
int data = 0; void reader(int id) { for (int i = 0; i < 10000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 假設我們只是讀取數據 int value = data; // ...(省略其他讀取操作) }
} void writer(int id) { for (int i = 0; i < 1000; ++i) { std::lock_guard<std::mutex> lock(mtx); // 假設我們只是寫入數據 data = id; // ...(省略其他寫入操作) std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模擬耗時寫入 }
} int main() { auto start = std::chrono::high_resolution_clock::now(); std::vector<std::thread> threads; for (int i = 0; i < 5; ++i) { threads.emplace_back(reader, i); } for (int i = 0; i < 2; ++i) { threads.emplace_back(writer, i); } for (auto& t : threads) { t.join(); } auto end = std::chrono::high_resolution_clock::now(); std::chrono::duration<double> diff = end - start; std::cout << "Time taken with std::mutex: " << diff.count() << " s\n"; return 0;
}
使用?std::shared_mutex
#include <iostream>
#include <thread>
#include <vector>
#include <shared_mutex>
#include <chrono> std::shared_mutex smtx;
int data = 0; void reader(int id) { for (int i = 0; i < 10000; ++i) { std::shared_lock<std::shared_mutex> lock(smtx); // 假設我們只是讀取數據 int value = data; // ...(省略其他讀取操作) }
} void writer(int id) { for (int i = 0; i < 1000; ++i) { std::unique_lock<std::shared_mutex> lock(smtx, std::defer_lock); lock.lock(); // 使用unique_lock來鎖定,因為shared_lock不允許升級鎖 // 假設我們只是寫入數據 data = id; // ...(省略其他寫入操作) std::this_thread::sleep_for(std::chrono::milliseconds(1)); // 模擬耗時寫入 }
} // main函數與上述類似,只是鎖的類型改變了
注意:在上面的std::shared_mutex
示例中,我們使用std::unique_lock
來鎖定寫操作,因為std::shared_lock
不允許鎖升級(即,從共享鎖升級到獨占鎖)。但是,為了保持示例的簡單性,我們仍然使用std::shared_mutex
來展示讀寫鎖的基本概念。
性能對比
要獲得準確的性能對比,您應該多次運行這兩個程序,并可能使用更復雜的基準測試工具來測量CPU時間、線程爭用等。但是,從概念上講,當讀者數量遠多于寫者時,使用std::shared_mutex
(或類似的讀寫鎖)應該比使用std::mutex
具有更好的性能,因為多個讀者可以同時訪問數據,而不會被彼此阻塞。
5.defer_lock 啥意思
在C++中,std::defer_lock
是一個標簽類型,它用于與互斥體(std::mutex
、std::timed_mutex
、std::recursive_mutex
、std::recursive_timed_mutex
、std::shared_mutex
等)的std::unique_lock
或std::shared_lock
結合使用,以指示在構造時不立即鎖定互斥體。
具體來說,當你使用std::unique_lock
或std::shared_lock
時,默認情況下,互斥體會在std::unique_lock
或std::shared_lock
的構造函數中被鎖定。但是,如果你使用std::defer_lock
標簽,則互斥體不會在構造函數中立即鎖定,你可以稍后通過調用lock()
、try_lock()
或unlock()
等方法來控制鎖定和解鎖。
這在某些情況下可能是有用的,例如:
- 條件變量:當你使用條件變量(如
std::condition_variable
)時,你可能希望在等待條件變量之前不鎖定互斥體,因為條件變量的wait()
方法會自動解鎖互斥體并在條件滿足時重新鎖定它。在這種情況下,你可以使用std::defer_lock
來構造一個未鎖定的std::unique_lock
,然后調用wait()
。 - 復雜的同步邏輯:在更復雜的同步場景中,你可能希望根據某些條件來決定是否立即鎖定互斥體。使用
std::defer_lock
可以讓你延遲這個決策。
下面是一個使用std::defer_lock
的示例:
#include <iostream>
#include <mutex>
#include <thread>
#include <chrono> std::mutex mtx; void print_block(int n, char c) { std::unique_lock<std::mutex> lck(mtx, std::defer_lock); // 在這里,mtx還沒有被鎖定 // 模擬一些工作 std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 現在我們決定鎖定互斥體 lck.lock(); for (int i = 0; i < n; ++i) { std::cout << c; } std::cout << '\n'; // 解鎖(但在這個例子中,當lck離開作用域時會自動解鎖) lck.unlock(); // 或者簡單地讓lck離開作用域,它會在析構時自動解鎖
} int main() { std::thread th1(print_block, 50, '*'); std::thread th2(print_block, 50, '$'); th1.join(); th2.join(); return 0;
}
注意,在這個示例中,盡管我們使用了std::defer_lock
,但實際上我們還是在lck.lock()
調用時立即鎖定了互斥體。但是,這個例子展示了如何使用std::defer_lock
來延遲鎖定決策。在更復雜的場景中,你可能會根據某些條件來決定是否調用lock()
。