C++/CX 非同步程式設計

注意

本主題可協助您維護 C++/CX 應用程式。 但我們建議您將 C++/WinRT 用於新的應用程式。 C++/WinRT 是完全標準現代的 Windows 執行階段 (WinRT) API 的 C++17 語言投影,僅實作為標頭檔案式程式庫,以及設計用來提供您現代化 Windows API 的第一級存取。

本文說明如何透過使用 ppltasks.h 的 concurrency 命名空間中定義的 task 類別,在 Visual C++ 元件擴充 (C++/CX) 中使用非同步方法。

Windows 執行階段非同步類型

Windows 執行階段具有用於呼叫非同步方法的定義良好的模型,並提供使用此類方法所需的類型。 如果您不熟悉 Windows 執行階段非同步模型,請先閱讀非同步程式設計,然後再閱讀本文的其餘部分。

雖然您可以直接在 C++ 中使用非同步 Windows 執行階段 API,但最好的方法是使用工作類別及其相關類型和函式,它們包含在並行命名空間中,並在 <ppltasks.h> 中定義。 concurrency::task 是通用類型,但是當使用通用 Windows 平台 (UWP) 應用程式和元件所需的 /ZW 編譯器開關時,工作類別會封裝 Windows 執行階段非同步類型,以便更容易:

  • 將多個非同步和同步作業連結在一起

  • 處理工作鏈結中的例外狀況

  • 在工作鏈結中執行取消

  • 確保各個工作在適當的執行緒內容或 Apartment 中執行

本文提供如何將工作類別與 Windows 執行階段非同步 API 搭配使用的基本指南。 有關工作及其相關方法 (包括 create_task) 的更完整文檔,請參閱工作平行處理原則 (並行執行階段)

透過使用工作來使用非同步作業

以下範例示範如何使用工作類別來使用非同步方法,這個方法會傳回 IAsyncOperation 介面,且其作業會產生值。 以下是基本步驟:

  1. 呼叫 create_task 方法,並向其傳遞 IAsyncOperation^ 物件。

  2. 在工作上呼叫成員函式 task::then,然後提供將在非同步作業完成時叫用的 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 方法,您可以直覺且直接地建立工作鏈結; 此方法傳回 task<T>,其中 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.");
    });
}

前述範例說明了四個要點:

  • 第一個接續會將 IAsyncAction^ 物件轉換為 task<void>,並傳回工作

  • 第二個接續不會執行錯誤處理,因此採用 void 而不是 task<void> 作為輸入。 它是值型接續。

  • DeleteAsync 作業完成之前,第二個接續不會執行。

  • 由於第二個接續是以值為基礎,因此如果透過呼叫 DeleteAsync 啟動的作業擲回例外狀況,則第二個接續根本不會執行。

注意 建立工作鏈結只是使用工作類別組成非同步作業的方法之一。 您也可以使用聯結和選擇運算子 &&|| 來組合作業。 有關詳細資訊,請參閱工作平行處理原則 (並行執行階段)

Lambda 函式會傳回類型和工作傳回類型

在工作接續中,Lambda 函式的傳回類型會包裝在工作物件中。 如果 lambda 傳回 double,則接續工作的類型為 task<double>。 但是,工作物件的設計使其不會產生不必要的巢狀傳回類型。 如果 lambda 傳回 IAsyncOperation<SyndicationFeed^>^,則接續會傳回 task<SynspirationFeed^>,而不是 task<task<SyndicationFeed^>>task<IAsyncOperation<SyndicationFeed^>^>^。 此過程稱為非同步展開,它還確保接續內的非同步作業在呼叫下一個接續之前完成。

在上述範例中,請注意工作傳回了 task<void>,即使其 lambda 傳回了 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 傳回類型都有一個從 IAsyncInfo 繼承的 Cancel 方法,但不適合將其公開給外部方法。 在工作鏈結中支援取消較好的方法是使用 cancel_token_source 建立 cancel_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 函式的輸入指定為 task<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 應用程式的 UI 會在單一執行緒 Apartment (STA) 中運作。 lambda 傳回 IAsyncActionIAsyncOperation 的工作是 Apartment 感知的。 如果工作是在 STA 中建立的,則預設其所有接續也將在其中執行,除非您另外指定。 換句話說,整個工作鏈結會從父工作繼承 Apartment 感知。 此行為有助於簡化與 UI 控制項的互動,這些控制項只能從 STA 存取。

例如,在 UWP 應用程式中,在表示 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;
    });
}

如果工作不傳回 IAsyncActionIAsyncOperation,則它不是 Apartment 感知的,並且預設情況下,其接續會在第一個可用的背景執行緒上執行。

您可以透過使用採用 task_continuation_contexttask::then 多載來覆寫任一類型工作的預設執行緒內容。 例如,在某些情況下,可能需要在後台執行緒上排程 Apartment 感知工作的接續。 在這種情況下,您可以傳遞 task_continuation_context::use_ Arbitration,將工作排程在多執行緒單元的下一個可用執行緒上。 這可以提高接續的效能,因為其工作不必與 UI 執行緒上的其他工作同步。

以下範例示範了何時指定 task_continuation_context::use_ Arbitration 選項會很有用,並且還示範了預設接續內容如何用於同步非執行緒安全集合上的並行作業。 在此程式代碼中,我們會迴圈查看 RSS 摘要的 URL 清單,並且對於每個 URL 啟動一個非同步作業來擷取摘要資料。 我們無法控制提要的擷取順序,那也不是我們在乎的事。 當每個 RetrieveFeedAsync 作業完成時,第一個接續會接受 SyndicateFeed^ 物件,並使用它來初始化應用程式定義的 FeedData^ 物件。 由於每個作業都獨立於其他作業,因此我們可以透過指定 task_continuation_context::use_ Arbitration 接續內容來加快速度。 但是,每個 FeedData 物件初始化後,我們必須將其新增至 Vector,這不是執行緒安全的集合。 因此,我們會建立一個接續並指定 [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true),以確保所有對 Append 的呼叫發生在同一個應用程式單一執行緒 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 感知。

處理進度更新

支援 IAsyncOperationWithProgressIAsyncActionWithProgress 的方法會在作業完成之前定期提供進度更新。 進度報告獨立於工作和接續的概念。 您只需為物件的 Progress 屬性提供委派即可。 委派的一般用途是更新 UI 中的進度列。