Paralelisme Tugas (Runtime Konkurensi)

Dalam Concurrency Runtime, tugas adalah unit pekerjaan yang melakukan pekerjaan tertentu dan biasanya berjalan secara paralel dengan tugas lain. Tugas dapat diurai menjadi tugas tambahan yang lebih halus yang diatur ke dalam grup tugas.

Anda menggunakan tugas saat menulis kode asinkron dan ingin beberapa operasi terjadi setelah operasi asinkron selesai. Misalnya, Anda dapat menggunakan tugas untuk membaca secara asinkron dari file lalu menggunakan tugas lain— tugas kelanjutan, yang dijelaskan nanti dalam dokumen ini—untuk memproses data setelah tersedia. Sebaliknya, Anda dapat menggunakan grup tugas untuk menguraikan pekerjaan paralel menjadi bagian yang lebih kecil. Misalnya, Anda memiliki algoritma rekursif yang membagi pekerjaan yang tersisa menjadi dua partisi. Anda dapat menggunakan grup tugas untuk menjalankan partisi ini secara bersamaan, lalu menunggu pekerjaan yang dibagi selesai.

Tip

Saat Anda ingin menerapkan rutinitas yang sama ke setiap elemen koleksi secara paralel, gunakan algoritma paralel, seperti konkurensi::p arallel_for, bukan tugas atau grup tugas. Untuk informasi selengkapnya tentang algoritma paralel, lihat Algoritma Paralel.

Poin Penting

  • Saat Anda meneruskan variabel ke ekspresi lambda dengan referensi, Anda harus menjamin bahwa masa pakai variabel tersebut bertahan hingga tugas selesai.

  • Gunakan tugas ( kelas konkurensi::tugas ) saat Anda menulis kode asinkron. Kelas tugas menggunakan Windows ThreadPool sebagai penjadwalnya, bukan Concurrency Runtime.

  • Gunakan grup tugas ( konkurensi::task_group kelas atau algoritma konkurensi::p arallel_invoke ) saat Anda ingin menguraikan pekerjaan paralel menjadi bagian yang lebih kecil dan kemudian menunggu potongan-potongan yang lebih kecil tersebut selesai.

  • Gunakan konkurensi::task::then metode untuk membuat kelanjutan. Kelanjutan adalah tugas yang berjalan secara asinkron setelah tugas lain selesai. Anda dapat menghubungkan sejumlah kelanjutan untuk membentuk rantai pekerjaan asinkron.

  • Kelanjutan berbasis tugas selalu dijadwalkan untuk eksekusi ketika tugas antecedent selesai, bahkan ketika tugas antecedent dibatalkan atau melemparkan pengecualian.

  • Gunakan konkurensi::when_all untuk membuat tugas yang selesai setelah setiap anggota serangkaian tugas selesai. Gunakan konkurensi::when_any untuk membuat tugas yang selesai setelah satu anggota sekumpulan tugas selesai.

  • Tugas dan grup tugas dapat berpartisipasi dalam mekanisme pembatalan Pustaka Pola Paralel (PPL). Untuk informasi selengkapnya, lihat Pembatalan di PPL.

  • Untuk mempelajari bagaimana runtime menangani pengecualian yang dilemparkan oleh tugas dan grup tugas, lihat Penanganan Pengecualian.

Dalam Dokumen ini

Menggunakan Ekspresi Lambda

Karena sintaksisnya yang singkat, ekspresi lambda adalah cara umum untuk menentukan pekerjaan yang dilakukan oleh tugas dan grup tugas. Berikut adalah beberapa tips penggunaan:

  • Karena tugas biasanya berjalan pada utas latar belakang, ketahui masa pakai objek saat Anda mengambil variabel dalam ekspresi lambda. Saat Anda mengambil variabel berdasarkan nilai, salinan variabel tersebut dibuat dalam bodi lambda. Saat Anda mengambil berdasarkan referensi, salinan tidak dibuat. Oleh karena itu, pastikan bahwa masa pakai variabel apa pun yang Anda ambil dengan referensi keluar dari tugas yang menggunakannya.

  • Saat Anda meneruskan ekspresi lambda ke tugas, jangan mengambil variabel yang dialokasikan pada tumpukan menurut referensi.

  • Jadilah eksplisit tentang variabel yang Anda ambil dalam ekspresi lambda sehingga Anda dapat mengidentifikasi apa yang Anda tangkap berdasarkan nilai versus berdasarkan referensi. Untuk alasan ini, kami sarankan Anda tidak menggunakan [=] opsi atau [&] untuk ekspresi lambda.

Pola umum adalah ketika satu tugas dalam rantai kelanjutan ditetapkan ke variabel, dan tugas lain membaca variabel tersebut. Anda tidak dapat mengambil berdasarkan nilai karena setiap tugas kelanjutan akan menyimpan salinan variabel yang berbeda. Untuk variabel yang dialokasikan tumpukan, Anda juga tidak dapat mengambil berdasarkan referensi karena variabel mungkin tidak lagi valid.

Untuk mengatasi masalah ini, gunakan penunjuk cerdas, seperti std::shared_ptr, untuk membungkus variabel dan meneruskan pointer pintar berdasarkan nilai. Dengan cara ini, objek yang mendasar dapat ditetapkan ke dan dibaca dari, dan akan mengungguli tugas yang menggunakannya. Gunakan teknik ini bahkan ketika variabel adalah penunjuk atau handel yang dihitung referensi (^) ke objek Windows Runtime. Berikut ini contoh dasarnya:

// lambda-task-lifetime.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <string>

using namespace concurrency;
using namespace std;

task<wstring> write_to_string()
{
    // Create a shared pointer to a string that is 
    // assigned to and read by multiple tasks.
    // By using a shared pointer, the string outlives
    // the tasks, which can run in the background after
    // this function exits.
    auto s = make_shared<wstring>(L"Value 1");

    return create_task([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value.
        *s = L"Value 2";

    }).then([s] 
    {
        // Print the current value.
        wcout << L"Current value: " << *s << endl;
        // Assign to a new value and return the string.
        *s = L"Value 3";
        return *s;
    });
}

int wmain()
{
    // Create a chain of tasks that work with a string.
    auto t = write_to_string();

    // Wait for the tasks to finish and print the result.
    wcout << L"Final value: " << t.get() << endl;
}

/* Output:
    Current value: Value 1
    Current value: Value 2
    Final value: Value 3
*/

Untuk informasi selengkapnya tentang ekspresi lambda, lihat Ekspresi Lambda.

Kelas tugas

Anda dapat menggunakan kelas konkurensi::task untuk menyusun tugas ke dalam serangkaian operasi dependen. Model komposisi ini didukung oleh gagasan kelanjutan. Kelanjutan memungkinkan kode dijalankan ketika tugas sebelumnya, atau antecedent, selesai. Hasil tugas antecedent diteruskan sebagai input ke satu atau beberapa tugas kelanjutan. Ketika tugas antecedent selesai, tugas kelanjutan apa pun yang menunggunya dijadwalkan untuk dieksekusi. Setiap tugas kelanjutan menerima salinan hasil tugas antecedent. Pada gilirannya, tugas kelanjutan tersebut mungkin juga merupakan tugas antecedent untuk kelanjutan lainnya, sehingga membuat rantai tugas. Kelanjutan membantu Anda membuat rantai tugas dengan panjang arbitrer yang memiliki dependensi tertentu di antaranya. Selain itu, tugas dapat berpartisipasi dalam pembatalan baik sebelum tugas dimulai atau dengan cara yang kooperatif saat sedang berjalan. Untuk informasi selengkapnya tentang model pembatalan ini, lihat Pembatalan di PPL.

task adalah kelas templat. Parameter T jenis adalah jenis hasil yang dihasilkan oleh tugas. Tipe ini bisa jika void tugas tidak mengembalikan nilai. T tidak dapat menggunakan pengubah const .

Saat membuat tugas, Anda menyediakan fungsi kerja yang melakukan isi tugas. Fungsi kerja ini hadir dalam bentuk fungsi lambda, penunjuk fungsi, atau objek fungsi. Untuk menunggu tugas selesai tanpa mendapatkan hasilnya, panggil metode konkurensi::task::wait . Metode mengembalikan task::waitnilai konkurensi::task_status yang menjelaskan apakah tugas selesai atau dibatalkan. Untuk mendapatkan hasil tugas, panggil metode konkurensi::task::get . Metode ini memanggil task::wait untuk menunggu tugas selesai, dan oleh karena itu memblokir eksekusi utas saat ini hingga hasilnya tersedia.

Contoh berikut menunjukkan cara membuat tugas, menunggu hasilnya, dan menampilkan nilainya. Contoh dalam dokumentasi ini menggunakan fungsi lambda karena memberikan sintaks yang lebih singkat. Namun, Anda juga dapat menggunakan penunjuk fungsi dan objek fungsi saat Anda menggunakan tugas.

// basic-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Create a task.
    task<int> t([]()
    {
        return 42;
    });

    // In this example, you don't necessarily need to call wait() because
    // the call to get() also waits for the result.
    t.wait();

    // Print the result.
    wcout << t.get() << endl;
}

/* Output:
    42
*/

Saat Anda menggunakan fungsi konkurensi::create_task , Anda dapat menggunakan auto kata kunci alih-alih mendeklarasikan jenisnya. Misalnya, pertimbangkan kode ini yang membuat dan mencetak matriks identitas:

// create-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <string>
#include <iostream>
#include <array>

using namespace concurrency;
using namespace std;

int wmain()
{
    task<array<array<int, 10>, 10>> create_identity_matrix([]
    {
        array<array<int, 10>, 10> matrix;
        int row = 0;
        for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
        {
            fill(begin(matrixRow), end(matrixRow), 0);
            matrixRow[row] = 1;
            row++;
        });
        return matrix;
    });

    auto print_matrix = create_identity_matrix.then([](array<array<int, 10>, 10> matrix)
    {
        for_each(begin(matrix), end(matrix), [](array<int, 10>& matrixRow) 
        {
            wstring comma;
            for_each(begin(matrixRow), end(matrixRow), [&comma](int n) 
            {
                wcout << comma << n;
                comma = L", ";
            });
            wcout << endl;
        });
    });

    print_matrix.wait();
}
/* Output:
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0
    0, 1, 0, 0, 0, 0, 0, 0, 0, 0
    0, 0, 1, 0, 0, 0, 0, 0, 0, 0
    0, 0, 0, 1, 0, 0, 0, 0, 0, 0
    0, 0, 0, 0, 1, 0, 0, 0, 0, 0
    0, 0, 0, 0, 0, 1, 0, 0, 0, 0
    0, 0, 0, 0, 0, 0, 1, 0, 0, 0
    0, 0, 0, 0, 0, 0, 0, 1, 0, 0
    0, 0, 0, 0, 0, 0, 0, 0, 1, 0
    0, 0, 0, 0, 0, 0, 0, 0, 0, 1
*/

Anda dapat menggunakan create_task fungsi untuk membuat operasi yang setara.

auto create_identity_matrix = create_task([]
{
    array<array<int, 10>, 10> matrix;
    int row = 0;
    for_each(begin(matrix), end(matrix), [&row](array<int, 10>& matrixRow) 
    {
        fill(begin(matrixRow), end(matrixRow), 0);
        matrixRow[row] = 1;
        row++;
    });
    return matrix;
});

Jika pengecualian dilemparkan selama eksekusi tugas, runtime marshals pengecualian tersebut dalam panggilan berikutnya ke task::get atau task::wait, atau ke kelanjutan berbasis tugas. Untuk informasi selengkapnya tentang mekanisme penanganan pengecualian tugas, lihat Penanganan Pengecualian.

Untuk contoh yang menggunakan task, konkurensi::task_completion_event, pembatalan, lihat Panduan: Koneksi Menggunakan Tugas dan Permintaan HTTP XML. (Kelas task_completion_event dijelaskan nanti dalam dokumen ini.)

Tip

Untuk mempelajari detail yang khusus untuk tugas di aplikasi UWP, lihat Pemrograman asinkron di C++ dan Membuat Operasi Asinkron di C++ untuk Aplikasi UWP.

Tugas Kelanjutan

Dalam pemrograman asinkron, sangat umum untuk satu operasi asinkron, setelah selesai, untuk memanggil operasi kedua dan meneruskan data ke dalamnya. Secara tradisional, ini dilakukan dengan menggunakan metode panggilan balik. Dalam Runtime Konkurensi, fungsionalitas yang sama disediakan oleh tugas kelanjutan. Tugas kelanjutan (juga dikenal sebagai kelanjutan) adalah tugas asinkron yang dipanggil oleh tugas lain, yang dikenal sebagai antecedent, ketika antecedent selesai. Dengan menggunakan kelanjutan, Anda dapat:

  • Meneruskan data dari anteseden ke kelanjutan.

  • Tentukan kondisi yang tepat di mana kelanjutan dipanggil atau tidak dipanggil.

  • Batalkan kelanjutan baik sebelum dimulai atau secara kooperatif saat sedang berjalan.

  • Memberikan petunjuk tentang bagaimana kelanjutan harus dijadwalkan. (Ini hanya berlaku untuk aplikasi Platform Windows Universal (UWP). Untuk informasi selengkapnya, lihat Membuat Operasi Asinkron di C++ untuk Aplikasi UWP.)

  • Memanggil beberapa kelanjutan dari anteseden yang sama.

  • Panggil satu kelanjutan ketika semua atau salah satu dari beberapa antecedent selesai.

  • Rantai kelanjutan satu demi satu untuk panjang apa pun.

  • Gunakan kelanjutan untuk menangani pengecualian yang dilemparkan oleh antecedent.

Fitur-fitur ini memungkinkan Anda menjalankan satu atau beberapa tugas saat tugas pertama selesai. Misalnya, Anda dapat membuat kelanjutan yang memadatkan file setelah tugas pertama membacanya dari disk.

Contoh berikut memodifikasi yang sebelumnya untuk menggunakan konkurensi::task::lalu metode untuk menjadwalkan kelanjutan yang mencetak nilai tugas antecedent saat tersedia.

// basic-continuation.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    {
        return 42;
    });

    t.then([](int result)
    {
        wcout << result << endl;
    }).wait();

    // Alternatively, you can chain the tasks directly and
    // eliminate the local variable.
    /*create_task([]() -> int
    {
        return 42;
    }).then([](int result)
    {
        wcout << result << endl;
    }).wait();*/
}

/* Output:
    42
*/

Anda dapat menautkan dan menumpuk tugas dengan panjang apa pun. Tugas juga dapat memiliki beberapa kelanjutan. Contoh berikut mengilustrasikan rantai kelanjutan dasar yang menaikkan nilai tugas sebelumnya tiga kali.

// continuation-chain.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]() -> int
    { 
        return 0;
    });
    
    // Create a lambda that increments its input value.
    auto increment = [](int n) { return n + 1; };

    // Run a chain of continuations and print the result.
    int result = t.then(increment).then(increment).then(increment).get();
    wcout << result << endl;
}

/* Output:
    3
*/

Kelanjutan juga dapat mengembalikan tugas lain. Jika tidak ada pembatalan, maka tugas ini dijalankan sebelum kelanjutan berikutnya. Teknik ini dikenal sebagai unwrapping asinkron. Pembongkaran asinkron berguna ketika Anda ingin melakukan pekerjaan tambahan di latar belakang, tetapi tidak ingin tugas saat ini memblokir utas saat ini. (Ini umum di aplikasi UWP, di mana kelanjutan dapat berjalan pada utas UI). Contoh berikut menunjukkan tiga tugas. Tugas pertama mengembalikan tugas lain yang dijalankan sebelum tugas kelanjutan.

// async-unwrapping.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t = create_task([]()
    {
        wcout << L"Task A" << endl;

        // Create an inner task that runs before any continuation
        // of the outer task.
        return create_task([]()
        {
            wcout << L"Task B" << endl;
        });
    });
  
    // Run and wait for a continuation of the outer task.
    t.then([]()
    {
        wcout << L"Task C" << endl;
    }).wait();
}

/* Output:
    Task A
    Task B
    Task C
*/

Penting

Ketika kelanjutan tugas mengembalikan tugas berlapis jenis N, tugas yang dihasilkan memiliki jenis N, bukan task<N>, dan selesai ketika tugas berlapis selesai. Dengan kata lain, kelanjutan melakukan pembongkaran tugas berlapis.

Kelanjutan Berbasis Nilai Versus Berbasis Tugas

task Mengingat objek yang jenis pengembaliannya adalah T, Anda dapat memberikan nilai jenis T atau task<T> tugas kelanjutannya. Kelanjutan yang mengambil jenis T dikenal sebagai kelanjutan berbasis nilai. Kelanjutan berbasis nilai dijadwalkan untuk eksekusi ketika tugas antecedent selesai tanpa kesalahan dan tidak dibatalkan. Kelanjutan yang mengambil jenis task<T> sebagai parameternya dikenal sebagai kelanjutan berbasis tugas. Kelanjutan berbasis tugas selalu dijadwalkan untuk eksekusi ketika tugas antecedent selesai, bahkan ketika tugas antecedent dibatalkan atau melemparkan pengecualian. Anda kemudian dapat memanggil task::get untuk mendapatkan hasil tugas antecedent. Jika tugas antecedent dibatalkan, task::get melemparkan konkurensi::task_canceled. Jika tugas antecedent melemparkan pengecualian, task::get melemparkan kembali pengecualian tersebut. Kelanjutan berbasis tugas tidak ditandai sebagai dibatalkan ketika tugas antecedent-nya dibatalkan.

Menyusun Tugas

Bagian ini menjelaskan fungsi konkurensi::when_all dan konkurensi::when_any , yang dapat membantu Anda menyusun beberapa tugas untuk menerapkan pola umum.

Fungsi when_all

Fungsi ini when_all menghasilkan tugas yang selesai setelah serangkaian tugas selesai. Fungsi ini mengembalikan objek std::vector yang berisi hasil setiap tugas dalam set. Contoh dasar berikut menggunakan when_all untuk membuat tugas yang mewakili penyelesaian tiga tugas lainnya.

// join-tasks.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks.
    array<task<void>, 3> tasks = 
    {
        create_task([] { wcout << L"Hello from taskA." << endl; }),
        create_task([] { wcout << L"Hello from taskB." << endl; }),
        create_task([] { wcout << L"Hello from taskC." << endl; })
    };

    auto joinTask = when_all(begin(tasks), end(tasks));

    // Print a message from the joining thread.
    wcout << L"Hello from the joining thread." << endl;

    // Wait for the tasks to finish.
    joinTask.wait();
}

/* Sample output:
    Hello from the joining thread.
    Hello from taskA.
    Hello from taskC.
    Hello from taskB.
*/

Catatan

Tugas yang Anda lewati when_all harus seragam. Dengan kata lain, mereka semua harus mengembalikan jenis yang sama.

Anda juga dapat menggunakan && sintaks untuk menghasilkan tugas yang selesai setelah serangkaian tugas selesai, seperti yang diperlihatkan dalam contoh berikut.

auto t = t1 && t2; // same as when_all

Adalah umum untuk menggunakan kelanjutan bersama dengan when_all untuk melakukan tindakan setelah serangkaian tugas selesai. Contoh berikut memodifikasi yang sebelumnya untuk mencetak jumlah tiga tugas yang masing-masing menghasilkan int hasil.

// Start multiple tasks.
array<task<int>, 3> tasks =
{
    create_task([]() -> int { return 88; }),
    create_task([]() -> int { return 42; }),
    create_task([]() -> int { return 99; })
};

auto joinTask = when_all(begin(tasks), end(tasks)).then([](vector<int> results)
{
    wcout << L"The sum is " 
          << accumulate(begin(results), end(results), 0)
          << L'.' << endl;
});

// Print a message from the joining thread.
wcout << L"Hello from the joining thread." << endl;

// Wait for the tasks to finish.
joinTask.wait();

/* Output:
    Hello from the joining thread.
    The sum is 229.
*/

Dalam contoh ini, Anda juga dapat menentukan task<vector<int>> untuk menghasilkan kelanjutan berbasis tugas.

Jika ada tugas dalam sekumpulan tugas yang dibatalkan atau melempar pengecualian, when_all segera selesai dan tidak menunggu tugas yang tersisa selesai. Jika pengecualian dilemparkan, runtime menumbuhkan kembali pengecualian saat Anda memanggil task::get atau task::wait pada objek tugas yang when_all kembali. Jika lebih dari satu tugas dilemparkan, runtime memilih salah satunya. Oleh karena itu, pastikan Anda mengamati semua pengecualian setelah semua tugas selesai; pengecualian tugas yang tidak tertangani menyebabkan aplikasi dihentikan.

Berikut adalah fungsi utilitas yang dapat Anda gunakan untuk memastikan bahwa program Anda mengamati semua pengecualian. Untuk setiap tugas dalam rentang yang disediakan, observe_all_exceptions memicu pengecualian apa pun yang terjadi untuk ditukarkan kembali dan kemudian menelan pengecualian tersebut.

// Observes all exceptions that occurred in all tasks in the given range.
template<class T, class InIt> 
void observe_all_exceptions(InIt first, InIt last) 
{
    std::for_each(first, last, [](concurrency::task<T> t)
    {
        t.then([](concurrency::task<T> previousTask)
        {
            try
            {
                previousTask.get();
            }
            // Although you could catch (...), this demonstrates how to catch specific exceptions. Your app
            // might handle different exception types in different ways.
            catch (Platform::Exception^)
            {
                // Swallow the exception.
            }
            catch (const std::exception&)
            {
                // Swallow the exception.
            }
        });
    });
}

Pertimbangkan aplikasi UWP yang menggunakan C++ dan XAML dan menulis sekumpulan file ke disk. Contoh berikut menunjukkan cara menggunakan when_all dan observe_all_exceptions untuk memastikan bahwa program mengamati semua pengecualian.

// Writes content to files in the provided storage folder.
// The first element in each pair is the file name. The second element holds the file contents.
task<void> MainPage::WriteFilesAsync(StorageFolder^ folder, const vector<pair<String^, String^>>& fileContents)
{
    // For each file, create a task chain that creates the file and then writes content to it. Then add the task chain to a vector of tasks.
    vector<task<void>> tasks;
    for (auto fileContent : fileContents)
    {
        auto fileName = fileContent.first;
        auto content = fileContent.second;

        // Create the file. The CreationCollisionOption::FailIfExists flag specifies to fail if the file already exists.
        tasks.emplace_back(create_task(folder->CreateFileAsync(fileName, CreationCollisionOption::FailIfExists)).then([content](StorageFile^ file)
        {
            // Write its contents.
            return create_task(FileIO::WriteTextAsync(file, content));
        }));
    }

    // When all tasks finish, create a continuation task that observes any exceptions that occurred.
    return when_all(begin(tasks), end(tasks)).then([tasks](task<void> previousTask)
    {
        task_status status = completed;
        try
        {
            status = previousTask.wait();
        }
        catch (COMException^ e)
        {
            // We'll handle the specific errors below.
        }
        // TODO: If other exception types might happen, add catch handlers here.

        // Ensure that we observe all exceptions.
        observe_all_exceptions<void>(begin(tasks), end(tasks));

        // Cancel any continuations that occur after this task if any previous task was canceled.
        // Although cancellation is not part of this example, we recommend this pattern for cases that do.
        if (status == canceled)
        {
            cancel_current_task();
        }
    });
}
Untuk menjalankan contoh ini
  1. Di MainPage.xaml, tambahkan Button kontrol.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
  1. Di MainPage.xaml.h, tambahkan deklarasi penerusan ini ke private bagian MainPage deklarasi kelas.
void Button_Click(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e);
concurrency::task<void> WriteFilesAsync(Windows::Storage::StorageFolder^ folder, const std::vector<std::pair<Platform::String^, Platform::String^>>& fileContents);
  1. Di MainPage.xaml.cpp, terapkan penanganan Button_Click aktivitas.
// A button click handler that demonstrates the scenario.
void MainPage::Button_Click(Object^ sender, RoutedEventArgs^ e)
{
    // In this example, the same file name is specified two times. WriteFilesAsync fails if one of the files already exists.
    vector<pair<String^, String^>> fileContents;
    fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 1")));
    fileContents.emplace_back(make_pair(ref new String(L"file2.txt"), ref new String(L"Contents of file 2")));
    fileContents.emplace_back(make_pair(ref new String(L"file1.txt"), ref new String(L"Contents of file 3")));

    Button1->IsEnabled = false; // Disable the button during the operation.
    WriteFilesAsync(ApplicationData::Current->TemporaryFolder, fileContents).then([this](task<void> previousTask)
    {
        try
        {
            previousTask.get();
        }
        // Although cancellation is not part of this example, we recommend this pattern for cases that do.
        catch (const task_canceled&)
        {
            // Your app might show a message to the user, or handle the error in some other way.
        }

        Button1->IsEnabled = true; // Enable the button.
    });
}
  1. Di MainPage.xaml.cpp, terapkan WriteFilesAsync seperti yang ditunjukkan dalam contoh.

Tip

when_all adalah fungsi non-pemblokiran yang menghasilkan task sebagai hasilnya. Tidak seperti task::wait, aman untuk memanggil fungsi ini di aplikasi UWP pada utas ASTA (Application STA).

Fungsi when_any

Fungsi ini when_any menghasilkan tugas yang selesai ketika tugas pertama dalam sekumpulan tugas selesai. Fungsi ini mengembalikan objek std::p air yang berisi hasil tugas yang diselesaikan dan indeks tugas tersebut dalam set.

Fungsi when_any ini sangat berguna dalam skenario berikut:

  • Operasi redundan. Pertimbangkan algoritma atau operasi yang dapat dilakukan dalam banyak hal. Anda dapat menggunakan when_any fungsi untuk memilih operasi yang selesai terlebih dahulu lalu membatalkan operasi yang tersisa.

  • Operasi yang Diselingi. Anda dapat memulai beberapa operasi yang harus diselesaikan dan menggunakan when_any fungsi untuk memproses hasil saat setiap operasi selesai. Setelah satu operasi selesai, Anda dapat memulai satu atau beberapa tugas tambahan.

  • Operasi yang dibatasi. Anda dapat menggunakan when_any fungsi untuk memperluas skenario sebelumnya dengan membatasi jumlah operasi bersamaan.

  • Operasi kedaluwarsa. Anda dapat menggunakan when_any fungsi untuk memilih antara satu atau beberapa tugas dan tugas yang selesai setelah waktu tertentu.

when_allSeperti halnya , adalah umum untuk menggunakan kelanjutan yang harus when_any melakukan tindakan ketika yang pertama dalam satu set tugas selesai. Contoh dasar berikut menggunakan when_any untuk membuat tugas yang selesai saat tugas pertama dari tiga tugas lainnya selesai.

// select-task.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <array>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Start multiple tasks.
    array<task<int>, 3> tasks = {
        create_task([]() -> int { return 88; }),
        create_task([]() -> int { return 42; }),
        create_task([]() -> int { return 99; })
    };

    // Select the first to finish.
    when_any(begin(tasks), end(tasks)).then([](pair<int, size_t> result)
    {
        wcout << "First task to finish returns "
              << result.first
              << L" and has index "
              << result.second
              << L'.' << endl;
    }).wait();
}

/* Sample output:
    First task to finish returns 42 and has index 1.
*/

Dalam contoh ini, Anda juga dapat menentukan task<pair<int, size_t>> untuk menghasilkan kelanjutan berbasis tugas.

Catatan

when_allSeperti halnya , tugas yang Anda teruskan ke when_any semuanya harus mengembalikan jenis yang sama.

Anda juga dapat menggunakan || sintaks untuk menghasilkan tugas yang selesai setelah tugas pertama dalam serangkaian tugas selesai, seperti yang diperlihatkan dalam contoh berikut.

auto t = t1 || t2; // same as when_any

Tip

when_allSeperti halnya , when_any tidak memblokir dan aman untuk dipanggil di aplikasi UWP pada utas ASTA.

Eksekusi Tugas Tertunda

Terkadang perlu untuk menunda eksekusi tugas sampai kondisi terpenuhi, atau untuk memulai tugas sebagai respons terhadap peristiwa eksternal. Misalnya, dalam pemrograman asinkron, Anda mungkin harus memulai tugas sebagai respons terhadap peristiwa penyelesaian I/O.

Dua cara untuk menyelesaikannya adalah dengan menggunakan kelanjutan atau untuk memulai tugas dan menunggu peristiwa di dalam fungsi kerja tugas. Namun, ada kasus di mana tidak mungkin untuk menggunakan salah satu teknik ini. Misalnya, untuk membuat kelanjutan, Anda harus memiliki tugas antecedent. Namun, jika Anda tidak memiliki tugas antecedent, Anda dapat membuat peristiwa penyelesaian tugas dan rantai selanjutnya yang menyelesaikan peristiwa ke tugas anteks ketika tersedia. Selain itu, karena tugas tunggu juga memblokir utas, Anda dapat menggunakan peristiwa penyelesaian tugas untuk melakukan pekerjaan ketika operasi asinkron selesai, dan dengan demikian membebaskan utas.

Kelas konkurensi::task_completion_event membantu menyederhanakan komposisi tugas tersebut. task Seperti kelas , parameter T jenis adalah jenis hasil yang dihasilkan oleh tugas. Tipe ini bisa jika void tugas tidak mengembalikan nilai. T tidak dapat menggunakan pengubah const . Biasanya, task_completion_event objek disediakan untuk utas atau tugas yang akan memberi sinyal ketika nilai untuknya tersedia. Pada saat yang sama, satu atau beberapa tugas diatur sebagai pendengar peristiwa tersebut. Saat acara diatur, tugas pendengar selesai dan kelanjutannya dijadwalkan untuk dijalankan.

Untuk contoh yang menggunakan task_completion_event untuk mengimplementasikan tugas yang selesai setelah penundaan, lihat Cara: Membuat Tugas yang Selesai Setelah Penundaan.

Grup Tugas

Grup tugas mengatur kumpulan tugas. Grup tugas mendorong tugas ke antrean pencurian kerja. Penjadwal menghapus tugas dari antrean ini dan menjalankannya pada sumber daya komputasi yang tersedia. Setelah Anda menambahkan tugas ke grup tugas, Anda bisa menunggu semua tugas selesai atau membatalkan tugas yang belum dimulai.

PPL menggunakan kelas konkurensi::task_group dan konkurensi::structured_task_group untuk mewakili grup tugas, dan kelas konkurensi::task_handle untuk mewakili tugas yang berjalan dalam grup ini. Kelas task_handle merangkum kode yang melakukan pekerjaan. task Seperti kelas , fungsi kerja hadir dalam bentuk fungsi lambda, penunjuk fungsi, atau objek fungsi. Anda biasanya tidak perlu bekerja dengan task_handle objek secara langsung. Sebagai gantinya, Anda meneruskan fungsi kerja ke grup tugas, dan grup tugas membuat dan mengelola task_handle objek.

PPL membagi grup tugas menjadi dua kategori ini: grup tugas yang tidak terstruktur dan grup tugas terstruktur. PPL menggunakan task_group kelas untuk mewakili grup tugas yang tidak terstruktur dan structured_task_group kelas untuk mewakili grup tugas terstruktur.

Penting

PPL juga mendefinisikan algoritma konkurensi::p arallel_invoke , yang menggunakan structured_task_group kelas untuk menjalankan serangkaian tugas secara paralel. parallel_invoke Karena algoritma memiliki sintaks yang lebih singkat, kami sarankan Anda menggunakannya alih-alih structured_task_group kelas ketika Anda bisa. Topik Algoritma Paralel menjelaskan parallel_invoke secara lebih rinci.

Gunakan parallel_invoke saat Anda memiliki beberapa tugas independen yang ingin Anda jalankan secara bersamaan, dan Anda harus menunggu semua tugas selesai sebelum melanjutkan. Teknik ini sering disebut sebagai fork dan bergabung dengan paralelisme. Gunakan task_group saat Anda memiliki beberapa tugas independen yang ingin Anda jalankan pada saat yang sama, tetapi Anda ingin menunggu tugas selesai di lain waktu. Misalnya, Anda dapat menambahkan tugas ke task_group objek dan menunggu tugas selesai di fungsi lain atau dari utas lain.

Grup tugas mendukung konsep pembatalan. Pembatalan memungkinkan Anda memberi sinyal ke semua tugas aktif yang ingin Anda batalkan operasi keseluruhannya. Pembatalan juga mencegah tugas yang belum dimulai dari awal. Untuk informasi selengkapnya tentang pembatalan, lihat Pembatalan di PPL.

Runtime juga menyediakan model penanganan pengecualian yang memungkinkan Anda untuk melempar pengecualian dari tugas dan menangani pengecualian tersebut saat Anda menunggu grup tugas terkait selesai. Untuk informasi selengkapnya tentang model penanganan pengecualian ini, lihat Penanganan Pengecualian.

Membandingkan task_group dengan structured_task_group

Meskipun kami menyarankan agar Anda menggunakan task_group atau parallel_invoke bukan structured_task_group kelas, ada kasus di mana Anda ingin menggunakan structured_task_group, misalnya, ketika Anda menulis algoritma paralel yang melakukan jumlah variabel tugas atau memerlukan dukungan untuk pembatalan. Bagian ini menjelaskan perbedaan antara task_group kelas dan structured_task_group .

Kelas task_group ini aman untuk utas. Oleh karena itu Anda dapat menambahkan tugas ke task_group objek dari beberapa utas dan menunggu atau membatalkan task_group objek dari beberapa utas. Konstruksi dan penghancuran structured_task_group objek harus terjadi dalam cakupan leksikal yang sama. Selain itu, semua operasi pada structured_task_group objek harus terjadi pada utas yang sama. Pengecualian untuk aturan ini adalah konkurensi::structured_task_group::cancel dan konkurensi::structured_task_group::is_canceling metode. Tugas anak dapat memanggil metode ini untuk membatalkan grup tugas induk atau memeriksa pembatalan kapan saja.

Anda dapat menjalankan tugas tambahan pada task_group objek setelah memanggil metode konkurensi::task_group::wait atau concurrency::task_group::run_and_wait . Sebaliknya, jika Anda menjalankan tugas tambahan pada structured_task_group objek setelah Anda memanggil metode konkurensi::structured_task_group::wait atau concurrency::structured_task_group::run_and_wait , maka perilaku tidak terdefinisi.

structured_task_group Karena kelas tidak disinkronkan di seluruh utas, kelas memiliki overhead eksekusi yang lebih sedikit daripada task_group kelas . Oleh karena itu, jika masalah Anda tidak mengharuskan Anda menjadwalkan pekerjaan dari beberapa utas dan Anda tidak dapat menggunakan parallel_invoke algoritma, structured_task_group kelas dapat membantu Anda menulis kode yang berkinerja lebih baik.

Jika Anda menggunakan satu structured_task_group objek di dalam objek lain structured_task_group , objek dalam harus selesai dan dihancurkan sebelum objek luar selesai. Kelas task_group tidak mengharuskan grup tugas berlapis selesai sebelum grup luar selesai.

Grup tugas yang tidak terstruktur dan grup tugas terstruktur bekerja dengan penanganan tugas dengan cara yang berbeda. Anda dapat meneruskan fungsi kerja langsung ke task_group objek; task_group objek akan membuat dan mengelola handel tugas untuk Anda. Kelas structured_task_group mengharuskan Anda mengelola task_handle objek untuk setiap tugas. Setiap task_handle objek harus tetap valid selama masa pakai objek terkait structured_task_group . Gunakan fungsi konkurensi::make_task untuk membuat objek, seperti yang task_handle ditunjukkan dalam contoh dasar berikut:

// make-task-structure.cpp
// compile with: /EHsc
#include <ppl.h>

using namespace concurrency;

int wmain()
{
   // Use the make_task function to define several tasks.
   auto task1 = make_task([] { /*TODO: Define the task body.*/ });
   auto task2 = make_task([] { /*TODO: Define the task body.*/ });
   auto task3 = make_task([] { /*TODO: Define the task body.*/ });

   // Create a structured task group and run the tasks concurrently.

   structured_task_group tasks;

   tasks.run(task1);
   tasks.run(task2);
   tasks.run_and_wait(task3);
}

Untuk mengelola penanganan tugas untuk kasus di mana Anda memiliki jumlah variabel tugas, gunakan rutinitas alokasi tumpukan seperti _malloca atau kelas kontainer, seperti std::vector.

Pembatalan task_group dukungan dan structured_task_group keduanya. Untuk informasi selengkapnya tentang pembatalan, lihat Pembatalan di PPL.

Contoh

Contoh dasar berikut menunjukkan cara bekerja dengan grup tugas. Contoh ini menggunakan parallel_invoke algoritma untuk melakukan dua tugas secara bersamaan. Setiap tugas menambahkan sub-tugas ke task_group objek. Perhatikan bahwa task_group kelas memungkinkan beberapa tugas untuk menambahkan tugas ke dalamnya secara bersamaan.

// using-task-groups.cpp
// compile with: /EHsc
#include <ppl.h>
#include <sstream>
#include <iostream>

using namespace concurrency;
using namespace std;

// Prints a message to the console.
template<typename T>
void print_message(T t)
{
   wstringstream ss;
   ss << L"Message from task: " << t << endl;
   wcout << ss.str(); 
}

int wmain()
{  
   // A task_group object that can be used from multiple threads.
   task_group tasks;

   // Concurrently add several tasks to the task_group object.
   parallel_invoke(
      [&] {
         // Add a few tasks to the task_group object.
         tasks.run([] { print_message(L"Hello"); });
         tasks.run([] { print_message(42); });
      },
      [&] {
         // Add one additional task to the task_group object.
         tasks.run([] { print_message(3.14); });
      }
   );

   // Wait for all tasks to finish.
   tasks.wait();
}

Berikut ini adalah contoh output untuk contoh ini:

Message from task: Hello
Message from task: 3.14
Message from task: 42

parallel_invoke Karena algoritma menjalankan tugas secara bersamaan, urutan pesan output dapat bervariasi.

Untuk contoh lengkap yang menunjukkan cara menggunakan parallel_invoke algoritma, lihat Cara: Menggunakan parallel_invoke untuk Menulis Rutinitas Pengurutan Paralel dan Cara: Menggunakan parallel_invoke untuk Menjalankan Operasi Paralel. Untuk contoh lengkap yang menggunakan task_group kelas untuk mengimplementasikan futures asinkron, lihat Panduan: Menerapkan Futures.

Pemrograman yang Kuat

Pastikan Anda memahami peran penanganan pembatalan dan pengecualian saat Anda menggunakan tugas, grup tugas, dan algoritma paralel. Misalnya, di pohon pekerjaan paralel, tugas yang dibatalkan mencegah tugas anak berjalan. Ini dapat menyebabkan masalah jika salah satu tugas anak melakukan operasi yang penting untuk aplikasi Anda, seperti membebaskan sumber daya. Selain itu, jika tugas anak melemparkan pengecualian, pengecualian tersebut dapat disebarluaskan melalui destruktor objek dan menyebabkan perilaku yang tidak ditentukan dalam aplikasi Anda. Untuk contoh yang mengilustrasikan poin-poin ini, lihat bagian Memahami bagaimana Penanganan Pembatalan dan Pengecualian Memengaruhi Penghancuran Objek di Praktik Terbaik dalam dokumen Pustaka Pola Paralel. Untuk informasi selengkapnya tentang model pembatalan dan penanganan pengecualian di PPL, lihat Penanganan Pembatalan dan Pengecualian.

Judul Deskripsi
Cara: Menggunakan parallel_invoke untuk Menulis Rutinitas Pengurutan Paralel Memperlihatkan cara menggunakan parallel_invoke algoritma untuk meningkatkan performa algoritma pengurutan bitonik.
Cara: Menggunakan parallel_invoke untuk Menjalankan Operasi Paralel Memperlihatkan cara menggunakan parallel_invoke algoritma untuk meningkatkan performa program yang melakukan beberapa operasi pada sumber data bersama.
Cara: Membuat Tugas yang Selesai Setelah Penundaan Memperlihatkan cara menggunakan taskkelas , cancellation_token_source, cancellation_token, dan task_completion_event untuk membuat tugas yang selesai setelah penundaan.
Panduan: Menerapkan Futures Memperlihatkan cara menggabungkan fungsionalitas yang ada dalam Runtime Konkurensi menjadi sesuatu yang melakukan lebih banyak hal.
Parallel Patterns Library (PPL) Menjelaskan PPL, yang menyediakan model pemrograman imperatif untuk mengembangkan aplikasi bersamaan.

Referensi

task Class (Concurrency Runtime)

Kelas task_completion_event

Fungsi when_all

Fungsi when_any

Kelas task_group

Fungsi parallel_invoke

Kelas structured_task_group