std::thread線程庫詳解(2)


目錄

簡介

上一篇博文中,介紹了一下如何創建一個線程,分別是std::threadstd::jthread (C++20)。這兩種方法相似,std::jthread相對來說,更加方便一些,具體可以再看看原來的博文,std::thread線程詳解(1)

這一次,我將介紹一下,多線程的鎖。鎖在多線程中是使用非常廣泛的。是多線程中最常見的同步方式。主要介紹的鎖有mutexrecursive_mutexshared_mutex

最基本的鎖 std::mutex

使用

std::mutex是最基本的鎖,也是最常見的鎖。它提供了最基本的多線程編程同步方法。

using namespace std::chrono_literals;

std::mutex g_mutex;

void thread_func() {
    g_mutex.lock();
    std::cout << "Thread out 1: " << std::this_thread::get_id() << std::endl;;
    std::this_thread::sleep_for(1s);
    std::cout << "Thread out 2: " << std::this_thread::get_id() << std::endl;;
    g_mutex.unlock();
}

int main() {
    std::cout << "Mutex Test." << std::endl;
    std::thread thread1(thread_func);
    std::thread thread2(thread_func);
    thread1.join();
    thread2.join();
    return 0;
}

以上示例中,只有一個線程函數thread_func,它的工作很簡單:

首先對g_mutex加鎖,然后輸出一段字符串,接着休眠1s,輸出第二段字符串,最后對g_mutex進行解鎖。

輸出結果如下:

mutex輸出

鎖的本質是解決多線程對同一資源競爭讀寫的問題。這里我們的資源是標准輸出std::cout。鎖的存在讓輸出有序,可預測了。

方法和屬性

  • lock() 為對象加鎖,如果已經被鎖了,則阻塞線程;
  • try_lock() 嘗試加鎖,如果已經被加鎖,則返回false,否則將對其進行加鎖並返回true;
  • unlock() 為對象解鎖,通常和加鎖(lock()try_lock())成對出現;
  • native_handle() 返回鎖的POSIX標准對象。

遞歸鎖 std::recursive_mutex

std::recursive_mutex是一個遞歸鎖,方法和使用都和std::mutex類似。唯一的不同是,std::mutex在同一時間,只允許加鎖一次,而std::revursive_mutex允許同一線程下進行多次加鎖。如:

// 定義遞歸鎖
std::recursive_mutex g_mutex;

// 線程函數
void thread_func(int thread_id, int time) {
    g_mutex.lock();
    std::cout << "Thread " << thread_id << ": " << time << std::endl;
    if (time != 0) thread_func(thread_id, time - 1);
    g_mutex.unlock();
}

// 初始化線程
std::thread thread1(thread_func, 1, 3);
std::thread thread2(thread_func, 2, 4);

這一次的方法和之前的略有不同,為了更加直觀的觀察不同的線程,這次是在輸入的時候輸入一個標志來區分不同的線程。可以清楚的看到,這是一個遞歸函數,每次調用的時候都將time減少1,直到其變為0。需要注意的是,在遞歸的時候並沒有釋放鎖,而是直接進入,因此在第二層遍歷的時候,又會對g_mutex進行一次加鎖,如果是普通的鎖,次數將會阻塞進程,變成死鎖。但是此時使用的是遞歸鎖,它允許在同一個線程,多次加鎖,因此這個程序可以成功運行,並獲得輸出。

遞歸鎖輸出

遞歸鎖的方法和普通鎖的方法類似。

共享鎖 std::shared_mutex (C++17)

std::shared_mutex在C++14已經存在了,但是在C++14中的std::shared_mutex是帶timing的版本的讀寫鎖(也就是說,C++14中的std::shared_mutex等於C++17中的std::shared_timed_mutex)。讀寫鎖有兩種加鎖的方式,一種是shared_lock(),另一種lock()shared_lock是讀模式,而lock是寫模式。讀寫鎖允許多個讀加鎖,而寫加鎖和其他所有加鎖互斥。即同一時間下:

  • 允許多個線程同時讀;
  • 只允許一個線程寫;
  • 寫的時候不允許讀,讀的時候不允許寫。

示例:

// 共享鎖
std::shared_mutex g_mutex;

// 讀線程 1
void thread_read_1_func(int thread_id) {
    // 第一個獲取讀權限
    g_mutex.lock_shared();
    std::cout << "Read thread " << thread_id << " out 1." << std::endl;
    // 睡眠2s,等待讀線程2,獲取讀權限,確認可以多個線程進行讀加鎖
    std::this_thread::sleep_for(2s);
    std::cout << "Read thread " << thread_id << " out 2." << std::endl;
    // 解鎖讀
    g_mutex.unlock_shared();
}

void thread_read_2_func(int thread_id) {
    // 睡眠500ms,確保讀線程1先獲取鎖
    std::this_thread::sleep_for(500ms);
    g_mutex.lock_shared();
    std::cout << "Read thread " << thread_id << " out 1."  << std::endl;
    std::this_thread::sleep_for(3s);
    std::cout << "Read thread " << thread_id << " out 2."  << std::endl;
    g_mutex.unlock_shared();
}

void thread_write_1_func(int thread_id) {
    // 確保讀線程先獲得鎖,確認讀寫互斥
    std::this_thread::sleep_for(300ms);
    g_mutex.lock();
    std::cout << "Write thread " << thread_id << " out 1."  << std::endl;
    g_mutex.unlock();
}

其輸出為:

讀寫鎖輸出

帶超時的鎖

上面介紹的所有的鎖,都帶有超時版本。即timed_mutexrecursive_timed_mutexshared_timed_mutex。他們使用時,和普通版本類似,不過try_lock方法多了兩個超時的版本try_lock_fortry_lock_until。調用這一函數時,如果鎖已經被獲取了,線程將會阻塞一段時間,如果這一段時間內,獲取到了鎖則返回true,否則返回false

這里我們只介紹timed_mutex,其他的類似。

void thread_func(int thread_id) {
    if (!g_mutex.try_lock_for(0.5s)) return;
    std::cout << "Thread out 1: " << thread_id << std::endl;;
    std::this_thread::sleep_for(1s);
    std::cout << "Thread out 2: " << thread_id << std::endl;;
    g_mutex.unlock();
    g_mutex.native_handle();
}

其輸出為:

超時鎖輸出

可以看到,這里只有一個線程有輸出,另一個線程,在等待0.5s后直接退出了(沒有獲取到鎖)。

總結

本文主要介紹了三種不同的鎖,普通鎖,遞歸鎖,讀寫鎖。三個鎖有着不一樣的使用方法,但是可以確定的是,過多的使用鎖,會導致程序中的串行部分過多,並行效果不好。因此對於鎖的使用,需要盡量的克制,盡量的合理。

下一篇文章將介紹鎖的管理。

博客原文:https://www.cnblogs.com/ink19/p/std_thread-2.html


免責聲明!

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



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