《C++併發編程實戰》讀書筆記(3):併發操作的同步

来源:https://www.cnblogs.com/moonzzz/archive/2023/09/04/17668383.html
-Advertisement-
Play Games

## 1、條件變數 當線程需要等待特定事件發生、或是某個條件成立時,可以使用條件變數`std::condition_variable`,它在標準庫頭文件``內聲明。 ```c++ std::mutex mut; std::queue data_queue; std::condition_variab ...


1、條件變數

當線程需要等待特定事件發生、或是某個條件成立時,可以使用條件變數std::condition_variable,它在標準庫頭文件<condition_variable>內聲明。

std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;
void data_preparation_thread()
{
    while (more_data_to_prepare())
    {
        const data_chunk data = prepare_data();
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(data);
        data_cond.notify_one();
    }
}
void data_processing_thread()
{
    while (true)
    {
        std::unique_lock<std::mutex> lk(mut);
        data_cond.wait(lk, [] { return !data_queue.empty(); });
        data_chunk data = data_queue.front();
        data_queue.pop();
        lk.unlock();
        process(data);
        if (is_last_chunk(data)) { break; }
    }
}

wait()會先在內部調用lambda函數判斷條件是否成立,若條件成立則wait()返回,否則解鎖互斥並讓當前線程進入等待狀態。當其它線程調用notify_one()時,當前調用wait()的線程被喚醒,重新獲取互斥鎖並查驗條件,若條件成立則wait()返回(互斥仍被鎖住),否則解鎖互斥並繼續等待。

wait()函數的第二個參數可以傳入lambda函數,也可以傳入普通函數或可調用對象,也可以不傳。

notify_one()喚醒正在等待當前條件的線程中的一個,如果沒有線程在等待,則函數不執行任何操作,如果正在等待的線程多於一個,則喚醒的線程是不確定的。notify_all()喚醒正在等待當前條件的所有線程,如果沒有正在等待的線程,則函數不執行任何操作。

2、使用future等待一次性事件發生

C++標準程式庫有兩種future,分別由兩個類模板實現,即std::future<>std::shared_future<>,它們的聲明位於頭文件<future>內。

2.1、從後臺任務返回值

由於std::thread沒有提供直接回傳結果的方法,所以我們使用函數模板std::async()來解決這個問題。std::async()以非同步方式啟動任務,並返回一個std::future對象,運行函數一旦完成,其返回值就由該對象持有。在std::future對象上調用get()方法時,當前線程就會阻塞,直到std::future準備妥當並返回非同步線程的結果。std::future模擬了對非同步結果的獨占行為,get()僅能被有效調用一次,調用時會對目標值進行移動操作。

int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
    std::future<int> the_answer = std::async(find_the_answer_to_ltuae);
    do_other_stuff();
    std::cout << "The answer is " << the_answer.get() << std::endl;
}

在調用std::async()時,它可以接收附加參數進而傳遞給任務函數作為其參數,此方式與std::thread的構造函數相同。更多啟動非同步線程的方法可參考下麵的常式:

struct X
{
    void foo(int, const std::string&);
    std::string bar(const std::string&);
};
X x;
auto f1 = std::async(&X::foo, &x, 42, "hello"); // 調用p->foo(42, "hello"),p是指向x的指針
auto f2 = std::async(&X::bar, x, "goodbye");    // 調用tmpx.bar("goodbye"), tmpx是x的拷貝副本
struct Y
{
    double operator()(double);
};
Y y;
auto f3 = std::async(Y(), 3.141);         // 調用tmpy(3.141),tmpy是由Y()生成的匿名變數
auto f4 = std::async(std::ref(y), 2.718); // 調用y(2.718)
X baz(X&);
std::async(baz, std::ref(x)); // 調用baz(x)

我們還能為std::async()補充一個std::launch類型的參數,來指定採用哪種方式運行:std::launch::deferred指定在當前線程上延後調用任務函數,等到在future上調用了wait()get(),任務函數才會執行;std::launch::async指定必須開啟專屬的線程,在其上運行任務函數。該參數的還可以是std::launch::deferred | std::launch::async,表示由std::async()的實現自行選擇運行方式,這也是這項參數的預設值。

auto f6 = std::async(std::launch::async, Y(), 1.2); // 在新線程上執行
auto f7 = std::async(std::launch::deferred, baz, std::ref(x)); // 在wait()或get()調用時執行
auto f8 = std::async(std::launch::deferred | std::launch::async, baz, std::ref(x)); // 交由實現自行選擇執行方式
auto f9 = std::async(baz, std::ref(x));
f7.wait(); // 調用延遲函數

2.2、關聯future實例和任務

std::packaged_task<>連結future對象與函數(或可調用對象,下同)。std::packaged_task<>對象在執行任務時,會調用關聯的函數,把返回值保存為future的內部數據,並令future準備就緒。若一項龐雜的操作能分解為多個子任務,則可以把它們分別包裝到多個std::packaged_task<>實例之中,再傳遞給任務調度器或線程池,這就隱藏了細節,使任務抽象化,讓調度器得以專註處理std::packaged_task<>實例,無需糾纏於形形色色的任務函數。

std::packaged_task<>是類模板,其模板參數是函數簽名(例如void()表示一個函數,不接收參數,也沒有返回值),傳入的函數必須與之相符,即它應接收指定類型的參數,返回值也必須可以轉換成指定類型。這些類型不必嚴格匹配,若某函數接收int類型參數並返回float值,則可以為其構建std::packaged_task<double(double)>的實例,因為對應的類型可以隱式轉換。

std::packaged_task<>具有成員函數get_future(),它返回std::future<>實例,該future的特化類型取決於函數簽名指定的返回值。std::packaged_task<>還具備函數調用操作符,它的參數取決於函數簽名的參數列表。

std::mutex m;
std::deque<std::packaged_task<void()>> tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread()
{
    while (!gui_shutdown_message_received())
    {
        get_and_process_gui_message();
        std::packaged_task<void()> task;
        {
            std::lock_guard<std::mutex> lk(m);
            if (tasks.empty()) { continue; }
            task = std::move(tasks.front());
            tasks.pop_front();
        }
        task();
    }
}
std::thread gui_bg_thread(gui_thread);
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
    std::packaged_task<void()> task(f);
    std::future<void> res = task.get_future();
    std::lock_guard<std::mutex> lk(m);
    tasks.push_back(std::move(task));
    return res;
}

2.3、創建std::promise

有些任務無法以簡單的函數調用表達出來,還有一些任務的執行結果可能來自多個部分的代碼,這時可以藉助std::promise顯式地非同步求值。配對的std::promisestd::future可以實現下麵的工作機制:等待數據的線程在future上阻塞,而提供數據的線程利用相配的std::promise設定關聯的值,使future準備就緒。

若需從給定的std::promise實例獲取關聯的std::future對象,調用前者的成員函數get_future()即可,這與std::package_task一樣。promise的值通過成員函數set_value()設置,只要設置好,future即準備就緒,憑藉它就能獲取該值。如果std::promise在被銷毀時仍未曾設置值,保存的數據則由異常代替。

void f(std::promise<int> ps)
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
    ps.set_value(42);
}

int main()
{
    std::promise<int> ps;
    std::future<int> ft = ps.get_future();
    std::thread t(f, std::move(ps));
    int val = ft.get();
    std::cout << val << std::endl;
    t.join();
}

2.4、將異常保存到future中

若經由std::async()調用的函數拋出異常,則會被保存到future中,future隨之進入就緒狀態,等到其成員函數get()被調用,存儲在內的異常即被重新拋出。std::packaged_task也是同理,若包裝的任務函數在執行時拋出異常,則也會被保存到future中,只要調用get(),該異常就會被再次拋出。自然而然,std::promise也具有同樣的功能,它通過成員函數顯式調用實現。假如我們不想保存值,而想保存異常,就不應調用set_value(),而應調用成員函數set_exception()

2.5、多個線程一起等待

若我們在多個線程上訪問同一個std::future對象,而不採取額外的同步措施,將引發數據競爭並導致未定義的行為。std::future僅能移動構造和移動賦值,而std::shared_future的實例則能複製出副本。但即便改用std::shared_future,同一個對象的成員函數卻依然沒有同步,若我們從多個線程訪問同一個對象,首選方式是:向每個線程傳遞std::shared_future對象的副本,它們為各線程獨有,這些副本就作為各線程的內部數據,由標準庫正確地同步,可以安全地訪問。

future和promise都具備成員函數valid(),用於判別非同步狀態是否有效。std::shared_future的實例依據std::future的實例構造而得,前者所指向的非同步狀態由後者決定。因為std::future對象獨占非同步狀態,所以若要按預設方式構造std::shared_future對象,則須用std::move向其預設構造函數傳遞歸屬權。

std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid());
std::shared_future<int> sf(std::move(f));
assert(!f.valid());
assert(sf.valid());

std::future具有成員函數share(),直接創建新的std::shared_future對象,並向它轉移歸屬權。

std::promise<std::map<SomeIndexType, SomeDataType, SomeComparator, SomeAllocator>::iterator> p;
auto sf = p.get_future().share();

3、限時等待

有兩種超時機制可供選擇:一是延遲超時,線程根據指定的時長而繼續等待;二是絕對超時,在某個特定時間點來臨之前,線程一直等待。大部分等待函數都有變體,專門處理這兩種機制的超時。處理延遲超時的函數變體以_for為尾碼,而處理絕對超時的函數變體以_until為尾碼。例如std::condition_variable的成員函數wait_for()wait_until()


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ## 1.1、MVC 概述 - MVC:是一種軟體架構的思想,將軟體按照模型、視圖、控制器來劃分; - M( Model ):模型層,指工程中的 JavaBean ,作用是處理數據; - V( View ):視圖層,指工程中的 html 或 jsp 等頁面,作用是與用戶進行交互、展示數據; - C( ...
  • # 簡介 緩存是程式員們繞不開的話題,像是常用的本地緩存Guava,分散式緩存Redis等,是提供高性能服務的基礎。今天敬姐帶大家一起認識一個更高效的本地緩存——**Caffeine**。 ![img](https://img2023.cnblogs.com/blog/37001/202309/37 ...
  • ## 1、問題描述 - Eclipse導入了一個JavaEE項目 - 在虛擬機環境中新建了一個資料庫 - 資料庫可以使用本地客戶端工具正常連接 - 導入的JavaEE項目修改了數據源配置後無法啟動 - 相同的數據源配置通過在Idea新建的測試項目可以訪問 > 具體報錯如下: ``` java.sql ...
  • ## 1 下班前的寂靜 剛準備下班呢,測試大姐又給我提個`bug`,你看我這就操作了一次,`network`里咋有兩個請求? 我心一驚,”不可能啊!我代碼明明就調用一次後端介面,咋可能兩個請求!“。打開她的截圖一看:多個`options`請求。 我不慌不忙解釋道:”這不用管,是瀏覽器預設發送的一個預 ...
  • ## 返回值優化RVO 在cppreference中,是這麼介紹RVO的 `In a return statement, when the operand is the name of a non-volatile object with automatic storage duration, wh ...
  • 背景: 最近在寫一個介面的時候,需求是這樣的,上傳excel,匹配項目有多少個欄位匹配上了,如果匹配上了在單元格上標註綠色背景,然後返回excel文件和匹配的詳細。 首先這個excel文件,後端是不會去保存的,所以無法直接返迴文件鏈接,然後需要返回一個json,告訴前端有多少行是匹配上了的,中匹配多 ...
  • # 02-針對商品排行榜,你是怎麼實現的 ## 背景描述 當時產品提出了每日熱銷排行榜在零點進行變更的需求。在我接到這個需求後,我立即想到了使用Redis的有序集合(ZSET)來實現這個功能,並與我們的技術負責人進行了溝通。 經過與技術負責人的討論和確認,我們一致認為使用有序集合是一個可行的解決方案 ...
  • ## switch語句 使用switch語句來選擇要執行的多個代碼塊中的一個。 在Go中的switch語句類似於C、C++、Java、JavaScript和PHP中的switch語句。不同之處在於它只執行匹配的case,因此不需要使用break語句。 單一case的switch語法 ```Go sw ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...