c++11新特性之線程相關全部知識點

c++11關於併發引入了好多好東西,這裏按照以下順序介紹:html

  • std::thread相關
  • std::mutex相關
  • std::lock相關
  • std::atomic相關
  • std::call_once相關
  • volatile相關
  • std::condition_variable相關
  • std::future相關
  • async相關

std::thread相關

c++11以前你可能使用pthread_xxx來建立線程,繁瑣且不易讀,c++11引入了std::thread來建立線程,支持對線程join或者detach。直接看代碼:ios

#include <iostream>
#include <thread>

using namespace std;

int main() {
    auto func = []() {
        for (int i = 0; i < 10; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread t(func);
    if (t.joinable()) {
        t.detach();
    }
    auto func1 = [](int k) {
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread tt(func1, 20);
    if (tt.joinable()) { // 檢查線程能否被join
        tt.join();
    }
    return 0;
}

上述代碼中,函數func和func1運行在線程對象t和tt中,從剛建立對象開始就會新建一個線程用於執行函數,調用join函數將會阻塞主線程,直到線程函數執行結束,線程函數的返回值將會被忽略。若是不但願線程被阻塞執行,能夠調用線程對象的detach函數,表示將線程和線程對象分離。c++

若是沒有調用join或者detach函數,假如線程函數執行時間較長,此時線程對象的生命週期結束調用析構函數清理資源,這時可能會發生錯誤,這裏有兩種解決辦法,一個是調用join(),保證線程函數的生命週期和線程對象的生命週期相同,另外一個是調用detach(),將線程和線程對象分離,這裏須要注意,若是線程已經和對象分離,那咱們就再也沒法控制線程何時結束了,不能再經過join來等待線程執行完。編程

這裏能夠對thread進行封裝,避免沒有調用join或者detach可致使程序出錯的狀況出現:promise

class ThreadGuard {
   public:
    enum class DesAction { join, detach };

    ThreadGuard(std::thread&& t, DesAction a) : t_(std::move(t)), action_(a){};

    ~ThreadGuard() {
        if (t_.joinable()) {
            if (action_ == DesAction::join) {
                t_.join();
            } else {
                t_.detach();
            }
        }
    }

    ThreadGuard(ThreadGuard&&) = default;
    ThreadGuard& operator=(ThreadGuard&&) = default;

    std::thread& get() { return t_; }

   private:
    std::thread t_;
    DesAction action_;
};

int main() {
    ThreadGuard t(std::thread([]() {
        for (int i = 0; i < 10; ++i) {
            std::cout << "thread guard " << i << " ";
        }
        std::cout << std::endl;}), ThreadGuard::DesAction::join);
    return 0;
}

c++11還提供了獲取線程id,或者系統cpu個數,獲取thread native_handle,使得線程休眠等功能安全

std::thread t(func);
cout << "當前線程ID " << t.get_id() << endl;
cout << "當前cpu個數 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用於pthread相關操做
std::this_thread::sleep_for(std::chrono::seconds(1));

std::mutex相關

std::mutex是一種線程同步的手段,用於保存多線程同時操做的共享數據。多線程

mutex分爲四種:併發

  • std::mutex:獨佔的互斥量,不能遞歸使用,不帶超時功能
  • std::recursive_mutex:遞歸互斥量,可重入,不帶超時功能
  • std::timed_mutex:帶超時的互斥量,不能遞歸
  • std::recursive_timed_mutex:帶超時的互斥量,能夠遞歸使用

拿一個std::mutex和std::timed_mutex舉例吧,別的都是相似的使用方式:異步

std::mutex:async

#include <iostream>
#include <mutex>
#include <thread>

using namespace std;
std::mutex mutex_;

int main() {
    auto func1 = [](int k) {
        mutex_.lock();
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
        mutex_.unlock();
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1, 200);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

std::timed_mutex:

#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::timed_mutex timed_mutex_;

int main() {
    auto func1 = [](int k) {
        timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
        timed_mutex_.unlock();
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1, 200);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

std::lock相關

這裏主要介紹兩種RAII方式的鎖封裝,能夠動態的釋放鎖資源,防止線程因爲編碼失誤致使一直持有鎖。

c++11主要有std::lock_guard和std::unique_lock兩種方式,使用方式都相似,以下:

#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::mutex mutex_;

int main() {
    auto func1 = [](int k) {
        // std::lock_guard<std::mutex> lock(mutex_);
        std::unique_lock<std::mutex> lock(mutex_);
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1, 200);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

std::lock_gurad相比於std::unique_lock更加輕量級,少了一些成員函數,std::unique_lock類有unlock函數,能夠手動釋放鎖,因此條件變量都配合std::unique_lock使用,而不是std::lock_guard,由於條件變量在wait時須要有手動釋放鎖的能力,具體關於條件變量後面會講到。

std::atomic相關

c++11提供了原子類型std::atomic<T>,理論上這個T能夠是任意類型,可是我平時只存放整形,別的還真的沒用過,整形有這種原子變量已經足夠方便,就不須要使用std::mutex來保護該變量啦。看一個計數器的代碼:

struct OriginCounter { // 普通的計數器
    int count;
    std::mutex mutex_;
    void add() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++count;
    }

    void sub() {
        std::lock_guard<std::mutex> lock(mutex_);
        --count;
    }

    int get() {
        std::lock_guard<std::mutex> lock(mutex_);
        return count;
    }
};

struct NewCounter { // 使用原子變量的計數器
    std::atomic<int> count;
    void add() {
        ++count;
        // count.store(++count);這種方式也能夠
    }

    void sub() {
        --count;
        // count.store(--count);
    }

    int get() {
        return count.load();
    }
};

是否是使用原子變量更加方便了呢?

std::call_once相關

c++11提供了std::call_once來保證某一函數在多線程環境中只調用一次,它須要配合std::once_flag使用,直接看使用代碼吧:

std::once_flag onceflag;

void CallOnce() {
    std::call_once(onceflag, []() {
        cout << "call once" << endl;
    });
}

int main() {
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(CallOnce);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

volatile相關

貌似把volatile放在併發裏介紹不太合適,可是貌似不少人都會把volatile和多線程聯繫在一塊兒,那就一塊兒介紹下吧。

volatile一般用來創建內存屏障,volatile修飾的變量,編譯器對訪問該變量的代碼一般再也不進行優化,看下面代碼:

int *p = xxx;
int a = *p;
int b = *p;

a和b都等於p指向的值,通常編譯器會對此作優化,把*p的值放入寄存器,就是傳說中的工做內存(不是主內存),以後a和b都等於寄存器的值,可是若是中間p地址的值改變,內存上的值改變啦,但a,b仍是從寄存器中取的值(不必定,看編譯器優化結果),這就不符合需求,因此在此對p加volatile修飾能夠避免進行此類優化。

注意:volatile不能解決多線程安全問題,針對特種內存才須要使用volatile,它和atomic的特色以下:

  • std::atomic用於多線程訪問的數據,且不用互斥量,用於併發編程中
  • volatile用於讀寫操做不能夠被優化掉的內存,用於特種內存中

std::condition_variable相關

條件變量是c++11引入的一種同步機制,它能夠阻塞一個線程或者個線程,直到有線程通知或者超時纔會喚醒正在阻塞的線程,條件變量須要和鎖配合使用,這裏的鎖就是上面介紹的std::unique_lock。

這裏使用條件變量實現一個CountDownLatch:

class CountDownLatch {
   public:
    explicit CountDownLatch(uint32_t count) : count_(count);

    void CountDown() {
        std::unique_lock<std::mutex> lock(mutex_);
        --count_;
        if (count_ == 0) {
            cv_.notify_all();
        }
    }

    void Await(uint32_t time_ms = 0) {
        std::unique_lock<std::mutex> lock(mutex_);
        while (count_ > 0) {
            if (time_ms > 0) {
                cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
            } else {
                cv_.wait(lock);
            }
        }
    }

    uint32_t GetCount() const {
        std::unique_lock<std::mutex> lock(mutex_);
          return count_; 
    }

   private:
    std::condition_variable cv_;
    mutable std::mutex mutex_;
    uint32_t count_ = 0;
};

關於條件變量其實還涉及到通知丟失和虛假喚醒問題,由於不是本文的主題,這裏暫不介紹,你們有須要能夠留言。

std::future相關

c++11關於異步操做提供了future相關的類,主要有std::future、std::promise和std::packaged_task,std::future比std::thread高級些,std::future做爲異步結果的傳輸通道,經過get()能夠很方便的獲取線程函數的返回值,std::promise用來包裝一個值,將數據和future綁定起來,而std::packaged_task則用來包裝一個調用對象,將函數和future綁定起來,方便異步調用。而std::future是不能夠複製的,若是須要複製放到容器中可使用std::shared_future。

std::promise與std::future配合使用

#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

void func(std::future<int>& fut) {
    int x = fut.get();
    cout << "value: " << x << endl;
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();
    std::thread t(func, std::ref(fut));
    prom.set_value(144);
    t.join();
    return 0;
}

std::packaged_task與std::future配合使用

#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

int func(int in) {
    return in + 1;
}

int main() {
    std::packaged_task<int(int)> task(func);
    std::future<int> fut = task.get_future();
    std::thread(std::move(task), 5).detach();
    cout << "result " << fut.get() << endl;
    return 0;
}

更多關於future的使用能夠看我以前寫的關於線程池和定時器的文章。

三者之間的關係

std::future用於訪問異步操做的結果,而std::promise和std::packaged_task在future高一層,它們內部都有一個future,promise包裝的是一個值,packaged_task包裝的是一個函數,當須要獲取線程中的某個值,可使用std::promise,當須要獲取線程函數返回值,可使用std::packaged_task。

async相關

async是比future,packaged_task,promise更高級的東西,它是基於任務的異步操做,經過async能夠直接建立異步的任務,返回的結果會保存在future中,不須要像packaged_task和promise那麼麻煩,關於線程操做應該優先使用async,看一段使用代碼:

#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

int func(int in) { return in + 1; }

int main() {
    auto res = std::async(func, 5);
    // res.wait();
    cout << res.get() << endl; // 阻塞直到函數返回
    return 0;
}

使用async異步執行函數是否是方便多啦。

async具體語法以下:

async(std::launch::async | std::launch::deferred, func, args...);

第一個參數是建立策略:

  • std::launch::async表示任務執行在另外一線程
  • std::launch::deferred表示延遲執行任務,調用get或者wait時纔會執行,不會建立線程,惰性執行在當前線程。

若是不明確指定建立策略,以上兩個都不是async的默認策略,而是未定義,它是一個基於任務的程序設計,內部有一個調度器(線程池),會根據實際狀況決定採用哪一種策略。

若從 std::async 得到的 std::future 未被移動或綁定到引用,則在完整表達式結尾, std::future的析構函數將阻塞直至異步計算完成,實際上至關於同步操做:

std::async(std::launch::async, []{ f(); }); // 臨時量的析構函數等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不開始

注意:關於async啓動策略這裏網上和各類書籍介紹的五花八門,這裏會以cppreference爲主。

有時候咱們若是想真正執行異步操做能夠對async進行封裝,強制使用std::launch::async策略來調用async。

template <typename F, typename... Args>
inline auto ReallyAsync(F&& f, Args&&... params) {
    return std::async(std::launch::async, std::forward<F>(f), std::forward<Args>(params)...);
}

關於c++11關於併發的新特性就介紹到這裏,你們有問題能夠給我留言~

參考資料

https://blog.csdn.net/zhangzq...

https://zh.cppreference.com/w...

https://zhuanlan.zhihu.com/p/...

https://www.runoob.com/w3cnot...

https://zh.cppreference.com/w...

《深刻應用c++11:代碼優化與工程級應用》

《Effective Modern C++》更多文章,請關注個人V X 公 主 號:程序喵大人,歡迎交流。

相關文章
相關標籤/搜索