C++/CX 中的非同步程式設計
注意
本主題是為協助您維護您 C++/CX 應用程式。 但我們建議您將 C++/WinRT 用於新的應用程式。 C++/WinRT 是完全標準現代的 Windows 執行階段 (WinRT) API 的 C++17 語言投影,僅實作為標頭檔案式程式庫,以及設計用來提供您現代化 Windows API 的第一級存取。
此文章說明在 Visual C++ 元件延伸 (C++/CX) 中取用非同步方法的建議方式 (使用在 ppltasks.h 的 concurrency
命名空間中所定義的 task
類別)。
Windows 執行階段非同步類型
Windows 執行階段具有定義完善的模型來呼叫非同步方法,並提供您需要取用這類方法的類型。 如果您不熟悉Windows 執行階段非同步模型,請先閱讀 非同步程式設計,再閱讀本文的其餘部分。
雖然您可以直接在 C++ 中使用非同步Windows 執行階段 API,但慣用的方法是使用工作類別及其相關類型和函式,這些類別包含在命名空間中,並在 中 <ppltasks.h>
定義。 並行::task是一般用途類型,但當使用 通用 Windows 平臺 (UWP) 應用程式和元件所需的/ZW編譯器參數時,工作類別會封裝Windows 執行階段非同步類型,以便更容易:
鏈結多個非同步和同步作業
處理工作鏈結中的例外狀況
在工作鏈結中執行取消動作
確定個別工作會在適當的執行緒內容或 Apartment 中執行
本文提供如何將工作類別與Windows 執行階段非同步 API 搭配使用的基本指引。 如需工作及其相關方法的更完整檔,包括create_task,請參閱工作平行處理原則 (並行執行時間) 。
使用工作取用非同步作業
下列範例示範如何使用工作類別來取用非同步方法,以傳回IAsyncOperation介面,以及其作業產生值。 以下是基本步驟:
呼叫
create_task
方法,並將 IAsyncOperation^ 物件傳遞給它。呼叫成員函式task::,然後在工作上提供在非同步作業完成時所叫用的 Lambda。
#include <ppltasks.h>
using namespace concurrency;
using namespace Windows::Devices::Enumeration;
...
void App::TestAsync()
{
//Call the *Async method that starts the operation.
IAsyncOperation<DeviceInformationCollection^>^ deviceOp =
DeviceInformation::FindAllAsync();
// Explicit construction. (Not recommended)
// Pass the IAsyncOperation to a task constructor.
// task<DeviceInformationCollection^> deviceEnumTask(deviceOp);
// Recommended:
auto deviceEnumTask = create_task(deviceOp);
// Call the task's .then member function, and provide
// the lambda to be invoked when the async operation completes.
deviceEnumTask.then( [this] (DeviceInformationCollection^ devices )
{
for(int i = 0; i < devices->Size; i++)
{
DeviceInformation^ di = devices->GetAt(i);
// Do something with di...
}
}); // end lambda
// Continue doing work or return...
}
task::then函式所建立和傳回的工作稱為接續。 使用者提供的 Lambda 輸入引數 (在這個情況中) 會是工作完成後所產生的結果。 如果您直接使用 IAsyncOperation 介面,它是與呼叫 IAsyncOperation::GetResults 所擷取的值相同的值。
task::then方法會立即傳回,而且在非同步工作成功完成之前,其委派不會執行。 在這個範例中,如果非同步作業擲回例外狀況,或者因取消要求而以取消狀態結束,則永遠不會執行接續。 我們稍後會描述如何編寫即使先前工作被取消或失敗,仍會執行的接續。
雖然您在本機堆疊中宣告這個工作變數,不過它會管理自己的週期,只會在所有作業完成且其所有相關參照超出範圍後,才刪除這個工作變數 (即使作業完成前已經傳回該方法)。
建立工作鏈結
非同步程式設計中通常會定義一連串的作業 (稱為「工作鏈結」),只有上一個接續完成後才會繼續執行下一個。 有時上一個 (或「前項」) 工作會產生接續要接受的輸入值。 您可以使用task::then方法,以直覺且直接的方式建立工作鏈結;方法會傳回工作<T> where T是 Lambda 函式的傳回型別。 您可以將多個接續組成工作鏈結: myTask.then(…).then(…).then(…);
當接續建立新的非同步作業時,工作鏈結尤為實用;這種工作稱為非同步工作。 下列範例說明有兩個接續的工作鏈結。 初始工作會取得現有檔案的控制代碼,而當該作業完成時,第一個接續會啟動新的非同步作業來刪除檔案。 當作業完成時,會執行第二個接續,然後輸出一個確認訊息。
#include <ppltasks.h>
using namespace concurrency;
...
void App::DeleteWithTasks(String^ fileName)
{
using namespace Windows::Storage;
StorageFolder^ localFolder = ApplicationData::Current->LocalFolder;
auto getFileTask = create_task(localFolder->GetFileAsync(fileName));
getFileTask.then([](StorageFile^ storageFileSample) ->IAsyncAction^ {
return storageFileSample->DeleteAsync();
}).then([](void) {
OutputDebugString(L"File deleted.");
});
}
上述範例中有 4 個重點:
第一個接續 IAsyncAction^ 會將 物件轉換成<工作> 並傳回工作 task.
第二個接續不會執行錯誤處理,因此會採用void,而不是工作 < void >作為輸入。 這是一個數值型接續。
第二個接續會在DeleteAsync作業完成之前執行。
因為第二個接續是以值為基礎,所以如果呼叫DeleteAsync所啟動的作業擲回例外狀況,則第二個接續完全不會執行。
注意 建立工作鏈結只是使用 工作 類別撰寫非同步作業的其中一種方式。 您也可以使用 join 和 choice 運算子 && 以及 || 來編寫作業。 如需詳細資訊,請參閱 處理原則 (並行執行時間) 。
Lambda 函式傳回類型和工作傳回類型
在工作接續中,Lambda 函式的傳回類型會包裝在 task 物件中。 如果 Lambda 傳回雙精度浮點數,則接續工作的類型為工作 < 雙精度浮點 >數。 不過,工作物件的設計不會讓它產生不必要的巢狀傳回類型。 如果 Lambda 傳回IAsyncOperation < SyndicationFeed^ > ^,接續會< 傳回工作 SyndicationFeed^ >,而不是工作任務 SyndicationFeed^ >> 或工作 <<< IAsyncOperation < SyndicationFeed^ > ^^ > 。 此程式稱為 非同步解除包裝 ,也可確保接續內的非同步作業會在叫用下一個接續之前完成。
在上一個< 範例中,請注意,即使工作 Lambda 傳回物件,工作仍會傳回 void >。IAsyncInfo 下表摘要說明 Lambda 函式和封入工作之間的類型轉換:
Lambda 傳回類型 | .then 傳回類型 |
---|---|
TResult | task < TResult> |
IAsyncOperation < TResult > ^ | task < TResult> |
IAsyncOperationWithProgress < TResult, TProgress > ^ | task < TResult> |
IAsyncAction^ | task < void> |
IAsyncActionWithProgress < TProgress > ^ | task < void> |
task < TResult> | task < TResult> |
取消工作
讓使用者有機會取消非同步作業通常是不錯的做法。 而且有時您也可能必須從工作鏈結外,透過程式設計的方式取消作業。 雖然每個 *Async傳回類型都有一個Cancel方法,它繼承自IAsyncInfo,但對外部方法公開它會很抱歉。 在工作鏈結中支援取消的慣用方式是使用 cancellation_token_source 來建立 cancellation_token,然後將權杖傳遞至初始工作的建構函式。 如果使用取消權杖建立異步工作,且 [cancellation_token_source::cancel] ( /cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017 & -view=true) 呼叫,工作就會在IAsync*作業上自動呼叫Cancel,並將取消要求傳遞至其接續鏈結。 下列虛擬程式碼將示範一些基本的方法。
//Class member:
cancellation_token_source m_fileTaskTokenSource;
// Cancel button event handler:
m_fileTaskTokenSource.cancel();
// task chain
auto getFileTask2 = create_task(documentsFolder->GetFileAsync(fileName),
m_fileTaskTokenSource.get_token());
//getFileTask2.then ...
取消工作時, task_canceled 例外狀況會傳播到工作鏈結中。 以值為基礎的接續不會執行,但工作型接續會導致呼叫task::get時擲回例外狀況。 如果您有錯誤處理接續,請確定它會明確攔截 task_canceled 例外狀況。 (這個例外狀況不是從 Platform::Exception 衍生的)。
取消作業需要搭配其他作業來完成。 如果您的接續不僅僅叫用 UWP 方法,還執行其他長時間執行的工作,則您必須定期檢查取消權杖的狀態,如果已經取消,請停止執行。 清除接續中配置的所有資源之後,請呼叫 cancel_current_task 來取消該工作,並將取消傳播到後續的任何以值為基礎的接續。 這裡是另一個範例:您可以建立一個工作鏈結來表示 FileSavePicker 作業的結果。 如果使用者選擇 [取消]按鈕,則不會呼叫IAsyncInfo::Cancel方法。 而作業將成功,但傳回 nullptr。 接續可以測試輸入參數,並在輸入為nullptr時呼叫cancel_current_task。
如需詳細資訊,請參閱 PPL 中的取消
處理工作鏈結中的錯誤
如果您想要讓接續執行,即使前項已取消或擲回例外狀況,則如果前項工作的 Lambda 傳回IAsyncAction^,請將其 Lambda 函式的輸入指定為< 工作 TResult >或task < void >,讓接續成為工作型接續。
若要處理工作鏈結中的錯誤和取消作業,您不需要將每個接續變成工作型接續,也不必在 try…catch
區塊置入每個可能擲回的作業。 您可以在工作鏈結的尾端加上工作型接續,然後在該處處理所有的錯誤。 任何例外狀況—這包括task_canceled例外狀況—將會向下傳播工作鏈結,並略過任何以值為基礎的接續,以便您可以在錯誤處理工作型接續中處理。 我們可以重新改寫上一個範例,改用錯誤處理工作型接續:
#include <ppltasks.h>
void App::DeleteWithTasksHandleErrors(String^ fileName)
{
using namespace Windows::Storage;
using namespace concurrency;
StorageFolder^ documentsFolder = KnownFolders::DocumentsLibrary;
auto getFileTask = create_task(documentsFolder->GetFileAsync(fileName));
getFileTask.then([](StorageFile^ storageFileSample)
{
return storageFileSample->DeleteAsync();
})
.then([](task<void> t)
{
try
{
t.get();
// .get() didn' t throw, so we succeeded.
OutputDebugString(L"File deleted.");
}
catch (Platform::COMException^ e)
{
//Example output: The system cannot find the specified file.
OutputDebugString(e->Message->Data());
}
});
}
在工作型接續中,我們會呼叫成員函式task::get來取得工作的結果。 即使作業是IAsyncAction,但不會產生任何結果,我們仍必須呼叫task::get,因為 task::get也會取得已向下傳輸至工作的任何例外狀況。 如果輸入工作正在儲存例外狀況,就會在呼叫 task::get 時擲回例外狀況。 如果您未呼叫 task::get,或未在鏈結結尾使用工作型接續,或不會攔截擲回的例外狀況類型,則會在刪除工作的所有參考時擲回 unobserved_task_exception 。
只攔截您可以處理的例外狀況。 如果應用程式遇到您無法修復的錯誤,寧可讓應用程式當機,也不要在不明的狀況下繼續執行。 此外,一般而言,請勿嘗試攔截 unobserved_task_exception 本身。 這個例外狀況主要用於診斷。 擲 回unobserved_task_exception 時,通常會指出程式碼中的錯誤。 大多數的原因是發生您必須處理的例外狀況,或者是程式碼的部分錯誤導致了無法修復的例外狀況。
管理執行緒內容
UWP app 的 UI 會在單一執行緒 Apartment (STA) 中執行。 Lambda 傳回IAsyncAction或IAsyncOperation的工作是 Apartment 感知的。 如果在 STA 建立工作,則它的所有接續也會在 STA 中執行 (預設值),除非您指定別的地方。 換言之,整個工作鏈結會繼承上層作業的 Apartment 感知。 這種行為有助於簡化與 UI 控制項的互動 (從 STA 才辦得到)。
例如,在 UWP app 中,在任何代表 XAML 頁面的類別成員函式中,您可以從task::then方法內填入ListBox控制項,而不需要使用Dispatcher物件。
#include <ppltasks.h>
void App::SetFeedText()
{
using namespace Windows::Web::Syndication;
using namespace concurrency;
String^ url = "http://windowsteamblog.com/windows_phone/b/wmdev/atom.aspx";
SyndicationClient^ client = ref new SyndicationClient();
auto feedOp = client->RetrieveFeedAsync(ref new Uri(url));
create_task(feedOp).then([this] (SyndicationFeed^ feed)
{
m_TextBlock1->Text = feed->Title->Text;
});
}
如果工作未傳回IAsyncAction或IAsyncOperation,則它不會感知 Apartment,而且預設會在第一個可用的背景執行緒上執行其接續。
您可以使用task::的多載來覆寫任一種工作的預設執行緒內容,然後採用task_continuation_coNtext。 例如,在部分情況下,您可能想在背景執行緒中排程 Apartment 感知工作的接續。 在這種情況下,您可以傳遞task_continuation_coNtext::use_arbitrary,以在多執行緒 Apartment 中的下一個可用執行緒上排程工作。 這可提高接續的效能,因為它的工作不必與 UI 執行緒中發生的其他工作同步。
下列範例示範如何指定task_continuation_coNtext::use_arbitrary選項,並示範預設接續內容如何有助於同步處理非執行緒安全集合上的並行作業。 在這個程式碼中,我們為 RSS 摘要設定了 URL 清單迴圈,而我們會針對每個 URL 啟動一個非同步作業以抓取摘要資料。 我們無法控制摘要的抓取順序,但這並不重要。 當每個 RetrieveFeedAsync 作業完成時,第一個接續會接受 SyndicationFeed^ 物件,然後使用它來初始化應用程式定義的 FeedData^
物件。 由於這些作業彼此無關,因此我們可以藉由指定 task_continuation_coNtext::use_arbitrary 接續內容來加快速度。 不過,初始化每個 FeedData
物件後,我們必須將它們新增至 Vector (不是安全執行緒集合)。 因此,我們會建立接續並指定 [task_continuation_coNtext::use_current] ( /cpp/parallel/concrt/reference/task-continuation-coNtext-class?view=vs-2017 & -view=true) ,以確保所有對 Append 的呼叫都發生在相同的 Application Single-Threaded Apartment (ASTA) 內容中。 因為 task_continuation_coNtext::use_default 是預設內容,所以我們不需要明確指定它,但為了清楚起見,我們在這裡這麼做。
#include <ppltasks.h>
void App::InitDataSource(Vector<Object^>^ feedList, vector<wstring> urls)
{
using namespace concurrency;
SyndicationClient^ client = ref new SyndicationClient();
std::for_each(std::begin(urls), std::end(urls), [=,this] (std::wstring url)
{
// Create the async operation. feedOp is an
// IAsyncOperationWithProgress<SyndicationFeed^, RetrievalProgress>^
// but we don't handle progress in this example.
auto feedUri = ref new Uri(ref new String(url.c_str()));
auto feedOp = client->RetrieveFeedAsync(feedUri);
// Create the task object and pass it the async operation.
// SyndicationFeed^ is the type of the return value
// that the feedOp operation will eventually produce.
// Then, initialize a FeedData object by using the feed info. Each
// operation is independent and does not have to happen on the
// UI thread. Therefore, we specify use_arbitrary.
create_task(feedOp).then([this] (SyndicationFeed^ feed) -> FeedData^
{
return GetFeedData(feed);
}, task_continuation_context::use_arbitrary())
// Append the initialized FeedData object to the list
// that is the data source for the items collection.
// This all has to happen on the same thread.
// By using the use_default context, we can append
// safely to the Vector without taking an explicit lock.
.then([feedList] (FeedData^ fd)
{
feedList->Append(fd);
OutputDebugString(fd->Title->Data());
}, task_continuation_context::use_default())
// The last continuation serves as an error handler. The
// call to get() will surface any exceptions that were raised
// at any point in the task chain.
.then( [this] (task<void> t)
{
try
{
t.get();
}
catch(Platform::InvalidArgumentException^ e)
{
//TODO handle error.
OutputDebugString(e->Message->Data());
}
}); //end task chain
}); //end std::for_each
}
巢狀工作是建立在接續內的新工作,不會繼承初始工作的 Apartment 感知。
處理進度更新
支援 IAsyncOperationWithProgress 或 IAsyncActionWithProgress 的方法可以在作業進行時 (完成前),定期提供進度更新。 進度報告與工作和接續的概念無關。 您只是為物件的 Progress 屬性提供委派而已。 委派的典型用途是更新 UI 中的進度列。