C++11——多線程異步操作


轉載來自:https://subingwen.cn/cpp/async/

1. std::futrue
C++11 中增加的線程類,使得我們能夠非常方便的創建和使用線程,但有時會有些不方便,比如需要獲取線程返回的結果,就不能通過 join() 得到結果,只能通過一些額外手段獲得,比如:定義一個全局變量,在子線程中賦值,在主線程中讀這個變量的值,整個過程比較繁瑣。C++ 提供的線程庫中提供了一些類用於訪問異步操作的結果。

那么,什么叫做異步呢?

 

 

 


我們去星巴克買咖啡,因為都是現磨的,所以需要等待,但是我們付完賬后不會站在櫃台前死等,而是去找個座位坐下來玩玩手機打發一下時間,當店員把咖啡磨好之后,就會通知我們過去取,這就叫做異步

顧客(主線程)發起一個任務(子線程磨咖啡),磨咖啡的過程中顧客去做別的事情了,有兩條時間線(異步)
顧客(主線程)發起一個任務(子線程磨咖啡),磨咖啡的過程中顧客沒去做別的事情而是死等,這時就只有一條時間線(同步),此時效率相對較低。
因此多線程程序中的任務大都是異步的,主線程和子線程分別執行不同的任務,如果想要在主線中得到某個子線程任務函數返回的結果可以使用 C++11 提供的 std:future 類,這個類需要和其他類或函數搭配使用,先來介紹一下這個類的 API 函數

類的定義

通過類的定義可以得知,futrue 是一個模板類,也就是這個類可以存儲任意指定類型的數據。

// 定義於頭文件 <future>
template< class T > class future;
template< class T > class future<T&>;
template<> class future<void>;

構造函數

//
future() noexcept;
//
future( future&& other ) noexcept;
//
future( const future& other ) = delete;

構造函數①:默認無參構造函數
構造函數②:移動構造函數,轉移資源的所有權
構造函數③:使用 =delete 顯示刪除拷貝構造函數,不允許進行對象之間的拷貝
常用成員函數(public)

一般情況下使用 = 進行賦值操作就進行對象的拷貝,但是 future 對象不可用復制,因此會根據實際情況進行處理:

如果 other 是右值,那么轉移資源的所有權
如果 other 是非右值,不允許進行對象之間的拷貝(該函數被顯示刪除禁止使用)

future& operator=( future&& other ) noexcept;
future& operator=( const future& other ) = delete;

 


取出 future 對象內部保存的數據,其中 void get() 是為 future<void> 准備的,此時對象內部類型就是 void,該函數是一個阻塞函數,當子線程的數據就緒后解除阻塞就能得到傳出的數值了。

T get();
T& get();
void get();

因為 future 對象內部存儲的是異步線程任務執行完畢后的結果,是在調用之后的將來得到的,因此可以通過調用 wait() 方法,阻塞當前線程,等待這個子線程的任務執行完畢,任務執行完畢當前線程的阻塞也就解除了

void wait() const;

如果當前線程 wait() 方法就會死等,直到子線程任務執行完畢將返回值寫入到 future 對象中,調用 wait_for() 只會讓線程阻塞一定的時長,但是這樣並不能保證對應的那個子線程中的任務已經執行完畢了。

wait_until() 和 wait_for() 函數功能是差不多,前者是阻塞到某一指定的時間點,后者是阻塞一定的時長。

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

template< class Clock, class Duration >
std::future_status wait_until( const std::chrono::time_point<Clock,Duration>& timeout_time ) const;

當 wait_until() 和 wait_for() 函數返回之后,並不能確定子線程當前的狀態,因此我們需要判斷函數的返回值,這樣就能知道子線程當前的狀態了:

常量 解釋
future_status::deferred 子線程中的任務函仍未啟動
future_status::ready 子線程中的任務已經執行完畢,結果已就緒
future_status::timeout 子線程中的任務正在執行中,指定等待時長已用完
2. std::promise
std::promise 是一個協助線程賦值的類,它能夠將數據和 future 對象綁定起來,為獲取線程函數中的某個值提供便利。

2.1 類成員函數
類定義

通過 std::promise 類的定義可以得知,這也是一個模板類,我們要在線程中傳遞什么類型的數據,模板參數就指定為什么類型

// 定義於頭文件 <future>
template< class R > class promise;
template< class R > class promise<R&>;
template<> class promise<void>;

構造函數

//
promise();
//
promise( promise&& other ) noexcept;
//
promise( const promise& other ) = delete;

構造函數①:默認構造函數,得到一個空對象
構造函數②:移動構造函數
構造函數③:使用 =delete 顯示刪除拷貝構造函數,不允許進行對象之間的拷貝
公共成員函數

在 std::promise 類內部管理着一個 future 類對象,調用 get_future() 就可以得到這個 future 對象了

C++

std::future<T> get_future();

存儲要傳出的 value 值,並立即讓狀態就緒,這樣數據被傳出其它線程就可以得到這個數據了。重載的第四個函數是為 promise<void> 類型的對象准備的。

void set_value( const R& value );
void set_value( R&& value );
void set_value( R& value );
void set_value();

存儲要傳出的 value 值,但是不立即令狀態就緒。在當前線程退出時,子線程資源被銷毀,再令狀態就緒。

void set_value_at_thread_exit( const R& value );
void set_value_at_thread_exit( R&& value );
void set_value_at_thread_exit( R& value );
void set_value_at_thread_exit();

2.2 promise 的使用
通過 promise 傳遞數據的過程一共分為 5 步:

在主線程中創建 std::promise 對象
這個 std::promise 對象通過引用的方式傳遞給子線程的任務函數
在子線程任務函數中給 std::promise 對象賦值
在主線程中通過 std::promise 對象取出綁定的 future 實例對象
通過得到的 future 對象取出子線程任務函數中返回的值。
子線程任務函數執行期間,讓狀態就緒

#include <iostream>
#include <thread>
#include <future>
using namespace std;

int main()
{
    promise<int> pr;
    thread t1([](promise<int>& p) {
        p.set_value(100);
        this_thread::sleep_for(chrono::seconds(3));
        cout << "睡醒了...." << endl;
        }, ref(pr));

    future<int> f = pr.get_future();
    int value = f.get();
    cout << "value: " << value << endl;

    t1.join();
    return 0;
}

 

示例程序輸出的結果:

value: 100
睡醒了....

示例程序的中子線程的任務函數指定的是一個匿名函數,在這個匿名的任務函數執行期間通過 p.set_value(100); 傳出了數據並且激活了狀態,數據就緒后,外部主線程中的 int value = f.get(); 解除阻塞,並將得到的數據打印出來,5 秒鍾之后子線程休眠結束,匿名的任務函數執行完畢。

子線程任務函數執行結束,讓狀態就緒

#include <iostream>
#include <thread>
#include <future>
using namespace std;

int main()
{
    promise<int> pr;
    thread t1([](promise<int>& p) {
        p.set_value_at_thread_exit(100);
        this_thread::sleep_for(chrono::seconds(3));
        cout << "睡醒了...." << endl;
        }, ref(pr));

    future<int> f = pr.get_future();
    int value = f.get();
    cout << "value: " << value << endl;

    t1.join();
    return 0;
}

 

示例程序輸出的結果:

睡醒了....
value: 100

在示例程序中,子線程的這個匿名的任務函數中通過 p.set_value_at_thread_exit(100); 在執行完畢並退出之后才會傳出數據並激活狀態,數據就緒后,外部主線程中的 int value = f.get(); 解除阻塞,並將得到的數據打印出來,因此子線程在休眠 5 秒鍾之后主線程中才能得到傳出的數據。

另外,在這兩個實例程序中有一個知識點需要強調,在外部主線程中創建的 promise 對象必須要通過引用的方式傳遞到子線程的任務函數中,在實例化子線程對象的時候,如果任務函數的參數是引用類型,那么實參一定要放到 std::ref () 函數中,表示要傳遞這個實參的引用到任務函數中。

3. std::packaged_task
std::packaged_task 類包裝了一個可調用對象包裝器類對象(可調用對象包裝器包裝的是可調用對象,可調用對象都可以作為函數來使用),惡補一下可調用對象和可調用對象包裝器

這個類可以將內部包裝的函數和 future 類綁定到一起,以便進行后續的異步調用,它和 std::promise 有點類似,std::promise 內部保存一個共享狀態的值,而 std::packaged_task 保存的是一個函數。

3.1 類成員函數
類的定義

通過類的定義可以看到這也是一個模板類,模板類型和要在線程中傳出的數據類型是一致的。

// 定義於頭文件 <future>
template< class > class packaged_task;
template< class R, class ...Args >
class packaged_task<R(Args...)>;

構造函數

//
packaged_task() noexcept;
//
template <class F>
explicit packaged_task( F&& f );
//
packaged_task( const packaged_task& ) = delete;
//
packaged_task( packaged_task&& rhs ) noexcept;

構造函數①:無參構造,構造一個無任務的空對象
構造函數②:通過一個可調用對象,構造一個任務對象
構造函數③:拷貝構造函數,任務不能被復制多份,因此會刪除舊任務
構造函數④:移動構造函數
常用公共成員函數

通過調用任務對象內部的 get_future() 方法就可以得到一個 future 對象,基於這個對象就可以得到傳出的數據了。

std::future<R> get_future();

3.2 packaged_task 的使用
packaged_task 其實就是對子線程要執行的任務函數進行了包裝,和可調用對象包裝器的使用方法相同,包裝完畢之后直接將包裝得到的任務對象傳遞給線程對象就可以了。

#include <iostream>
#include <thread>
#include <future>
using namespace std;

int main()
{
    packaged_task<int(int)> task([](int x) {
        return x += 100;
        });

    thread t1(ref(task), 100);

    future<int> f = task.get_future();
    int value = f.get();
    cout << "value: " << value << endl;

    t1.join();
    return 0;
}
value: 200

 

在上面的示例代碼中,通過 packaged_task 類包裝了一個匿名函數作為子線程的任務函數,最終的得到的這個任務對象需要通過引用的方式傳遞到子線程內部,這樣才能在主線程的最后通過任務對象得到 future 對象,再通過這個 future 對象取出子線程通過返回值傳遞出的數據。

4. std::async
std::async 函數比前面提到的 std::promise 和 packaged_task 更高級一些,因為通過這函數可以直接啟動一個子線程並在這個子線程中執行對應的任務函數,異步任務執行完成返回的結果也是存儲到一個 future 對象中,當需要獲取異步任務的結果時,只需要調用 future 類的get() 方法即可,如果不關注異步任務的結果,只是簡單地等待任務完成的話,可以調用 future 類的wait()或者wait_for() 方法。該函數的函數原型如下:

// 定義於頭文件 <future>
//
template< class Function, class... Args>
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( Function&& f, Args&&... args );

//
template< class Function, class... Args >
std::future<std::result_of_t<std::decay_t<Function>(std::decay_t<Args>...)>>
async( std::launch policy, Function&& f, Args&&... args );

 

可以看到這是一個模板函數,在 C++11 中這個函數有兩種調用方式:

函數①:直接調用傳遞到函數體內部的可調用對象,返回一個 future 對象
函數②:通過指定的策略調用傳遞到函數內部的可調用對象,返回一個 future 對象
函數參數:

f:可調用對象,這個對象在子線程中被作為任務函數使用

Args:傳遞給 f 的參數(實參)

policy:可調用對象・f 的執行策略

策略 說明
std::launch::async 調用 async 函數時開始創建線程
std::launch::deferred 調用 async 函數時不創建線程,直到調用了 future 的 get() 或者 wait() 時才創建線程
關於 std::async() 函數的使用,對應的示例代碼如下:

4.1 方式 1
調用 async () 函數直接創建線程執行任務

#include <iostream>
#include <thread>
#include <future>
using namespace std;

int main()
{
    cout << "主線程ID: " << this_thread::get_id() << endl;
    // 調用函數直接創建線程執行任務
    future<int> f = async([](int x) {
        cout << "子線程ID: " << this_thread::get_id() << endl;
        this_thread::sleep_for(chrono::seconds(5));
        return x += 100;
        }, 1000);

    future_status status;
    do {
        status = f.wait_for(chrono::seconds(1));
        if (status == future_status::deferred)
        {
            cout << "線程還沒有執行..." << endl;
            f.wait();
        }
        else if (status == future_status::ready)
        {
            cout << "子線程返回值: " << f.get() << endl;
        }
        else if (status == future_status::timeout)
        {
            cout << "任務還未執行完畢, 繼續等待..." << endl;
        }
    } while (status != future_status::ready);

    return 0;
}

 

示例程序輸出的結果為:

主線程ID: 7096
子線程ID: 23788
任務還未執行完畢, 繼續等待...
任務還未執行完畢, 繼續等待...
子線程返回值: 1100


調用 async() 函數時不指定策略就是直接創建線程並執行任務,示例代碼的主線程中做了如下操作 status = f.wait_for(chrono::seconds(1)); 其實直接調用 f.get() 就能得到子線程的返回值。這里為了給大家演示 wait_for() 的使用,所以寫的復雜了些。

4.2 方式 2
調用 async () 函數不創建線程執行任務

#include <iostream>
#include <thread>
#include <future>
using namespace std;

int main()
{
    cout << "主線程ID: " << this_thread::get_id() << endl;
    // 調用函數直接創建線程執行任務
    future<int> f = async(launch::deferred, [](int x) {
        cout << "子線程ID: " << this_thread::get_id() << endl;
        return x += 100;
        }, 100);

    this_thread::sleep_for(chrono::seconds(5));
    cout << f.get();

    return 0;
}

示例程序輸出的結果:

主線程ID: 24548
子線程ID: 24548
200

由於指定了 launch::deferred 策略,因此調用 async() 函數並不會馬上創建線程並執行任務,必須使用 future 類對象調用 get() 或者 wait() 時才能創建線程(此處一定要注意調用 wait_for () 函數是不行的)。通過示例程序可以推導出在主線程休眠 5 秒后,子線程才能被創建並執行,打印出的結果亦如此。

最終總結:

使用 async () 函數,是多線程操作中最簡單的一種方式,不需要自己創建線程對象,並且可以得到子線程函數的返回值。
使用 std::promise 類,在子線程中可以傳出返回值也可以傳出其他數據,並且可選擇在什么時機將數據從子線程中傳遞出來,使用起來更靈活。
使用 std::packaged_task 類,可以將子線程的任務函數進行包裝,並且可以得到子線程的返回值。


免責聲明!

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



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