c++11 多執行緒入門教程(一)

aircraft發表於2019-05-29

 

原文作者:aircraft

原文連結:https://www.cnblogs.com/DOMLX/p/10945309.html

    

 

      最近是恰好寫了一些c++11多執行緒有關的東西,就寫一下筆記留著以後自己忘記回來看吧,也不是專門寫給讀者看的,我就想到哪就寫到哪吧

 

  c++11呢,就是c++升級之後的一個版本,現在馬上就出c++20了,裡面增加了很多對多執行緒支援的類,讓多執行緒程式設計更加簡單了,好了廢話不多說,先來建立一個簡單的多執行緒程式設計案例,看看c++11下多執行緒程式設計建立到底有多麼的簡單。

 

1.建立一個簡單的多執行緒案例:

首先匯入#include<thread>---用於建立執行緒

其次匯入#include<chrono>--用於時間延時 獲取時間之類的

定義一個執行緒物件t1,這就自動建立了一個執行緒,引數就是你要執行緒去執行的函式,t1是變數名字 隨便取

std::thread t1(func);

下面這裡返回一個毫秒級別的時間間隔引數值,間隔10毫秒 

std::chrono::milliseconds(10)

this_thread::sleep_for()就是讓此執行緒休眠,可以傳入休眠的時間

this_thread::sleep_for(std::chrono::milliseconds(10));讓本執行緒休眠10毫秒

 

好了知道這些引數意思就行了,看一下程式碼:

#include<windows.h>
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;

int number = 1;

int ThreadProc1()
{
    while (number < 100)
    {
        cout << "thread 1 :" << number << endl;
        ++number;
       this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    return 0;
}

int ThreadProc2()
{
    while (number < 100)
    {
        cout << "thread 2 :" << number << endl;
        ++number;
        this_thread::sleep_for(std::chrono::milliseconds(10));
    }

    return 0;
}

int main()
{
    thread t1(ThreadProc1);
    thread t2(ThreadProc2);

    t1.join();
    t2.join();

    system("pause");
    return 0;
}

  join()就是阻塞執行緒,直到執行緒函式執行完畢,如果函式有返回值,在這裡會直接忽略。阻塞的目的就是讓Main主執行緒等待一下建立的執行緒,免得我函式還在跑,程式就直接結束了。

  如果不想阻塞在這裡就將join()換成使用執行緒的detach()方法,將執行緒與執行緒物件分離,執行緒就可以繼續執行下去,並且不會造成影響。

  從示例可以看到c++11下建立多執行緒多麼方便了吧 ,比在Linux下用posix建立還簡便,而這個也是可以在windows使用的(想想windows下多執行緒的程式碼,看著都頭疼好吧,亂七八糟一大堆)。

 

2.互斥量的使用

  跟往常的多執行緒一樣,多執行緒在執行過程中都會對臨界區進行訪問,也就是一起訪問共享資源。這樣就會造成一個問題,當兩個執行緒都要對一個變數int value值假如為11,加一時,執行緒一取出11 進行加一還沒有存入value,這時候執行緒二又取得value的11進行加一,然後執行緒一存入12,執行緒二又存入12,這就匯入兩個執行緒訪問衝突,也就是臨界區問題。所以引進互斥量來解決。

匯入#include <mutex>

程式碼案例:

一個執行緒對變數number進行加一100次,另外一個減一100次,最後結果應該還是原來的值0。

 

#include<windows.h>
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
using namespace std;

int number = 0;
mutex g_lock;

int ThreadProc1()
{
    
    for (int i = 0; i < 100; i++)
    {
        g_lock.lock();
        ++number;
        cout << "thread 1 :" << number << endl;
        g_lock.unlock();
        this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    
    return 0;
}

int ThreadProc2()
{
    
    for (int i = 0; i < 100; i++)
    {
        g_lock.lock();
        --number;
        cout << "thread 2 :" << number << endl;
        g_lock.unlock();
        this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    
    return 0;
}

int main()
{
    thread t1(ThreadProc1);
    thread t2(ThreadProc2);

    t1.detach();
    t2.detach();

    system("pause");
    return 0;
}

 

 

上面的每次都要對mutex變數進行鎖以及解鎖,有時候忘記解鎖就涼涼了。所以c++11還提供了一個lock_guard類,它利用了RAII機制可以保證安全釋放mutex。

在std::lock_guard物件構造時,傳入的mutex物件(即它所管理的mutex物件)會被當前執行緒鎖住。在lock_guard物件被析構時,它所管理的mutex物件會自動解鎖,不需要程式設計師手動呼叫lock和unlock對mutex進行上鎖和解鎖操作。lock_guard物件並不負責管理mutex物件的生命週期,lock_guard物件只是簡化了mutex物件的上鎖和解鎖操作,方便執行緒對互斥量上鎖,即在某個lock_guard物件的生命週期內,它所管理的鎖物件會一直保持上鎖狀態;而lock_guard的生命週期結束之後,它所管理的鎖物件會被解鎖。程式設計師可以非常方便地使用lock_guard,而不用擔心異常安全問題。

程式碼:

 

#include<windows.h>
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
using namespace std;

int number = 0;
mutex g_lock;

int ThreadProc1()
{
    lock_guard<mutex> loker(mutex);
    for (int i = 0; i < 100; i++)
    {
        ++number;
        cout << "thread 1 :" << number << endl;
        
    }
    //this_thread::sleep_for(std::chrono::milliseconds(100));
    return 0;
}

int ThreadProc2()
{
    lock_guard<mutex> loker(mutex);
    for (int i = 0; i < 100; i++)
    {
        --number;
        cout << "thread 2 :" << number << endl;
        //this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    
    return 0;
}

int main()
{
    thread t1(ThreadProc1);
    thread t2(ThreadProc2);

    t1.detach();
    t2.detach();

    system("pause");
    return 0;
}

 

除了lock_guard,之外c++11還提供了std::unique_lock

類 unique_lock 是通用互斥包裝器,允許延遲鎖定、鎖定的有時限嘗試、遞迴鎖定、所有權轉移和與條件變數一同使用
unique_lock比lock_guard使用更加靈活,功能更加強大。
使用unique_lock需要付出更多的時間、效能成本。

#include <iostream>       // std::cout
#include <thread>         // std::thread
#include <mutex>          // std::mutex, std::unique_lock
#include <vector>

std::mutex mtx;           // mutex for critical section
std::once_flag flag;        //定義一個once_flag型別的變數作為call_once引數,    
                                    //用std::call_once來保證多執行緒環境中只被呼叫一次
void print_block (int n, char c) {
    //unique_lock有多組建構函式, 這裡std::defer_lock不設定鎖狀態
    std::unique_lock<std::mutex> my_lock (mtx, std::defer_lock);
    //嘗試加鎖, 如果加鎖成功則執行
    //(適合定時執行一個job的場景, 一個執行緒執行就可以, 可以用更新時間戳輔助)
    if(my_lock.try_lock()){
        for (int i=0; i<n; ++i)
            std::cout << c;
        std::cout << '\n';
    }
}

void run_one(int &n){
    std::call_once(flag, [&n]{n=n+1;}); //只執行一次, 適合延遲載入; 多執行緒static變數情況
}

int main () {
    std::vector<std::thread> ver;
    int num = 0;
    for (auto i = 0; i < 10; ++i){
        ver.emplace_back(print_block,50,'*');  
        ver.emplace_back(run_one, std::ref(num));
  //emplace_back比push_back更好 是c++11增加的
    }

    for (auto &t : ver){
        t.join();
    }
    std::cout << num << std::endl;
    return 0;
}                    

  

 這裡還要補充一下跟互斥量很像的條件變數的知識。

條件變數std::condition_variable的使用

  std::condition_variable 是為了解決死鎖而生的。當互斥操作不夠用而引入的。比如,執行緒可能需要等待某個條件為真才能繼續執行,而一個忙等待迴圈中可能會導致所有其他執行緒都無法進入臨界區使得條件為真時,就會發生死鎖。所以,condition_variable例項被建立出現主要就是用於喚醒等待執行緒從而避免死鎖。std::condition_variable的 notify_one()用於喚醒一個執行緒;notify_all() 則是通知所有執行緒。
C++11中的std::condition_variable就像Linux下使用pthread_cond_wait和pthread_cond_signal一樣,可以讓執行緒休眠,直到別喚醒,現在在從新執行。執行緒等待在多執行緒程式設計中使用非常頻繁,經常需要等待一些非同步執行的條件的返回結果。
示例程式碼:

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


std::mutex g_mu;
std::condition_variable g_vc;
bool g_ready = false;

void dispaly_id(int id)
{
    std::unique_lock<std::mutex> lck(g_mu);
    g_vc.wait(lck, []() {return g_ready; });   //執行緒阻塞,直到第二個引數返回值為真

    std::cout << "id:" << id << std::endl;
}

void ready()
{
    std::unique_lock<std::mutex> lck(g_mu);
    g_ready = true;
    g_vc.notify_all();    //喚醒所有的等待執行緒
}

int main()
{
    std::thread t[8];
    for (int i = 0; i < 8; i++)
    {
        t[i] = std::thread(dispaly_id, i);
    }
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "all thread lock......" << std::endl;
    
    ready();
    for (auto & th : t) th.join();

    system("pause");

    return 0;
}

 

 

3.原子變數的使用

  在新標準C++11,引入了原子操作的概念,原子操作更接近核心,並通過這個新的標頭檔案提供了多種原子運算元據型別,例如,atomic_bool,atomic_int等等,如果我們在多個執行緒中對這些型別的共享資源進行操作,編譯器將保證這些操作都是原子性的,也就是說,確保任意時刻只有一個執行緒對這個資源進行訪問,編譯器將保證,多個執行緒訪問這個共享資源的正確性。從而避免了鎖的使用,提高了效率。

  上面我們用互斥鎖來實現加一百次,減少一百次。使用原子變數會更加簡潔。

 

#include<windows.h>
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <atomic>
using namespace std;

atomic<int> number(0);//定義原子變數 一次只允許一個執行緒對其進行訪問
//int number = 0;
//mutex g_lock;

int ThreadProc1()
{
    //lock_guard<mutex> loker(mutex);
    for (int i = 0; i < 100; i++)
    {
        ++number;
        cout << "thread 1 :" << number << endl;
        
    }
    //this_thread::sleep_for(std::chrono::milliseconds(100));
    return 0;
}

int ThreadProc2()
{
    //lock_guard<mutex> loker(mutex);
    for (int i = 0; i < 100; i++)
    {
        --number;
        cout << "thread 2 :" << number << endl;
        //this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    
    return 0;
}

int main()
{
    thread t1(ThreadProc1);
    thread t2(ThreadProc2);

    t1.detach();
    t2.detach();

    system("pause");
    return 0;
}

 

可以看到使用了原子變數之後,程式碼簡化了很多,以及以後對某些共享資源我們都可以酌情的定義為原子變數型別,很方便有木有。。。。。

 

 4.future與promise的使用

  在c++11中增加的執行緒庫很方便的讓我們去使用執行緒,但是因為做出了一些改變,我們並不能像往常一樣直接使用thread.join()獲取執行緒函式的返回值了,而我們有時候又確實要利用執行緒函式的返回值。

  而thread庫提供了future用來訪問非同步操作的結果,因為一個非同步操作的結果往往不能立即獲取,只能在未來的某個時候從某個地方獲取,這個非同步操作的結果是一個未來的期待值,所以被稱為future

  future和promise的作用是在不同執行緒之間傳遞資料。

假設執行緒1需要執行緒2的資料,那麼組合使用方式如下:

  1.     執行緒1初始化一個promise物件和一個future物件,promise傳遞給執行緒2,相當於執行緒2對執行緒1的一個承諾;future相當於一個接受一個承諾,用來獲取未來執行緒2傳遞的值
  2.     執行緒2獲取到promise後,需要對這個promise傳遞有關的資料,之後執行緒1的future就可以獲取資料了。
  3.     如果執行緒1想要獲取資料,而執行緒2未給出資料,則執行緒1阻塞,直到執行緒2的資料到達

示例程式碼:

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <atomic>
#include <future>
#include <vector>

void disPlay(std::future<int>& value)
{
    std::cout << "wait some times......" << std::endl;
    auto result = value.get();    //沒有獲取到值會阻塞等待獲取
    std::cout << "Value:" << result << std::endl;
}


int main()
{
    std::promise<int> promise;    
    std::future<int> value = promise.get_future(); //將promise與future繫結
    
    std::thread t1(disPlay, std::ref(value));  //建立執行緒並且函式傳參,ref()是傳一個引用
    std::this_thread::sleep_for(std::chrono::seconds(1)); //執行緒延時1秒

    //給執行緒傳值進去
    promise.set_value(15);
    t1.join();
    
    
    system("pause");

    return 0;
}

  獲取future的結果有三種方式上面是get()獲取非同步結果值返回,還有wait()等待非同步操作完成,以及wait_for()超時等待返回結果。

 

 5.future與package_task的使用

  std::packaged_task包裝一個可呼叫的物件,並且允許非同步獲取該可呼叫物件產生的結果。
std::packaged_task將其包裝的可呼叫物件的執行結果傳遞給一個std::future物件,與std::promise某種程度上是很像的,promise儲存一個共享狀態的值,而package_task儲存的是一個函式。

 

示例程式碼:

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <atomic>
#include <future>
#include <vector>

inline int func(int x)
{
    return x + 6;
}

int main()
{
    std::packaged_task<int(int)> tsk(func);
    std::future<int> fut = tsk.get_future(); //獲取future繫結起來
    
    std::thread(std::move(tsk), 2).detach();//直接將task轉移作為執行緒函式使用

    auto value = fut.get();
    std::cout << "result:" << value << std::endl;
    
    system("pause");

    return 0;
}

 

 6.執行緒非同步操作函式async的用法

  ,std::async比std::packaged_task,std::promise中,std::thread更高一層,它可以直接用來建立非同步的task,非同步的結果也儲存在future中。完成後,外面再通過future.get/wait來獲取這個未來的結果,強烈推薦使用async,我們不需要關注非同步任務的結果,只要等待任務完成獲取值就行了。

  現在來看看std::async的原型async(std::launch::async | std::launch::deferred, f, args...),第一個引數是執行緒的建立策略,有兩種策略,預設的策略是立即建立執行緒:

 

  • std::launch::async:在呼叫async就開始建立執行緒。
  • std::launch::deferred:延遲載入方式建立執行緒。呼叫async時不建立執行緒,直到呼叫了future的get或者wait時才建立執行緒。

 

第二個引數是執行緒函式,第三個引數是執行緒函式的引數。

程式碼示例:

#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <atomic>
#include <future>
#include <vector>


int main()
{
    std::future<int> fut = std::async(std::launch::async, []() {
        return 9;
    });
    
    std::cout << "result:" << fut.get() << std::endl;
    system("pause");

    return 0;
}

  []()這是c++11裡面lambda表示式用法

 

7.std::future::wait_for()函式作用

  函式原型:

template< class Rep, class Period >
std::future_status wait_for( const std::chrono::duration<Rep,Period>& timeout_duration ) const;
 

  等待結果變得可用。阻塞直至經過指定的 timeout_duration ,或結果變為可用,兩者的先到來者。返回值鑑別結果的狀態。

此函式可能由於排程或資源爭議延遲而阻塞長於 timeout_duration

推薦標準庫用穩定時鐘度量時長。若實現用系統時鐘代替,則等待時間可能也對時鐘調整敏感。

若呼叫此函式前 valid()== false 則行為未定義。

引數

timeout_duration - 要阻塞的最大時長

返回值

 
常量 解釋
future_status::deferred 要計算結果的函式仍未啟動
future_status::ready 結果就緒
future_status::timeout 已經過時限

異常

時鐘、時間點或時長在執行中可能拋的任何異常(標準庫提供的時鐘、時間點和時長決不丟擲)。

注意

鼓勵實現在呼叫前檢測 valid == false 的情況並丟擲以 future_errc::no_state 為 error_condition 的 future_error

 

程式碼示例:

#include <iostream>
#include <future>
#include <thread>
#include <chrono>
 
int main()
{
    std::future<int> future = std::async(std::launch::async, [](){ 
        std::this_thread::sleep_for(std::chrono::seconds(3));
        return 8;  
    }); 
 
    std::cout << "waiting...\n";
    std::future_status status;
    do {
        status = future.wait_for(std::chrono::seconds(1));
        if (status == std::future_status::deferred) {
            std::cout << "deferred\n";
        } else if (status == std::future_status::timeout) {
            std::cout << "timeout\n";
        } else if (status == std::future_status::ready) {
            std::cout << "ready!\n";
        }
    } while (status != std::future_status::ready); 
 
    std::cout << "result is " << future.get() << '\n';
}

可能結果:

waiting...
timeout
timeout
ready!
result is 8

 

後面還會出很多一系列的入門教程,可以關注我噢。。。。。hhhhhhhh

 

也可以補一下基礎多執行緒程式設計教程如下:

c++ 網路程式設計課設入門超詳細教程 ---目錄

相關文章