日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 人文社科 > 生活经验 >内容正文

生活经验

【C++】多线程与异步编程【四】

發布時間:2023/11/27 生活经验 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【C++】多线程与异步编程【四】 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

文章目錄

  • 【C++】多線程與異步編程【四】
      • 0.三問
      • 1.什么是異步編程?
      • 1.1同步與異步
      • 1.2 **阻塞與非阻塞**
      • 2、如何使用異步編程
      • 2.1 使用全局變量與條件變量傳遞結果
        • 實例1:
      • 2.2 使用promise與future傳遞結果
        • 實例2
        • 實例3
      • 2.3使用packaged_task與future傳遞結果
        • 實例4
      • 2.4 使用async傳遞結果
        • 實例5
      • 3.小結

【C++】多線程與異步編程【四】

0.三問

同步,異步,多線程之間是什么關系?異步比同步高效在哪?多線程比單線程高效在哪? 捋一下, 想一下怎么回答。

1.什么是異步編程?

1.1同步與異步

前面談到并發,互斥鎖與條件變量, 前面提到的線程同步主要是為了解決對共享數據的競爭訪問問題,所以線程同步主要是對共享數據的訪問同步化(按照既定的先后次序,一個訪問需要阻塞等待前一個訪問完成后才能開始)。這篇文章談到的異步編程主要是針對任務或線程的執行順序,也即一個任務不需要阻塞等待上一個任務執行完成后再開始執行,程序的執行順序與任務的排列順序是不一致的。下面從任務執行順序的角度解釋下同步與異步的區別:

  • 同步:就是在發出一個調用時,在沒有得到結果之前,該調用就不返回。但是一旦調用返回,就得到返回值了。換句話說,就是由調用者主動等待這個調用的結果。
  • 異步:調用在發出之后,這個調用就直接返回了,所以沒有返回結果。換句話說,當一個異步過程調用發出后,調用者不會立刻得到結果。 而是在調用發出之后,被調用者通過“狀態”、“通知”、“回調”三種途徑通知調用者。

可以使用哪一種途徑依賴于被帶調用者的實現,除非被調用者提供多種選擇,否則不受調用者控制。如果被調用者用狀態來通知,那么調用者就需要每隔一定時間檢查一次,效率就很低。如果使用通知和回調的方式,效率則很高。因為被調用者幾乎不需要做額外的操作。

舉個栗子:
你打電話問書店老板有沒有《程序員的自我修養》這本書,如果是同步通信機制,書店老板會說,你稍等,”我查一下",然后開始查啊查,等查好了(可能是5秒,也可能是一天)告訴你結果(返回結果)。而異步通信機制,書店老板直接告訴你我查一下啊,查好了打電話給你,然后直接掛電話了(不返回結果)。然后查好了,他會主動打電話給你。在這里老板通過“回電”這種方式來回調。

1.2 阻塞與非阻塞

阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態。

阻塞調用是指調用結果返回之前,當前線程會被掛起。調用線程只有在得到結果之后才會返回。

非阻塞調用指在不能立刻得到結果之前,該調用不會阻塞當前線程。

舉個栗子:

愛喝茶的老張,有兩把水壺(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺)。

  1. 老張把水壺放到火上,立等水開,然后泡茶。(同步阻塞)老張覺得自己有點傻。
  2. 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有,然后泡茶。(同步非阻塞)。
  3. 老張還是覺得自己有點傻,于是變高端了,買了把會響笛的那種水壺。水開后,能大聲發出嘀~~~的噪音。
  4. 老張把響水壺放到火上,立等水開泡茶。(異步阻塞)。
  5. 老張覺得這樣傻等意義不大,老張把響水壺放到火上,去客廳看電視,水壺響后泡茶。(異步非阻塞)。
  • 這里所謂同步異步,只是對于事件燒水和事件看電視。普通水壺,同步,指水壺燒水(線程)和來張(主線程)同時開始;響水壺,異步。雖然都能干活,但響水壺可以在自己完工之后,燒水事件,提示老張(主線程)水開了。這是普通水壺所不能及的。同步只能讓調用者去輪詢自己(情況2中),造成老張效率的低下。
  • 所謂阻塞非阻塞,僅僅對于老張的狀態(主線程)而言。立等的老張,阻塞,主線程不能進行其他工作;看電視的老張,非阻塞,主線程可以進行其他工作。
    情況1和情況3中老張就是阻塞的,主線程(老張的狀態)不能去做其他任何事情,媳婦喊他都不知道。雖然4中響水壺是異步的,可對于立等的老張沒有太大的意義。所以一般異步是配合非阻塞使用的,這樣才能發揮異步的效用。

這里針對多核的CPU在阻塞的情況下,計算量比較大的情況下,可以采用異步的方法,在將第i次的燒水和泡茶采用異步實現,使得喝茶這個目標的實現可以更加的高效。

2、如何使用異步編程

在線程庫< thread >中并沒有獲得線程執行結果的方法,通常情況下,線程調用者需要獲得線程的執行結果或執行狀態,以便后續任務的執行。那么,通過什么方式獲得被調用者的執行結果或狀態呢?

2.1 使用全局變量與條件變量傳遞結果

前面談到的條件變量具有“通知–喚醒”功能,可以把執行結果或執行狀態放入一個全局變量中,當被調用者執行完任務后,通過條件變量通知調用者結果或狀態已更新,可以使用了。

實例1:

//future1.cpp 使用全局變量傳遞被調用線程返回結果,使用條件變量通知調用線程已獲得結果#include <vector>
#include <numeric>
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <condition_variable>int res = 0;						//保存結果的全局變量
std::mutex mu;						//互斥鎖全局變量
std::condition_variable cond;       //全局條件變量void accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last)
{int sum = std::accumulate(first, last, 0);      //標準庫求和函數std::unique_lock<std::mutex> locker(mu);res = sum;locker.unlock();cond.notify_one();              // 向一個等待線程發出“條件已滿足”的通知
}int main()
{std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };std::thread work_thread(accumulate, numbers.begin(), numbers.end());std::unique_lock<std::mutex> locker(mu);//如果條件變量被喚醒,檢查結果是否被改變,為真則直接返回,為假則繼續等待cond.wait(locker, [](){ return res;});   std::cout << "result=" << res << '\n';locker.unlock();work_thread.join();//阻塞等待線程執行完成getchar();return 0;
}
result=21

從上面的代碼可以看出,雖然也實現了獲取異步任務執行結果的功能,但需要的全局變量較多,多線程間的耦合度也較高,編寫復雜程序時容易引入bug。有沒有更好的方式實現異步編程呢?C++ 11新增了一個< future >庫函數為異步編程提供了很大的便利。

2.2 使用promise與future傳遞結果

< future >頭文件功能允許對特定提供者設置的值進行異步訪問,可能在不同的線程中。
這些提供程序(要么是promise 對象,要么是packaged_task對象,或者是對異步的調用async)與future對象共享共享狀態:提供者使共享狀態就緒的點與future對象訪問共享狀態的點同步。< future >頭文件的結構如下:

詳細資料見此

實例2

std::promise< T >構造時,產生一個未就緒的共享狀態(包含存儲的T值和是否就緒的狀態)。可設置T值,并讓狀態變為ready。也可以通過產生一個future對象獲取到已就緒的共享狀態中的T值。繼續使用上面的程序示例,改為使用promise傳遞結果,修改后的代碼如下:

實例3

//future2.cpp 使用promise傳遞被調用線程返回結果,通過共享狀態變化通知調用線程已獲得結果#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>void accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last,std::promise<int> accumulate_promise)
{int sum = std::accumulate(first, last, 0);accumulate_promise.set_value(sum);  // 將結果存入,并讓共享狀態變為就緒以提醒future
}int main()
{// 演示用 promise<int> 在線程間傳遞結果。std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };std::promise<int> accumulate_promise;std::future<int> accumulate_future = accumulate_promise.get_future();std::thread work_thread(accumulate, numbers.begin(), numbers.end(),std::move(accumulate_promise));accumulate_future.wait();  //等待結果std::cout << "result=" << accumulate_future.get() << '\n';work_thread.join();  //阻塞等待線程執行完成getchar();return 0;
}
result=21

std::promise< T >對象的成員函數get_future()產生一個std::future< T >對象,代碼示例中已經展示了future對象的兩個方法:wait()與get(),下面給出更多操作函數供參考:

值得注意的是,std::future< T >在多個線程等待時,只有一個線程能獲取等待結果。當需要多個線程等待相同的事件的結果(即多處訪問同一個共享狀態),需要用std::shared_future< T >來替代std::future < T >,std::future< T >也提供了一個將future轉換為shared_future的方法f.share(),但轉換后原future狀態失效。這有點類似于智能指針std::unique_ptr< T >與std::shared_ptr< T >的關系,使用時需要留心。

2.3使用packaged_task與future傳遞結果

除了為一個任務或線程提供一個包含共享狀態的變量,還可以直接把共享狀態包裝進一個任務或線程中。這就需要借助std::packaged_task< Func >來實現了,其具體用法如下:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-CpRCBIyA-1609675314863)(C:\Users\guoqi\AppData\Roaming\Typora\typora-user-images\1609669883139.png)]

std::packaged_task< Func >構造時綁定一個函數對象,也產生一個未就緒的共享狀態。通過thread啟動或者仿函數形式啟動該函數對象。但是相比promise,沒有提供set_value()公用接口,而是當執行完綁定的函數對象,其執行結果返回值或所拋異常被存儲于能通過 std::future 對象訪問的共享狀態中。繼續使用上面的程序示例,改為使用packaged_task傳遞結果,修改后的代碼如下:

實例4

//future3.cpp 使用packaged_task傳遞被調用線程返回結果,通過共享狀態變化通知調用線程已獲得結果#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>int accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last)
{int sum = std::accumulate(first, last, 0);return sum;
}int main()
{// 演示用 packaged_task 在線程間傳遞結果。std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };std::packaged_task<int(std::vector<int>::iterator,std::vector<int>::iterator)> accumulate_task(accumulate);std::future<int> accumulate_future = accumulate_task.get_future();std::thread work_thread(std::move(accumulate_task), numbers.begin(), numbers.end());accumulate_future.wait();  //等待結果std::cout << "result=" << accumulate_future.get() << '\n';work_thread.join();  //阻塞等待線程執行完成getchar();return 0;
}
result=21

一般不同函數間傳遞數據時,主要是借助全局變量、返回值、函數參數等來實現的。上面第一種方法使用全局變量傳遞數據,會使得不同函數間的耦合度較高,不利于模塊化編程。后面兩種方法分別通過函數參數與返回值來傳遞數據,可以降低函數間的耦合度,使編程和維護更簡單快捷。

2.4 使用async傳遞結果

前面介紹的std::promise< T >與std::packaged_task< Func >已經提供了較豐富的異步編程工具,但在使用時既需要創建提供共享狀態的對象(promise與packaged_task),又需要創建訪問共享狀態的對象(future與shared_future),還是覺得使用起來不夠方便。有沒有更簡單的異步編程工具呢?future頭文件也確實封裝了更高級別的函數std::async,其具體用法如下:

  • std::future std::async(std::launch policy, Func, Args…)

std::async是一個函數而非類模板,其函數執行完后的返回值綁定給使用std::async的std::futrue對象(std::async其實是封裝了thread,packged_task的功能,使異步執行一個任務更為方便)。Func是要調用的可調用對象(function, member function, function object, lambda),Args是傳遞給Func的參數,std::launch policy是啟動策略,它控制std::async的異步行為,我們可以用三種不同的啟動策略來創建std::async:

  • std::launch::async參數 保證異步行為,即傳遞函數將在單獨的線程中執行;
  • std::launch::deferred參數 當其他線程調用get()/wait()來訪問共享狀態時,將調用非異步行為;
  • std::launch::async | std::launch::deferred參數 是默認行為(可省略)。有了這個啟動策略,它可以異步運行或不運行,這取決于系統的負載。

繼續使用上面的程序示例,改為使用std::async傳遞結果,修改后的代碼如下:

實例5

//future4.cpp 使用async傳遞被調用線程返回結果#include <vector>
#include <thread>
#include <future>
#include <numeric>
#include <iostream>
#include <chrono>int accumulate(std::vector<int>::iterator first,std::vector<int>::iterator last)
{int sum = std::accumulate(first, last, 0);return sum;
}int main()
{// 演示用 async 在線程間傳遞結果。std::vector<int> numbers = { 1, 2, 3, 4, 5, 6 };auto accumulate_future = std::async(std::launch::async, accumulate, numbers.begin(), numbers.end());		//auto可以自動推斷變量的類型std::cout << "result=" << accumulate_future.get() << '\n';getchar();return 0;
}
result=21

從上面的代碼可以看出使用std::async能在很大程度上簡少編程工作量,使我們不用關注線程創建內部細節,就能方便的獲取異步執行狀態和結果,還可以指定線程創建策略。所以,我們可以使用std::async替代線程的創建,讓它成為我們做異步操作的首選。

此外,還有什么機制可以通過底層實現,提高性能,解決鎖機制的問題,下面將會學習基于原子數據類型和對應的原子操作無鎖編程的的思想。

3.小結

  1. 多線程比單線程高效的原因就是利用了CPU的多核計算把一個大的任務分而治之從而加速任務計算。

  2. 異步比同步高效的原因是前者釋放了調用線程,讓調用線程可以做更多的事情而不至于被windows強制休眠浪費線程資源。

  3. 就能方便的獲取異步執行狀態和結果,還可以指定線程創建策略。所以,我們可以使用std::async替代線程的創建,讓它成為我們做異步操作的首選。

  4. 此外,還有什么機制可以通過底層實現,提高性能,解決鎖機制的問題,下面將會學習基于原子數據類型和對應的原子操作無鎖編程的的思想。

總結

以上是生活随笔為你收集整理的【C++】多线程与异步编程【四】的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。