Paralelisme Tugas (Konkurensi Runtime)

Dalam Concurrency Runtime, tugas adalah unit kerja yang melakukan pekerjaan tertentu dan biasanya berjalan secara paralel dengan tugas lain. Sebuah tugas dapat diuraikan menjadi tugas tambahan yang lebih halus yang diatur ke dalam kelompok 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 dan kemudian 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 potongan-potongan yang lebih kecil. Misalnya, misalkan 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 terbagi selesai.

Tip

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

Poin Penting

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

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

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

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

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

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

  • Tugas dan grup tugas dapat berpartisipasi dalam mekanisme pembatalan Parallel Patterns Library (PPL). Untuk informasi selengkapnya, lihat Pembatalan di PPL.

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

Dalam Dokumen ini

Menggunakan Ekspresi Lambda

Karena sintaks ringkasnya, 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, waspadai masa pakai objek saat Anda menangkap variabel dalam ekspresi lambda. Saat Anda menangkap variabel berdasarkan nilai, salinan variabel tersebut dibuat di badan lambda. Saat Anda menangkap dengan referensi, salinan tidak dibuat. Oleh karena itu, pastikan bahwa masa pakai variabel apa pun yang Anda ambil dengan referensi hidup lebih lama dari tugas yang menggunakannya.

  • Saat Anda meneruskan ekspresi lambda ke tugas, jangan menangkap 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 dengan referensi. Untuk alasan ini kami sarankan Anda tidak menggunakan [=] atau [&] opsi untuk ekspresi lambda.

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

Untuk mengatasi masalah ini, gunakan penunjuk cerdas, seperti std::shared_ptr, untuk membungkus variabel dan meneruskan penunjuk cerdas berdasarkan nilai. Dengan cara ini, objek yang mendasarinya dapat ditugaskan dan dibaca, dan akan hidup lebih lama dari tugas-tugas yang menggunakannya. Gunakan teknik ini bahkan ketika variabel adalah pointer atau pegangan yang dihitung referensi (^) ke objek Windows 运行时. Berikut adalah contoh dasar:

// 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::tugas untuk menyusun tugas ke dalam serangkaian operasi dependen. Model komposisi ini didukung oleh gagasan kelanjutan. Kelanjutan memungkinkan kode dijalankan ketika tugas sebelumnya, atau pendahulu, selesai. Hasil dari tugas anteseden dilewatkan sebagai input ke satu atau lebih tugas kelanjutan. Ketika tugas pendahulu selesai, setiap tugas kelanjutan yang menunggunya dijadwalkan untuk dieksekusi. Setiap tugas kelanjutan menerima salinan hasil tugas anteseden. Pada gilirannya, tugas-tugas kelanjutan tersebut juga dapat menjadi tugas pendahulu untuk kelanjutan lainnya, sehingga menciptakan rantai tugas. Kelanjutan membantu Anda membuat rantai tugas yang sewenang-wenang yang memiliki dependensi tertentu di antara mereka. Selain itu, tugas dapat berpartisipasi dalam pembatalan baik sebelum tugas dimulai atau dengan cara kooperatif saat sedang berjalan. Untuk informasi selengkapnya tentang model pembatalan ini, lihat Pembatalan di PPL.

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

Saat Anda membuat tugas, Anda menyediakan fungsi kerja yang melakukan tugas tubuh. Fungsi kerja ini datang dalam bentuk fungsi lambda, penunjuk fungsi, atau objek fungsi. Untuk menunggu tugas selesai tanpa mendapatkan hasilnya, hubungi metode konkurensi::task::wait . Metode mengembalikan task::wait nilai konkurensi::task_status yang menjelaskan apakah tugas selesai atau dibatalkan. Untuk mendapatkan hasil tugas, hubungi metode konkurensi::task::get . Metode ini panggilan task::wait untuk menunggu tugas selesai, dan karena itu memblokir eksekusi thread saat ini sampai hasilnya tersedia.

Contoh berikut menunjukkan cara membuat tugas, menunggu hasilnya, dan menampilkan nilainya. Contoh dalam dokumentasi ini menggunakan fungsi lambda karena mereka memberikan sintaks yang lebih ringkas. Namun, Anda juga dapat menggunakan penunjuk fungsi dan objek fungsi saat 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 pelaksanaan tugas, runtime marshal 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: Menghubungkan Menggunakan Tugas dan Permintaan HTTP XML. (Kelas task_completion_event dijelaskan kemudian 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 sana. Secara tradisional, ini dilakukan dengan menggunakan metode callback. Dalam Concurrency Runtime, fungsi yang sama disediakan oleh tugas kelanjutan. Tugas kelanjutan (juga dikenal hanya sebagai kelanjutan) adalah tugas asinkron yang dipanggil oleh tugas lain, yang dikenal sebagai anteseden, ketika anteseden selesai. Dengan menggunakan continuation, Anda dapat:

  • Berikan data dari anteseden ke kelanjutan.

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

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

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

  • Panggil beberapa kelanjutan dari anteseden yang sama.

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

  • Rantai berlanjut satu demi satu dengan panjang berapa pun.

  • Gunakan kelanjutan untuk menangani pengecualian yang dilemparkan oleh anteseden.

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

Contoh berikut memodifikasi yang sebelumnya untuk menggunakan konkurensi::task::then metode untuk menjadwalkan kelanjutan yang mencetak nilai tugas anteseden 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 merantai dan menumpuk tugas hingga panjang apa pun. Tugas juga dapat memiliki beberapa kelanjutan. Contoh berikut menggambarkan rantai kelanjutan dasar yang meningkatkan 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 asynchronous unwrapping. Membuka bungkuskron 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 di 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 tipe Nbersarang, tugas yang dihasilkan memiliki tipe N, bukan task<N>, dan selesai ketika tugas bersarang selesai. Dengan kata lain, kelanjutannya melakukan unwrapping dari tugas bersarang.

Kelanjutan Value-Based Versus Task-Based

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 pendahulu selesai tanpa kesalahan dan tidak dibatalkan. Kelanjutan yang mengambil tipe task<T> sebagai parameternya dikenal sebagai kelanjutan berbasis tugas. Kelanjutan berbasis tugas selalu dijadwalkan untuk eksekusi ketika tugas anteseden selesai, bahkan ketika tugas anteseden dibatalkan atau memberikan pengecualian. Anda kemudian dapat menelepon task::get untuk mendapatkan hasil dari tugas anteseden. Jika tugas pendahulu dibatalkan, task::getlemparkan konkurensi::task_canceled. Jika tugas anteseden melemparkan pengecualian, task::get rethrows pengecualian itu. Kelanjutan berbasis tugas tidak ditandai sebagai dibatalkan ketika tugas pendahulunya 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 himpunan. Contoh dasar berikut digunakan 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 ditunjukkan pada contoh berikut.

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

Adalah umum untuk menggunakan kelanjutan bersama dengan when_all melakukan suatu 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 satu set tugas yang dibatalkan atau memberikan pengecualian, when_all segera selesaikan dan jangan menunggu tugas yang tersisa selesai. Jika pengecualian dilemparkan, runtime mengembalikan pengecualian saat Anda memanggil task::get atau task::wait pada objek tugas yang when_all kembali. Jika lebih dari satu tugas melempar, runtime memilih salah satunya. Oleh karena itu, pastikan Anda mengamati semua pengecualian setelah semua tugas selesai; pengecualian tugas yang tidak ditangani 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 yang terjadi untuk rethrown dan kemudian menelan pengecualian itu.

// 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 tulis satu set file ke disk. Contoh berikut menunjukkan cara menggunakan when_all dan observe_all_exceptions 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 teruskan 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 Button_Click penangan peristiwa.
// 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 pada contoh.

Tip

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

Fungsi when_any

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

Fungsi ini when_any sangat berguna dalam skenario berikut:

  • Operasi yang berlebihan. Pertimbangkan algoritma atau operasi yang dapat dilakukan dalam banyak cara. Anda dapat menggunakan when_any fungsi untuk memilih operasi yang selesai terlebih dahulu dan kemudian membatalkan operasi yang tersisa.

  • Operasi interleaved. Anda dapat memulai beberapa operasi yang semuanya harus selesai 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 memperpanjang 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 digunakan when_any untuk membuat tugas yang selesai ketika 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 lewati 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 ditunjukkan pada contoh berikut.

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

Tip

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

Eksekusi Tugas tertunda

Kadang-kadang perlu untuk menunda pelaksanaan tugas sampai suatu kondisi terpenuhi, atau untuk memulai tugas sebagai tanggapan terhadap peristiwa eksternal. Misalnya, dalam pemrograman asinkron, Anda mungkin harus memulai tugas sebagai respons terhadap peristiwa penyelesaian I /O.

Dua cara untuk mencapai hal ini adalah dengan menggunakan kelanjutan atau untuk memulai tugas dan menunggu suatu peristiwa dalam fungsi kerja tugas. Namun, ada kasus di mana tidak mungkin menggunakan salah satu teknik ini. Misalnya, untuk membuat kelanjutan, Anda harus memiliki tugas anteseden. Namun, jika Anda tidak memiliki tugas pendahulu, Anda dapat membuat acara penyelesaian tugas dan kemudian merantai peristiwa penyelesaian itu ke tugas pendahulu ketika tersedia. Selain itu, karena tugas menunggu juga memblokir utas, Anda dapat menggunakan peristiwa penyelesaian tugas untuk melakukan pekerjaan ketika operasi asinkron selesai, dan dengan demikian membebaskan utas.

Konkurensi::task_completion_event kelas membantu menyederhanakan komposisi tugas tersebut. task Seperti kelas, parameter T tipe adalah jenis hasil yang dihasilkan oleh tugas. Jenis ini bisa void jika tugas tidak mengembalikan nilai. T tidak dapat menggunakan const pengubah. Biasanya, objek task_completion_event disediakan untuk thread atau tugas yang akan menandakan ketika nilai untuk itu menjadi tersedia. Pada saat yang sama, satu atau lebih tugas ditetapkan sebagai pendengar acara itu. Ketika acara diatur, tugas pendengar selesai dan kelanjutannya dijadwalkan untuk dijalankan.

Untuk contoh yang digunakan task_completion_event untuk menerapkan 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 antrian ini dan mengeksekusinya pada sumber daya komputasi yang tersedia. Setelah menambahkan tugas ke grup tugas, Anda dapat menunggu semua tugas selesai atau membatalkan tugas yang belum dimulai.

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

PPL membagi kelompok tugas ke dalam dua kategori ini: kelompok tugas tidak terstruktur dan kelompok tugas terstruktur. PPL menggunakan task_group kelas untuk mewakili kelompok tugas yang tidak terstruktur dan structured_task_group kelas untuk mewakili kelompok tugas terstruktur.

Penting

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

Gunakan parallel_invoke ketika Anda memiliki beberapa tugas independen yang ingin Anda jalankan pada saat yang sama, dan Anda harus menunggu semua tugas selesai sebelum Anda melanjutkan. Teknik ini sering disebut sebagai garpu dan bergabung paralelisme . Gunakan task_group ketika 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 objek task_group dan menunggu tugas selesai di fungsi lain atau dari utas lain.

Kelompok tugas mendukung konsep pembatalan. Pembatalan memungkinkan Anda memberi sinyal ke semua tugas aktif yang ingin Anda batalkan operasi secara keseluruhan. 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 memberikan pengecualian dari tugas dan menangani pengecualian itu ketika 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 sarankan Anda menggunakan task_group atau parallel_invoke alih-alih structured_task_group kelas, ada kasus di mana Anda ingin menggunakan structured_task_group, misalnya, ketika Anda menulis algoritma paralel yang melakukan sejumlah tugas variabel atau memerlukan dukungan untuk pembatalan. Bagian ini menjelaskan perbedaan antara task_group kelas dan structured_task_group kelas.

Kelas ini task_group aman untuk thread. Oleh karena itu Anda dapat menambahkan tugas ke objek task_group dari beberapa utas dan menunggu atau membatalkan task_group objek dari beberapa utas. Konstruksi dan penghancuran suatu structured_task_group objek harus terjadi dalam lingkup leksikal yang sama. Selain itu, semua operasi pada suatu 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 objek task_group setelah Anda memanggil konkurensi::task_group::tunggu atau konkurensi::task_group::run_and_wait metode. Sebaliknya, jika Anda menjalankan tugas tambahan pada objek structured_task_group setelah Anda memanggil konkurensi::structured_task_group::tunggu atau konkurensi::structured_task_group::run_and_wait metode, maka perilaku tersebut tidak terdefinisi.

structured_task_group Karena kelas tidak disinkronkan di seluruh utas, ia 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 algoritme, structured_task_group kelas dapat membantu Anda menulis kode berkinerja lebih baik.

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

Kelompok tugas yang tidak terstruktur dan kelompok tugas terstruktur bekerja dengan menangani tugas dengan cara yang berbeda. Anda dapat meneruskan fungsi kerja langsung ke objek task_group ; task_group objek akan membuat dan mengelola pegangan tugas untuk Anda. Kelas structured_task_group mengharuskan Anda untuk mengelola objek task_handle untuk setiap tugas. Setiap task_handle objek harus tetap valid sepanjang masa pakai objek terkait structured_task_group . Gunakan fungsi konkurensi::make_task untuk membuat objek, seperti yang task_handle ditunjukkan pada 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 sejumlah tugas variabel, gunakan rutinitas alokasi tumpukan seperti _malloca atau kelas kontainer, seperti std::vector.

Keduanya task_group dan structured_task_group mendukung pembatalan. 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 objek task_group . 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 output sampel 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 algoritme, lihat Cara: Gunakan parallel_invoke Menulis Rutinitas Pengurutan Paralel dan Cara: Gunakan parallel_invoke untuk Menjalankan Operasi Paralel. Untuk contoh lengkap yang menggunakan task_group kelas untuk menerapkan futures asinkron, lihat Walkthrough: Menerapkan Futures.

Pemrograman yang Kuat

Pastikan Anda memahami peran pembatalan dan penanganan pengecualian saat Menggunakan tugas, grup tugas, dan algoritme paralel. Misalnya, di pohon pekerjaan paralel, tugas yang dibatalkan mencegah tugas anak berjalan. Hal 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 memberikan pengecualian, pengecualian itu dapat menyebar melalui perusakan objek dan menyebabkan perilaku yang tidak terdefinisi dalam aplikasi Anda. Untuk contoh yang mengilustrasikan poin-poin ini, lihat bagian Pahami Bagaimana Penanganan Pembatalan dan Pengecualian Memengaruhi Penghancuran Objek dalam 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: Gunakan parallel_invoke untuk Menulis Rutinitas SortOr Paralel Menunjukkan cara menggunakan parallel_invoke algoritma untuk meningkatkan kinerja algoritma sortir bitonic.
Cara: Gunakan parallel_invoke untuk Menjalankan Operasi Paralel Menunjukkan cara menggunakan parallel_invoke algoritme untuk meningkatkan kinerja program yang melakukan beberapa operasi pada sumber data bersama.
Cara: Buat Tugas yang Selesai Setelah Penundaan Menunjukkan cara menggunakan task, , cancellation_token_source, cancellation_token, dan task_completion_event kelas untuk membuat tugas yang selesai setelah penundaan.
Panduan: Menerapkan Futures Menunjukkan cara menggabungkan fungsionalitas yang ada di Concurrency Runtime menjadi sesuatu yang melakukan lebih banyak.
Pustaka Pola Paralel (PPL) Menjelaskan PPL, yang menyediakan model pemrograman imperatif untuk mengembangkan aplikasi bersamaan.

Referensi

Kelas tugas (Runtime Konkurensi)

Kelas task_completion_event

Fungsi when_all

Fungsi when_any

Kelas task_group

Fungsi parallel_invoke

Kelas structured_task_group