《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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...