在 C++ 中使用 PPL 进行异步编程
在異步系統(tǒng)中,本范例讓你將異步操作的開始與完成進(jìn)行分離。程序員啟動(dòng)操作,然后注冊(cè)回調(diào),并在結(jié)果可用時(shí)調(diào)用回調(diào)。不必等待完成意味著你可以在操作運(yùn)行期間執(zhí)行有用的工作,例如,處理消息循環(huán)或啟動(dòng)其他異步操作。如果你對(duì)所有潛在阻止的操作嚴(yán)格遵循此模式,則“毛玻璃窗口”、“旋轉(zhuǎn)的同心圓”以及其他此類現(xiàn)象都將成為歷史。正如你曾聽到的那樣,你的應(yīng)用程序?qū)⒆兊每於鲿场?/span>
在 Windows 8 中,異步操作很普遍,并且 WinRT 提供了一個(gè)新編程模型,以一致方式對(duì)異步進(jìn)行處理。
圖 1 演示了處理異步操作的基本模式。在這段代碼中,C++ 函數(shù)從文件讀取字符串。
圖 1 從文件進(jìn)行讀取
要注意的第一件事情是 ReadString 的返回類型為 void。沒錯(cuò):該函數(shù)不返回值;相反,它使用用戶提供的回調(diào),并在結(jié)果可用時(shí)調(diào)用回調(diào)。歡迎來(lái)到異步編程的世界:“別聯(lián)系我們,我們會(huì)聯(lián)系你的!”
WinRT 異步操作的分析
WinRT 中異步的核心是在 Windows::Foundation 命名空間中定義的四個(gè)接口:IAsyncOperation、IAsyncAction、IAsyncOperationWithProgress 和 IAsyncActionWithProgress。WinRT 中所有潛在阻止或長(zhǎng)期運(yùn)行的操作都被定義為異步。按照慣例,方法的名稱都以“Async”結(jié)尾,而返回類型則為四個(gè)接口中的一個(gè)。例如圖 1 所示示例中的方法 GetFileAsync,它返回 IAsyncOperation<StorageFile^>。許多異步操作不返回值,且它們的類型為 IAsyncAction。可以報(bào)告進(jìn)度的操作將通過 IAsync-OperationWithProgress 和 IAsyncActionWithProgress 公開。
要為異步操作指定完成回調(diào),可以設(shè)置 Completed 屬性。該屬性是一個(gè)接收異步接口和完成狀態(tài)的委托。盡管該委托可以使用函數(shù)指針進(jìn)行實(shí)例化,但你通常使用 lambda(我希望到現(xiàn)在為此,你已經(jīng)熟悉這部分的 C++11)。
要獲得操作的值,需要對(duì)接口調(diào)用 GetResults 方法。請(qǐng)注意,盡管這是從 GetFileAsync 調(diào)用返回給你的同樣接口,但是當(dāng)你位于完成處理程序中時(shí),你只能對(duì)它調(diào)用 GetResults。
完成委托的第二個(gè)參數(shù)是 AsyncStatus,它返回操作的狀態(tài)。在實(shí)際的應(yīng)用程序中,你將先檢查它的值再調(diào)用 GetResults。在圖 1 中,為了簡(jiǎn)單起見而省略了這部分。
你經(jīng)常會(huì)發(fā)現(xiàn),自己同時(shí)使用多個(gè)異步操作。在我的示例中,我首先獲取 StorageFile 的實(shí)例(通過調(diào)用 GetFileAsync),然后使用 OpenAsync 打開它,再獲取 IInputStream。接下來(lái),我加載數(shù)據(jù) (LoadAsync) 并使用 DataReader 進(jìn)行讀取。最后,獲取字符串并調(diào)用用戶提供的回調(diào)函數(shù)。
組合
將操作的啟動(dòng)和完成分離對(duì)于消除阻止調(diào)用非常重要。問題是撰寫多個(gè)基于回調(diào)的異步操作非常困難,并且得到的代碼很難研究和調(diào)試。必須采取措施控制隨之發(fā)生的“回調(diào)亂局”。
讓我們看一個(gè)具體的示例。我想使用之前示例中的 ReadString 函數(shù)按順序在兩個(gè)文件中進(jìn)行讀取,然后將結(jié)果連接成一個(gè)字符串。我打算再次將它實(shí)現(xiàn)為采用回調(diào)的函數(shù):
效果還不錯(cuò)吧?
如果你看不出這個(gè)解決方案存在的瑕疵,那么請(qǐng)考慮下這個(gè)問題:什么時(shí)候開始從 file2 進(jìn)行讀取?你真的需要先讀完第一個(gè)文件,再開始讀第二個(gè)文件嗎?當(dāng)然不是!積極啟動(dòng)多個(gè)異步操作并在數(shù)據(jù)傳入時(shí)進(jìn)行處理,效果要好得多。
我們來(lái)試一試。首先,因?yàn)槲也l(fā)啟動(dòng)了兩個(gè)操作,并在操作完成前從函數(shù)返回,所以我需要一個(gè)特殊的堆分配對(duì)象存放中間結(jié)果。我將它命名為 ResultHolder:
如圖 2 所示,接下來(lái)的第一個(gè)操作是設(shè)置 results->str 成員。要完成的第二個(gè)操作將用它構(gòu)成最終的結(jié)果。
圖 2 并發(fā)從兩個(gè)文件進(jìn)行讀取
大多數(shù)時(shí)候這種做法都是奏效的。該代碼有很明顯的爭(zhēng)用條件,并且它不處理錯(cuò)誤,因此我們?nèi)匀挥泻芏喙ぷ饕觥?/span>對(duì)于結(jié)合兩個(gè)操作這么簡(jiǎn)單的事情,卻用了這么多的代碼,難免會(huì)出錯(cuò)。
并行模式庫(kù)中的任務(wù)
Visual Studio 并行模式庫(kù) (PPL) 旨在讓 C++ 中異步并行程序的編寫變得簡(jiǎn)單而高效。PPL 用戶可以使用諸如任務(wù)、并行算法(例如 parallel_for 和 parallel_sort)等更高級(jí)的抽象和并發(fā)友好型容器(例如 concurrent_vector),來(lái)取代在線程和線程池級(jí)運(yùn)行。
PPL 任務(wù)類是下一版 Visual Studio 中的新增功能,它使你可以簡(jiǎn)潔地表示要異步執(zhí)行的單個(gè)工作單元。使用該功能可以按照獨(dú)立(或互相獨(dú)立)任務(wù)表達(dá)程序邏輯,然后讓運(yùn)行時(shí)以最佳方式安排這些任務(wù)。
任務(wù)之所以這么有用,是因?yàn)樗鼈兊目山M合性。在最簡(jiǎn)單的形式中,對(duì)于兩個(gè)任務(wù),可以將一個(gè)任務(wù)聲明為另一個(gè)任務(wù)的延續(xù)來(lái)按順序編寫。這看起來(lái)非常簡(jiǎn)單的結(jié)構(gòu)卻允許你以有趣的方式組合多個(gè)任務(wù)。諸如聯(lián)接和選項(xiàng)(我稍后再進(jìn)行介紹)的許多更高級(jí) PPL 構(gòu)造都是通過這個(gè)概念自我建構(gòu)的。任務(wù)延續(xù)還可用于以更簡(jiǎn)潔方式表示異步操作的完成。讓我們重新看看圖 1 中的示例,現(xiàn)在使用 PPL 任務(wù)編寫它,如圖 3 所示。
圖 3 使用嵌套的 PPL 任務(wù)從文件進(jìn)行讀取
因?yàn)槲椰F(xiàn)在使用任務(wù)而不是回調(diào)表示異步,所以用戶提供的回調(diào)消失了。該函數(shù)實(shí)際改為返回任務(wù)。
在實(shí)現(xiàn)過程中,我從 GetFileAsync 返回的異步操作創(chuàng)建了 getFileTask 任務(wù),然后將該操作的完成設(shè)置為任務(wù)的延續(xù)(使用 then 方法)。
then 方法值得仔細(xì)研究一下。該方法的參數(shù)是 lambda 表達(dá)式。實(shí)際上,參數(shù)還可以是函數(shù)指針、函數(shù)對(duì)象或 std::function 的實(shí)例,但是因?yàn)?lambda 表達(dá)式在 PPL 中十分普遍(實(shí)際上在現(xiàn)代的 C++ 中也一樣),從這里開始我將只說“l(fā)ambda”,用來(lái)表示所有類型的可調(diào)用對(duì)象。
then 方法的返回類型是某類型 T 的任務(wù)。這種類型 T 由傳遞給 then 的 lambda 返回類型決定。在最基本的形式下,當(dāng) lambda 返回類型 T 的表達(dá)式時(shí),then 方法返回 task<T>。例如,下面延續(xù)中的 lambda 返回了 int;因此,生成類型為 task<int>:
圖 3 中使用的延續(xù)類型稍有不同。它返回一個(gè)任務(wù)并執(zhí)行該任務(wù)的異步展開,所以生成類型不是 task<task<int>>,而是 task<int>:
如果所有這些讓你覺得有點(diǎn)頭大,不要緊,繼續(xù)往下看。我保證在幾個(gè)具有代表意義的示例之后,立即就會(huì)豁然開朗起來(lái)的。
任務(wù)組合
根據(jù)上面部分講述的內(nèi)容,繼續(xù)在文件讀取示例的基礎(chǔ)上進(jìn)行構(gòu)建。
前面曾提到,C++ 中函數(shù)和 lambda 的所有本地變量在返回時(shí)均已丟失。要保持該狀態(tài),你必須手動(dòng)將變量復(fù)制到堆或其他某個(gè)生存期較長(zhǎng)的存儲(chǔ)。這就是為什么我之前就創(chuàng)建了儲(chǔ)存器類。在異步運(yùn)行的 lambda 中,請(qǐng)務(wù)必小心不要通過指針或引用捕獲外圍函數(shù)的任何狀態(tài);否則,當(dāng)函數(shù)完成時(shí),你將隨指針終止于一個(gè)無(wú)效的內(nèi)存位置。
我要強(qiáng)調(diào)的是,then 方法對(duì)異步接口執(zhí)行了展開操作,我以更簡(jiǎn)潔的形式重寫了示例,然而成本只不過是引入了另一個(gè)儲(chǔ)存器結(jié)構(gòu),如圖 4 所示。
圖 4 鏈接多個(gè)任務(wù)
與圖 3 中的示例相比,這段代碼更易于閱讀,因?yàn)樗尸F(xiàn)的是按順序的步驟,而不是“樓梯式”的嵌套操作。
除了 then 方法,PPL 還具有一些其他組合構(gòu)造。其中一個(gè)是聯(lián)接操作,由 when_all 方法實(shí)現(xiàn)。when_all 方法采用一系列任務(wù)然后返回生成任務(wù),生成任務(wù)將構(gòu)成任務(wù)的所有輸出收集到 std::vector 中。對(duì)于兩個(gè)參數(shù)的一般情況,PPL 具有一個(gè)簡(jiǎn)便的表達(dá)方法:運(yùn)算符 &&。
這就是我如何使用聯(lián)接運(yùn)算符重新實(shí)現(xiàn)文件串聯(lián)方法:
選項(xiàng)操作也很有用。如果有一系列的任務(wù),選項(xiàng)(通過 when_any 方法實(shí)現(xiàn))在序列中第一個(gè)任務(wù)完成時(shí)完成。像聯(lián)接一樣,選項(xiàng)也具有一個(gè)雙參數(shù)的簡(jiǎn)便表達(dá)方法,使用運(yùn)算符 ||。
選項(xiàng)在冗余或推測(cè)執(zhí)行的情況下比較方便;你啟動(dòng)多個(gè)任務(wù),由要完成的第一個(gè)任務(wù)提供所需的結(jié)果。你還可以對(duì)操作添加超時(shí)設(shè)置 - 啟動(dòng)一個(gè)返回任務(wù)的操作,然后將它與休眠指定時(shí)間量的任務(wù)相組合。如果休眠任務(wù)先完成,就表示你的操作超時(shí),因此被放棄或取消。
PPL 具有另一個(gè)有助于任務(wù)可組合性的構(gòu)造 (task_completion_event),你可以將它用于任務(wù)與非 PPL 代碼的交互操作。task_completion_event 可以傳遞給線程或期望最后設(shè)置的 IO 完成回調(diào)。從 task_completion_event 創(chuàng)建的任務(wù)在設(shè)置 task_completion_event 之后即完成。
使用 PPL 編寫異步操作
無(wú)論何時(shí)你需要發(fā)揮硬件的最大性能,C++ 語(yǔ)言都是你的明智之選。其他語(yǔ)言在 Windows 8 中發(fā)揮各自的作用:JavaScript/HTML5 組合很適合編寫 GUI;C# 提供高效的開發(fā)人員體驗(yàn);等等。要編寫 Metro 樣式的應(yīng)用程序,請(qǐng)使用你擅長(zhǎng)的方法和你了解的方式。實(shí)際上,你可以在同一個(gè)應(yīng)用程序中使用多種語(yǔ)言。
你經(jīng)常會(huì)發(fā)現(xiàn),編寫應(yīng)用程序前端時(shí)使用 JavaScript 或 C# 等語(yǔ)言,而編寫后端組件時(shí)則使用 C++ 語(yǔ)言,以獲得最大性能。如果 C++ 組件導(dǎo)出的操作受計(jì)算限制或受 I/O 限制,最好將該操作定義為異步操作。
為實(shí)現(xiàn)之前介紹的四種 WinRT 異步接口(IAsyncOperation、IAsyncAction、IAsyncOperation-WithProgress 和 IAsyncActionWithProgress),PPL 在并發(fā)命名空間中同時(shí)定義了 create_async 方法和 progress_reporter 類。
在最簡(jiǎn)單的形式中,create_async 采用返回值的 lambda 或函數(shù)指針。lambda 的類型決定從 create_async 返回的接口的類型。
如果某個(gè)無(wú)參數(shù) lambda 返回非 void 類型 T,則 create_async 返回 IAsyncOperation<T> 的實(shí)現(xiàn)。對(duì)于返回 void 的 lambda,生成接口為 IAsyncAction。
lambda 可以采用 progress_reporter<P> 類型的參數(shù)。該類型的實(shí)例用于將類型 P 的進(jìn)度報(bào)告發(fā)布回調(diào)用方。例如,采用 progress_reporter<int> 的 lambda 可以使用整數(shù)值報(bào)告完成百分比。這種情況下,lambda 的返回類型決定生成接口是 IAsyncOperationWithProgress<T,P> 還是 IAsyncAction<P>。參見圖 5。
圖 5 在 PPL 中編寫異步操作
要向其他 WinRT 語(yǔ)言公開異步操作,請(qǐng)?jiān)谀愕?C++ 組件中定義一個(gè)公共 ref 類,并定義一個(gè)返回四個(gè)異步接口之一的函數(shù)。你可以在 PPL 示例包中找到有關(guān)混合 C++/JavaScript 應(yīng)用程序的具體示例(要獲得該示例包,請(qǐng)聯(lián)機(jī)搜索“Asynchrony with PPL”)。以下代碼段以帶進(jìn)度的異步操作公開圖像轉(zhuǎn)換例程:
如圖 6 所示,應(yīng)用程序的客戶端部分在 JavaScript 中使用 promise 對(duì)象實(shí)現(xiàn)。
圖 6 在 JavaScript 中使用圖像轉(zhuǎn)換例程
錯(cuò)誤處理和取消
留心的讀者可能已經(jīng)注意到,這種異步處理到目前為止幾乎完全不涉及任何錯(cuò)誤處理和取消。下面就立即開始討論這個(gè)主題!
文件讀取例程總會(huì)不可避免地遇到不存在的文件或因眾多原因而無(wú)法打開的文件。字典查詢功能將遇到不認(rèn)識(shí)的字詞。圖像轉(zhuǎn)換無(wú)法盡快生成結(jié)果,而被用戶取消。在這些場(chǎng)景中,操作在執(zhí)行完預(yù)期的工作之前已經(jīng)永遠(yuǎn)終止。
在現(xiàn)代的 C++ 中,異常用于指示錯(cuò)誤或其他異常條件。異常在單線程中運(yùn)行非常好:當(dāng)引發(fā)異常時(shí),堆棧隨即展開,一直展開到調(diào)用堆棧下的適當(dāng) catch 塊。加入并發(fā)后,事情就變得雜亂了,因?yàn)閺囊粋€(gè)線程生成的異常不容易被另一個(gè)線程捕獲。
考慮任務(wù)和延續(xù)任務(wù)發(fā)生了什么:當(dāng)任務(wù)的主體引發(fā)了異常時(shí),其執(zhí)行流即被中斷,并且無(wú)法生成值。如果沒有值可以傳遞給延續(xù)任務(wù),則延續(xù)任務(wù)不會(huì)運(yùn)行。即使是不生成值的 void 任務(wù),你也需要能夠告訴它之前的任務(wù)是否已成功完成。
這就是為什么存在延續(xù)任務(wù)的另一種形式:對(duì)于類型 T 的任務(wù),錯(cuò)誤處理延續(xù)任務(wù)的 lambda 采用 task<T>。要獲得之前任務(wù)生成的值,必須對(duì)參數(shù)任務(wù)調(diào)用 get 方法。如果之前的任務(wù)已成功完成,則 get 也成功完成。否則,get 方法將引發(fā)異常。
在此我想要強(qiáng)調(diào)一個(gè)重點(diǎn)。對(duì)于 PPL 中的所有任務(wù),包括從異步操作創(chuàng)建的任務(wù),對(duì)其調(diào)用 get 函數(shù)在語(yǔ)法上是有效的。然而,在結(jié)果可用之前,get 方法必須阻止調(diào)用線程,當(dāng)然,這與我們“快而流暢”的口號(hào)是矛盾的。因此,一般不鼓勵(lì)對(duì)任務(wù)調(diào)用 get 方法,并且在 STA 中禁止調(diào)用該方法(運(yùn)行時(shí)將引發(fā)“無(wú)效操作”異常)。僅當(dāng)你將任務(wù)作為延續(xù)任務(wù)的參數(shù),才能調(diào)用 get。圖 7 顯示了一個(gè)示例。
圖 7 錯(cuò)誤處理延續(xù)任務(wù)
你程序中的每個(gè)延續(xù)任務(wù)都可能是錯(cuò)誤處理延續(xù)任務(wù),你可以選擇處理所有延續(xù)任務(wù)中的異常。然而,在由多個(gè)任務(wù)組成的程序中,處理所有延續(xù)任務(wù)中的異常可能會(huì)造成過度負(fù)載。幸運(yùn)的是,這種情況不一定發(fā)生。與未處理的異常相似,沿著調(diào)用堆棧向下處理,直到找到捕獲它們的框架,由任務(wù)引發(fā)的異常可以“慢慢流向”鏈中的下一個(gè)延續(xù)任務(wù)(直到到達(dá)最后處理它們的位置)。并且必須對(duì)他們進(jìn)行處理,如果某個(gè)異常保持未處理狀態(tài)超過了任務(wù)本可以對(duì)它完成處理的生存期,則運(yùn)行時(shí)將引發(fā)“未觀察到的異常”異常。
現(xiàn)在讓我們回到文件讀取示例,并針對(duì)它討論錯(cuò)誤處理。由 WinRT 引發(fā)的所有異常都屬于類型 Platform::Exception,因此這也是我要在最后的延續(xù)任務(wù)中捕獲的內(nèi)容,如圖 8 所示。
圖 8 使用錯(cuò)誤處理從文件讀取字符串
延續(xù)任務(wù)捕獲到異常后,將視異常為“已處理”,而延續(xù)任務(wù)則返回成功完成的任務(wù)。所以,在圖 8 中,ReadStringWithErrorHandling 的調(diào)用方將無(wú)法得知文件讀取是否已成功完成。我在這里要說的是太早處理異常并不總是好事。
取消是過早終止任務(wù)的另一種形式。與 PPL 一樣,在 WinRT 中進(jìn)行取消需要雙方的協(xié)作,即操作的客戶端和操作本身。它們的作用不同:客戶端請(qǐng)求取消,而操作確認(rèn)或拒絕請(qǐng)求。由于客戶端和操作之間的自然競(jìng)爭(zhēng),因此取消請(qǐng)求并不保證一定成功。
在 PPL 中,這兩種作用分別由兩個(gè)類型表示:cancellation_token_source 和 cancellation_token。前一個(gè)類型的實(shí)例用于通過調(diào)用 cancel 方法來(lái)請(qǐng)求取消。后一個(gè)類型的實(shí)例則從 cancellation_token_source 進(jìn)行實(shí)例化,并作為最后一個(gè)參數(shù)傳遞給任務(wù)的構(gòu)造函數(shù)(then 方法)或 create_async 方法的 lambda。
在任務(wù)的主體內(nèi)部,實(shí)現(xiàn)可以通過調(diào)用 is_task_cancellation_requested 方法輪詢?nèi)∠?qǐng)求,并通過調(diào)用 cancel_current_task 方法確認(rèn)請(qǐng)求。由于 cancel_current_task 方法在封面下引發(fā)異常,因此可以在調(diào)用 cancel_current_task 之前進(jìn)行一些資源清理。圖 9 顯示了一個(gè)示例。
圖 9 任務(wù)中取消以及對(duì)取消請(qǐng)求的反應(yīng)
請(qǐng)注意,許多任務(wù)都可以通過相同的 cancellation_token_source 取消。這對(duì)于處理任務(wù)鏈和任務(wù)圖形時(shí)非常方便。你可以取消指定的 cancellation_-token_source 管理的所有任務(wù),而無(wú)需單獨(dú)地取消每一個(gè)任務(wù)。當(dāng)然,不保證所有任務(wù)都能實(shí)際響應(yīng)取消請(qǐng)求。此類任務(wù)將完成,但是它們正常(基于值)的延續(xù)任務(wù)不會(huì)運(yùn)行。錯(cuò)誤處理延續(xù)任務(wù)將運(yùn)行,但在嘗試從之前任務(wù)獲取值時(shí)將引發(fā) task_canceled 異常。
最后,讓我們看一下對(duì)生產(chǎn)方使用取消令牌。create_async 方法的 lambda 可以采用 cancellation_token 參數(shù),使用 is_canceled 方法對(duì)該參數(shù)進(jìn)行輪詢,并在響應(yīng)取消請(qǐng)求時(shí)取消該操作:
請(qǐng)注意,在任務(wù)延續(xù)的情況下,由 then 方法接收取消令牌,而對(duì)于 create_async,取消令牌則傳遞到 lambda。在后一種情況下,通過對(duì)生成的異步接口調(diào)用 cancel 方法啟動(dòng)取消,然后由 PPL 通過取消令牌直接將它插入取消請(qǐng)求。
總結(jié)
如同 Tony Hoare 曾經(jīng)嘲笑的一樣,我們需要教育我們的程序“等待快一點(diǎn)”。然而,不等待的異步編程仍然很難掌控,并且其優(yōu)勢(shì)也不是非常明顯,因此開發(fā)人員不使用它。
在 Windows 8 中,所有阻止操作都是異步的。如果你是一名 C++ 程序員,PPL 可以使異步編程非常愉快。擁抱異步世界吧,告訴你的程序等待再快一點(diǎn)!
?
趕緊下載VS11體驗(yàn)吧
http://www.microsoft.com/click/services/Redirect2.ashx?CR_CC=200098144
?
轉(zhuǎn)載于:https://www.cnblogs.com/new0801/archive/2012/03/19/6177757.html
總結(jié)
以上是生活随笔為你收集整理的在 C++ 中使用 PPL 进行异步编程的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于scws分词的一些记录
- 下一篇: c++ map的使用方法[转]