Pemrograman asinkron di C++/CX

Catatan

Topik ini ada untuk membantu Anda mempertahankan aplikasi C++/CX Anda. Tetapi kami sarankan Anda menggunakan C++/WinRT untuk aplikasi baru. C++/WinRT adalah proyeksi bahasa C++17 modern yang sepenuhnya standar untuk API Windows Runtime (WinRT), yang diimplementasikan sebagai pustaka berbasis file header, dan dirancang untuk memberi Anda akses kelas satu ke Windows API modern.

Artikel ini menjelaskan cara yang disarankan untuk menggunakan metode asinkron di ekstensi komponen Visual C++ (C++/CX) dengan menggunakan task kelas yang ditentukan dalam concurrency namespace layanan di ppltasks.h.

Jenis asinkron Windows Runtime

Windows Runtime menampilkan model yang terdefinisi dengan baik untuk memanggil metode asinkron dan menyediakan jenis yang Anda butuhkan untuk menggunakan metode tersebut. Jika Anda tidak terbiasa dengan model asinkron Windows Runtime, baca Pemrograman Asinkron sebelum Anda membaca sisa artikel ini.

Meskipun Anda dapat menggunakan API Windows Runtime asinkron langsung di C++, pendekatan yang disukai adalah menggunakan kelas tugas dan jenis dan fungsi terkaitnya, yang terkandung dalam namespace layanan konkurensi dan didefinisikan dalam .<ppltasks.h> Konkurensi::task adalah jenis tujuan umum, tetapi ketika sakelar pengompilasi /ZW—yang diperlukan untuk aplikasi dan komponen Platform Windows Universal (UWP) —digunakan, kelas tugas merangkum jenis asinkron Windows Runtime sehingga lebih mudah untuk:

  • rantai beberapa operasi asinkron dan sinkron bersama-sama

  • menangani pengecualian dalam rantai tugas

  • melakukan pembatalan dalam rantai tugas

  • memastikan bahwa tugas individual berjalan dalam konteks utas atau apartemen yang sesuai

Artikel ini menyediakan panduan dasar tentang cara menggunakan kelas tugas dengan API asinkron Windows Runtime. Untuk dokumentasi selengkapnya tentang tugas dan metode terkait termasuk create_task, lihat Paralelisme Tugas (Concurrency Runtime).

Menggunakan operasi asinkron dengan menggunakan tugas

Contoh berikut menunjukkan cara menggunakan kelas tugas untuk menggunakan metode asinkron yang mengembalikan antarmuka IAsyncOperation dan yang operasinya menghasilkan nilai. Berikut adalah langkah-langkah dasarnya:

  1. create_task Panggil metode dan berikan objek IAsyncOperation^.

  2. Panggil tugas fungsi anggota::lalu pada tugas dan berikan lambda yang akan dipanggil ketika operasi asinkron selesai.

#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...
}

Tugas yang dibuat dan dikembalikan oleh tugas::maka fungsi dikenal sebagai kelanjutan. Argumen input (dalam hal ini) ke lambda yang disediakan pengguna adalah hasil yang dihasilkan operasi tugas ketika selesai. Ini adalah nilai yang sama yang akan diambil dengan memanggil IAsyncOperation::GetResults jika Anda menggunakan antarmuka IAsyncOperation secara langsung.

Metode task::then segera kembali, dan delegasinya tidak berjalan sampai pekerjaan asinkron berhasil diselesaikan. Dalam contoh ini, jika operasi asinkron menyebabkan pengecualian dilemparkan, atau berakhir dalam status dibatalkan sebagai akibat dari permintaan pembatalan, kelanjutan tidak akan pernah dijalankan. Nantinya, kita akan menjelaskan cara menulis kelanjutan yang dijalankan meskipun tugas sebelumnya dibatalkan atau gagal.

Meskipun Anda mendeklarasikan variabel tugas pada tumpukan lokal, variabel tersebut mengelola masa pakainya sehingga tidak dihapus sampai semua operasinya selesai dan semua referensi ke dalamnya keluar dari cakupan, bahkan jika metode kembali sebelum operasi selesai.

Membuat rantai tugas

Dalam pemrograman asinkron, umum untuk menentukan urutan operasi, juga dikenal sebagai rantai tugas, di mana setiap kelanjutan hanya dijalankan ketika yang sebelumnya selesai. Dalam beberapa kasus, tugas sebelumnya (atau antecedent) menghasilkan nilai yang diterima kelanjutan sebagai input. Dengan menggunakan metode task::then, Anda dapat membuat rantai tugas secara intuitif dan mudah; metode mengembalikan tugas<T> di mana T adalah jenis pengembalian fungsi lambda. Anda dapat menyusun beberapa kelanjutan ke dalam rantai tugas: myTask.then(…).then(…).then(…);

Rantai tugas sangat berguna ketika kelanjutan membuat operasi asinkron baru; tugas seperti itu dikenal sebagai tugas asinkron. Contoh berikut mengilustrasikan rantai tugas yang memiliki dua kelanjutan. Tugas awal memperoleh handel ke file yang ada, dan ketika operasi tersebut selesai, kelanjutan pertama memulai operasi asinkron baru untuk menghapus file. Ketika operasi selesai, kelanjutan kedua berjalan, dan menghasilkan pesan konfirmasi.

#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.");
    });
}

Contoh sebelumnya mengilustrasikan empat poin penting:

  • Kelanjutan pertama mengonversi objek IAsyncAction^ menjadi tugas batal><dan mengembalikan tugas.

  • Kelanjutan kedua tidak melakukan penanganan kesalahan, dan karenanya membatalkan tugas<> sebagai input. Ini adalah kelanjutan berbasis nilai.

  • Kelanjutan kedua tidak dijalankan hingga operasi DeleteAsync selesai.

  • Karena kelanjutan kedua berbasis nilai, jika operasi yang dimulai oleh panggilan ke DeleteAsync memberikan pengecualian, kelanjutan kedua tidak dijalankan sama sekali.

Catatan Membuat rantai tugas hanyalah salah satu cara untuk menggunakan kelas tugas untuk menyusun operasi asinkron. Anda juga dapat menyusun operasi dengan menggunakan operator gabungan dan pilihan && dan ||. Untuk informasi selengkapnya, lihat Paralelisme Tugas (Runtime Konkurensi).

Jenis pengembalian fungsi Lambda dan jenis pengembalian tugas

Dalam kelanjutan tugas, jenis pengembalian fungsi lambda dibungkus dalam objek tugas . Jika lambda mengembalikan ganda, maka jenis tugas kelanjutan adalah tugas<ganda>. Namun, objek tugas dirancang agar tidak menghasilkan jenis pengembalian berlapis yang tidak perlu. Jika lambda mengembalikan IAsyncOperation SyndicationFeed<^^>, kelanjutan mengembalikan tugas<SyndicationFeed^>, bukan tugas tugas<<SyndicationFeed^>> atau tugas<IAsyncOperation SyndicationFeed<^>^>^. Proses ini dikenal sebagai unwrapping asinkron dan juga memastikan bahwa operasi asinkron di dalam kelanjutan selesai sebelum kelanjutan berikutnya dipanggil.

Dalam contoh sebelumnya, perhatikan bahwa tugas mengembalikan tugas batal> meskipun lambdanya mengembalikan objek IAsyncInfo.< Tabel berikut ini meringkas konversi jenis yang terjadi antara fungsi lambda dan tugas penutup:

jenis pengembalian lambda .then jenis pengembalian
TResult TResult tugas<>
IAsyncOperation<TResult>^ TResult tugas<>
IAsyncOperationWithProgress<TResult, TProgress>^ TResult tugas<>
IAsyncAction^ tugas<batal>
IAsyncActionWithProgress<TProgress>^ tugas<batal>
TResult tugas<> TResult tugas<>

Membatalkan tugas

Seringkali merupakan ide yang baik untuk memberi pengguna opsi untuk membatalkan operasi asinkron. Dan dalam beberapa kasus Anda mungkin harus membatalkan operasi secara terprogram dari luar rantai tugas. Meskipun setiap jenis pengembalian *Asinkron memiliki metode Batal yang diwarisinya dari IAsyncInfo, tidak canggung untuk mengeksposnya ke metode luar. Cara yang disukai untuk mendukung pembatalan dalam rantai tugas adalah dengan menggunakan cancellation_token_source untuk membuat cancellation_token, lalu meneruskan token ke konstruktor tugas awal. Jika tugas asinkron dibuat dengan token pembatalan, dan [cancellation_token_source::cancel](/cpp/parallel/concrt/reference/cancellation-token-source-class?view=vs-2017& -view=true) dipanggil, tugas secara otomatis memanggil Batal pada operasi IAsync* dan meneruskan permintaan pembatalan ke bawah rantai kelanjutannya. Pseudocode berikut menunjukkan pendekatan dasar.

//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 ...

Saat tugas dibatalkan, pengecualian task_canceled disebarluaskan ke rantai tugas. Kelanjutan berbasis nilai tidak akan dijalankan, tetapi kelanjutan berbasis tugas akan menyebabkan pengecualian dilemparkan ketika task::get dipanggil. Jika Anda memiliki kelanjutan penanganan kesalahan, pastikan bahwa ia menangkap pengecualian task_canceled secara eksplisit. (Pengecualian ini tidak berasal dari Platform::Exception.)

Pembatalan bersifat kooperatif. Jika kelanjutan Anda melakukan beberapa pekerjaan jangka panjang selain hanya memanggil metode UWP, maka Anda bertanggung jawab untuk memeriksa status token pembatalan secara berkala dan menghentikan eksekusi jika dibatalkan. Setelah Anda membersihkan semua sumber daya yang dialokasikan dalam kelanjutan, panggil cancel_current_task untuk membatalkan tugas tersebut dan menyebarluaskan pembatalan ke kelanjutan berbasis nilai apa pun yang mengikutinya. Berikut adalah contoh lain: Anda dapat membuat rantai tugas yang mewakili hasil operasi FileSavePicker. Jika pengguna memilih tombol Batalkan, metode IAsyncInfo::Cancel tidak dipanggil. Sebaliknya, operasi berhasil tetapi mengembalikan nullptr. Kelanjutan dapat menguji parameter input dan memanggil cancel_current_task jika input adalah nullptr.

Untuk informasi selengkapnya, lihat Pembatalan di PPL

Menangani kesalahan dalam rantai tugas

Jika Anda ingin kelanjutan dijalankan bahkan jika antecedent dibatalkan atau melemparkan pengecualian, maka buat kelanjutan kelanjutan berbasis tugas dengan menentukan input ke fungsi lambdanya sebagai tugas TResult> atau tugas<batal> jika lambda tugas antecedent mengembalikan IAsyncAction^.<

Untuk menangani kesalahan dan pembatalan dalam rantai tugas, Anda tidak perlu membuat setiap tugas kelanjutan berbasis atau mengapit setiap operasi yang mungkin dilemparkan dalam try…catch blok. Sebagai gantinya, Anda dapat menambahkan kelanjutan berbasis tugas di akhir rantai dan menangani semua kesalahan di sana. Pengecualian apa pun—ini termasuk pengecualian task_canceled —akan menyebarluaskan rantai tugas dan melewati kelanjutan berbasis nilai apa pun, sehingga Anda dapat menanganinya dalam kelanjutan berbasis tugas penanganan kesalahan. Kita dapat menulis ulang contoh sebelumnya untuk menggunakan kelanjutan berbasis tugas penanganan kesalahan:

#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());
        }

    });
}

Dalam kelanjutan berbasis tugas, kami memanggil tugas fungsi anggota::get untuk mendapatkan hasil tugas. Kita masih harus memanggil tugas::get meskipun operasinya adalah IAsyncAction yang tidak menghasilkan hasil karena task::get juga mendapatkan pengecualian apa pun yang telah diangkut ke tugas. Jika tugas input menyimpan pengecualian, tugas tersebut akan dilemparkan ke panggilan ke task::get. Jika Anda tidak memanggil tugas::get, atau tidak menggunakan kelanjutan berbasis tugas di akhir rantai, atau tidak menangkap jenis pengecualian yang dilemparkan, maka unobserved_task_exception dilemparkan ketika semua referensi ke tugas telah dihapus.

Hanya menangkap pengecualian yang dapat Anda tangani. Jika aplikasi Anda mengalami kesalahan yang tidak dapat Anda pulihkan, lebih baik membiarkan aplikasi crash daripada membiarkannya terus berjalan dalam keadaan tidak diketahui. Juga, secara umum, jangan mencoba menangkap unobserved_task_exception itu sendiri. Pengecualian ini terutama ditujukan untuk tujuan diagnostik. Ketika unobserved_task_exception dilemparkan, biasanya menunjukkan bug dalam kode. Seringkali penyebabnya adalah pengecualian yang harus ditangani, atau pengecualian yang tidak dapat dipulihkan yang disebabkan oleh beberapa kesalahan lain dalam kode.

Mengelola konteks utas

UI aplikasi UWP berjalan di apartemen berulir tunggal (STA). Tugas yang lambdanya mengembalikan IAsyncAction atau IAsyncOperation sadar akan apartemen. Jika tugas dibuat di STA, maka semua kelanjutannya juga akan berjalan di dalamnya secara default, kecuali Anda menentukan sebaliknya. Dengan kata lain, seluruh rantai tugas mewarisi kesadaran apartemen dari tugas induk. Perilaku ini membantu menyederhanakan interaksi dengan kontrol UI, yang hanya dapat diakses dari STA.

Misalnya, dalam aplikasi UWP, dalam fungsi anggota kelas apa pun yang mewakili halaman XAML, Anda dapat mengisi kontrol ListBox dari dalam tugas::lalu metode tanpa harus menggunakan objek 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;
    });
}

Jika tugas tidak mengembalikan IAsyncAction atau IAsyncOperation, maka tugas tersebut tidak diketahui apartemen dan, secara default, kelanjutannya dijalankan pada utas latar belakang pertama yang tersedia.

Anda dapat mengambil alih konteks utas default untuk salah satu jenis tugas dengan menggunakan kelebihan beban tugas::lalu yang mengambil task_continuation_context. Misalnya, dalam beberapa kasus, mungkin diinginkan untuk menjadwalkan kelanjutan tugas sadar apartemen pada utas latar belakang. Dalam kasus seperti itu, Anda dapat meneruskan task_continuation_context::use_arbitrary untuk menjadwalkan pekerjaan tugas pada utas berikutnya yang tersedia di apartemen multi-utas. Ini dapat meningkatkan performa kelanjutan karena pekerjaannya tidak harus disinkronkan dengan pekerjaan lain yang terjadi pada utas UI.

Contoh berikut menunjukkan kapan berguna untuk menentukan opsi task_continuation_context::use_arbitrary , dan juga menunjukkan bagaimana konteks kelanjutan default berguna untuk menyinkronkan operasi bersamaan pada koleksi yang tidak aman. Dalam kode ini, kita mengulangi daftar URL untuk umpan RSS, dan untuk setiap URL, kita memulai operasi asinkron untuk mengambil data umpan. Kami tidak dapat mengontrol urutan pengambilan umpan, dan kami tidak benar-benar peduli. Ketika setiap operasi RetrieveFeedAsync selesai, kelanjutan pertama menerima objek SyndicationFeed^ dan menggunakannya untuk menginisialisasi objek yang ditentukan FeedData^ aplikasi. Karena masing-masing operasi ini independen dari yang lain, kita berpotensi mempercepat dengan menentukan konteks kelanjutan task_continuation_context::use_arbitrary . Namun, setelah setiap FeedData objek diinisialisasi, kita harus menambahkannya ke Vektor, yang bukan koleksi utas yang aman. Oleh karena itu, kami membuat kelanjutan dan menentukan [task_continuation_context::use_current](/cpp/parallel/concrt/reference/task-continuation-context-class?view=vs-2017& -view=true) untuk memastikan bahwa semua panggilan ke Append terjadi dalam konteks Application Single-Threaded Apartment (ASTA) yang sama. Karena task_continuation_context::use_default adalah konteks default, kita tidak perlu menentukannya secara eksplisit, tetapi kita melakukannya di sini demi kejelasan.

#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
}

Tugas berlapis, yang merupakan tugas baru yang dibuat di dalam kelanjutan, tidak mewarisi kesadaran apartemen tentang tugas awal.

Menyerahkan pembaruan kemajuan

Metode yang mendukung IAsyncOperationWithProgress atau IAsyncActionWithProgress memberikan pembaruan kemajuan secara berkala saat operasi sedang berlangsung, sebelum selesai. Pelaporan kemajuan bersifat independen dari gagasan tugas dan kelanjutan. Anda hanya menyediakan delegasi untuk properti Kemajuan objek. Penggunaan umum delegasi adalah memperbarui bilah kemajuan di UI.