c++11新特性之線程相關所有知識點

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

  • 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。直接看代碼:

#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函數,表示將線程和線程對象分離。

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

這裏可以對thread進行封裝,避免沒有調用join或者detach可導致程序出錯的情況出現:

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:

#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)...);
}

總結

 std::thread使線程的創建變得非常簡單,還可以獲取線程id等信息。

 std::mutex通過多種方式保證了線程安全,互斥量可以獨佔,也可以重入,還可以設置互斥量的超時時間,避免一直阻塞等鎖。

• std::lock通過RAII技術方便了加鎖和解鎖調用,有std::lock_guard和std::unique_lock。

• std::atomic提供了原子變量,更方便實現實現保護,不需要使用互斥量

• std::call_once保證函數在多線程環境下只調用一次,可用於實現單例。

• volatile常用於讀寫操作不可以被優化掉的內存中。

• std::condition_variable提供等待的同步機制,可阻塞一個或多個線程,等待其它線程通知後喚醒。

• std::future用於異步調用的包裝和返回值。

• async更方便的實現了異步調用,異步調用優先使用async取代創建線程。

關於c++11關於併發的新特性就介紹到這裏

參考資料

https://blog.csdn.net/zhangzq86/article/details/70623394

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

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

https://www.runoob.com/w3cnote/c-volatile-keyword.html

https://zh.cppreference.com/w/cpp/thread/async

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

《Effective Modern C++》

十大經典排序算法(動態演示+代碼)

C語言與C++面試知識總結

數據結構之堆棧

一文輕鬆理解內存對齊

一文輕鬆理解打印有效日誌

一文讀懂C語言與C++動態內存

面試中常見的C語言與C++區別的問題

數據結構之線性表

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