c++11 新特性實戰 (一):多線程操作

c++11 新特性實戰 (一)

c++11多線程操作

  • 線程

    • thread
    int main()
    {
        thread t1(Test1);
        t1.join();
        thread t2(Test2);
        t2.join();
        thread t3 = t1;
        thread t4(t1);
        thread t5 = std::move(t1);
        thread t6(std::move(t1));
        return 0;
    }
    

    t3,t4創建失敗,因爲thread的拷貝構造和賦值運算符重載的原型是:

    thread(const thread&) = delete;
    thread& operator=(const thread&) = delete;
    

    被禁用了,但是t5, t6線程是創建成功的。std::move把t1轉換爲右值,調用的是函數原型爲thread& operator=(thread&& _Other) noexceptthread(thread&& _Other) noexcept

    當線程對象t1被移動拷貝和移動賦值給t5和t6的時候,t1就失去了線程控制權,也就是一個線程只能同時被一個線程對象所控制。最直觀的是t1.joinable()返回值爲false,joinable()函數後面介紹。

    使用類成員函數作爲線程參數

    class Task
    {
    public:
        Task(){}
        void Task1() {}
        void Task2() {}
    private:
    };
    
    int main()
    {
        Task task;
        thread t3(&Task::Task1, &task);
        t3.join();
        return 0;
    }
    

    關鍵點是要創建一個類對象,並作爲第二個參數傳入thread()線程的構造函數中去。

  • 管理當前線程的函數

    • yield

    此函數的準確性爲依賴於實現,特別是使用中的 OS 調度器機制和系統狀態。例如,先進先出實時調度器( Linux 的 SCHED_FIFO )將懸掛當前線程並將它放到準備運行的同優先級線程的隊列尾(而若無其他線程在同優先級,則 yield 無效果)。

    #include <iostream>
    #include <chrono>
    #include <thread>
     
    // 建議其他線程運行一小段時間的“忙睡眠”
    void little_sleep(std::chrono::microseconds us)
    {
        auto start = std::chrono::high_resolution_clock::now();
        auto end = start + us;
        do {
            std::this_thread::yield();
        } while (std::chrono::high_resolution_clock::now() < end);
    }
     
    int main()
    {
        auto start = std::chrono::high_resolution_clock::now();
     
        little_sleep(std::chrono::microseconds(100));
     
        auto elapsed = std::chrono::high_resolution_clock::now() - start;
        std::cout << "waited for "
                  << std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count()
                  << " microseconds\n";
    }
    
    • get_id

    這個函數不用過多介紹了,就是用來獲取當前線程id的,用來標識線程的身份。

     std::thread::id this_id = std::this_thread::get_id();
    
    • sleep_for

    位於this_thread命名空間下,msvc下支持兩種時間參數。

    std::this_thread::sleep_for(2s);
    std::this_thread::sleep_for(std::chrono::seconds(1));
    
    • sleep_untile

    參數構建起來挺麻煩的,一般場景下要求線程睡眠的就用sleep_for就行了

    using std::chrono::system_clock;
    time_t tt = system_clock::to_time_t(system_clock::now());
    struct std::tm *ptm = localtime(&tt);
     std::this_thread::sleep_until(system_clock::from_time_t(mktime(ptm)));
    
  • 互斥

    • mutex

    對於互斥量看到一個很好的比喻:

    單位上有一臺打印機(共享數據a),你要用打印機(線程1要操作數據a),同事老王也要用打印機(線程2也要操作數據a),但是打印機同一時間只能給一個人用,此時,規定不管是誰,在用打印機之前都要向領導申請許可證(lock),用完後再向領導歸還許可證(unlock),許可證總共只有一個,沒有許可證的人就等着在用打印機的同事用完後才能申請許可證(阻塞,線程1lock互斥量後其他線程就無法lock,只能等線程1unlock後,其他線程才能lock),那麼,這個許可證就是互斥量。互斥量保證了使用打印機這一過程不被打斷。

    代碼示例:

    mutex mtx;
    
    int gNum = 0;
    void Test1()
    {
        mtx.lock();
        for(int n = 0; n < 5; ++n)
            gNum++;
        mtx.unlock();
    }
    
    void Test2()
    {
        std::cout << "gNum = " << gNum << std::endl;
    }
    
    int main()
    {
        thread t1(Test1);
        t1.join();
        thread t2(Test2);
        t2.join();
        return 0;
    }
    
    

    join()表示主線程等待子線程結束再繼續執行,如果我們的期望是打印循環自增之後的gNum的值,那t1.join()就放在t2創建之前調用。因爲t2的創建就標誌着t2線程創建好然後開始執行了

    通常mutex不單獨使用,因爲lock和unlock必須配套使用,如果忘記unlock很可能造成死鎖,即使unlock寫了,但是如果在執行之前程序捕獲到異常,也還是一樣會死鎖。如何解決使用mutex造成的死鎖問題呢?下面介紹unique_gard和lock_guard的時候詳細說明。

    • timed_mutex
    std::mutex cout_mutex; // 控制到 std::cout 的訪問
    std::timed_mutex mutex;
    
    void job(int id)
    {
        using Ms = std::chrono::milliseconds;
        std::ostringstream stream;
    
        for (int i = 0; i < 3; ++i) {
            if (mutex.try_lock_for(Ms(100))) {
                stream << "success ";
                std::this_thread::sleep_for(Ms(100));
                mutex.unlock();
            } else {
                stream << "failed ";
            }
            std::this_thread::sleep_for(Ms(100));
        }
    
        std::lock_guard<std::mutex> lock(cout_mutex);
        std::cout << "[" << id << "] " << stream.str() << "\n";
    }
    
    int main()
    {
        std::vector<std::thread> threads;
        for (int i = 0; i < 4; ++i) {
            threads.emplace_back(job, i);
        }
    
        for (auto& i: threads) {
            i.join();
        }
    }
    

    這裏的第28行衍生出一個知識點:STL的emplace_back函數。這是c++11新增的容器類的操作函數,如果第二個參數忽略,用法和push_back相似,都是在stl後面追加元素。函數原型:

    template<class... _Valty>
    decltype(auto) emplace_back(_Valty&&... _Val)
    

    是一個變長的模板函數,例子中的代碼傳遞的是一個函數指針jobemplace_back的實現會把job傳遞給std::thread的構造函數,與push_back需要是std::thread類型作爲參數不同,所以emplace_back是直接在容器中構造了要添加的元素,省去了再次把參數拷貝到stl中的過程,效率更高。

    提供互斥設施,實現有時限鎖定

    • recursive_mutex

    提供能被同一線程遞歸鎖定的互斥設施

    • recursive_timed_mutex

    提供能被同一線程遞歸鎖定的互斥設施,並實現有時限鎖定

  • 通用互斥管理

    • lock_guard
    void Test1()
    {
        std::lock_guard<std::mutex> lg(mtx);
        for(int n = 0; n < 5; ++n)
        {
            gNum++;
            std::cout << "gNum = " << gNum << std::endl;
        }
    }
    int main()
    {
        thread t1(Test1);
        thread t2(Test1);
        t1.join();
        t2.join();
        return 0;
    }
    

    lock_guard相當於利用RAII機制(“資源獲取就是初始化”)把mutex封裝了一下,在構造中lock,在析構中unlock。避免了中間過程出現異常導致的mutex不能夠正常unlock.

    • scoped_lock(c++17)
    • unique_lock
    • defer_lock_t
    • try_to_lock_t
    • adopt_lock_t
    • defer_lock
    • try_to_lock
    • adopt_lock
  • 通用鎖算法

    • try_lock
    • lock
  • 單次調用

    • once_flag
    • call_once
  • 條件變量

    • condition_variable
    • condition_variable_any
    • notify_all_at_thread_exit
    • cv_status
  • Future

    • promise
    • packaged_task
    • future
    • shared_future
    • async
    • launch
    • future_status
    • Future錯誤
      • future_error
      • future_category
      • future_errc
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章