Równoległość zadania (współbieżność środowiska wykonawczego)
W środowisku uruchomieniowym współbieżności zadanie jest jednostką pracy, która wykonuje określone zadanie i zwykle działa równolegle z innymi zadaniami. Zadanie można podzielić na dodatkowe, bardziej szczegółowe zadania, które są zorganizowane w grupę zadań.
Zadania są używane podczas pisania kodu asynchronicznego i chcesz, aby niektóre operacje miały miejsce po zakończeniu operacji asynchronicznej. Na przykład można użyć zadania do asynchronicznego odczytu z pliku, a następnie użyć innego zadania — zadania kontynuacji, które zostało wyjaśnione w dalszej części tego dokumentu — w celu przetworzenia danych po udostępnieniu. Z drugiej strony można użyć grup zadań, aby rozłożyć równoległą pracę na mniejsze elementy. Załóżmy na przykład, że masz algorytm rekursywny, który dzieli pozostałą pracę na dwie partycje. Grupy zadań umożliwiają równoczesne uruchamianie tych partycji, a następnie oczekiwanie na ukończenie podzielonej pracy.
Napiwek
Jeśli chcesz zastosować tę samą procedurę do każdego elementu kolekcji równolegle, użyj algorytmu równoległego, takiego jak współbieżność::p arallel_for, zamiast zadania lub grupy zadań. Aby uzyskać więcej informacji na temat algorytmów równoległych, zobacz Parallel Algorithms (Algorytmy równoległe).
Kwestie kluczowe
Po przekazaniu zmiennych do wyrażenia lambda przez odwołanie należy zagwarantować, że okres istnienia tej zmiennej będzie się powtarzać do momentu zakończenia zadania.
Użyj zadań ( współbieżności::task class ) podczas pisania kodu asynchronicznego. Klasa zadań używa puli wątków systemu Windows jako harmonogramu, a nie środowiska uruchomieniowego współbieżności.
Użyj grup zadań ( współbieżności::task_group klasy lub współbieżności::p arallel_invoke algorytmu), gdy chcesz rozłożyć równoległą pracę na mniejsze elementy, a następnie poczekaj na ukończenie tych mniejszych elementów.
Użyj metody concurrency::task::then, aby utworzyć kontynuacje. Kontynuacja to zadanie, które jest uruchamiane asynchronicznie po zakończeniu innego zadania. Możesz połączyć dowolną liczbę kontynuacji, aby utworzyć łańcuch pracy asynchronicznej.
Kontynuacja oparta na zadaniach jest zawsze zaplanowana do wykonania, gdy zadanie przedterminowe zostanie zakończone, nawet gdy zadanie wcześniejsze zostanie anulowane lub zgłosi wyjątek.
Użyj współbieżności::when_all , aby utworzyć zadanie, które zostanie wykonane po zakończeniu każdego elementu członkowskiego zestawu zadań. Użyj współbieżności::when_any , aby utworzyć zadanie, które zostanie ukończone po zakończeniu jednego elementu członkowskiego zestawu zadań.
Zadania i grupy zadań mogą uczestniczyć w mechanizmie anulowania biblioteki wzorców równoległych (PPL). Aby uzyskać więcej informacji, zobacz Anulowanie w PPL.
Aby dowiedzieć się, jak środowisko uruchomieniowe obsługuje wyjątki zgłaszane przez zadania i grupy zadań, zobacz Obsługa wyjątków.
W tym dokumencie
Korzystając z wyrażenia Lambda
Ze względu na ich zwięzłą składnię wyrażenia lambda są typowym sposobem definiowania pracy wykonywanej przez zadania i grupy zadań. Oto kilka wskazówek dotyczących użycia:
Ponieważ zadania są zwykle uruchamiane w wątkach w tle, należy pamiętać o okresie istnienia obiektu podczas przechwytywania zmiennych w wyrażeniach lambda. Podczas przechwytywania zmiennej według wartości kopia tej zmiennej jest dokonana w treści lambda. Podczas przechwytywania przy użyciu odwołania kopia nie jest wykonana. W związku z tym upewnij się, że okres istnienia każdej zmiennej przechwyconej przez odwołanie przeżywa zadanie, które go używa.
Po przekazaniu wyrażenia lambda do zadania nie przechwytuj zmiennych przydzielonych na stos przez odwołanie.
Należy jawnie określać zmienne przechwycone w wyrażeniach lambda, dzięki czemu można zidentyfikować, co przechwytujesz według wartości, a nie według odwołania. Z tego powodu zalecamy, aby nie używać
[=]
opcji lub[&]
dla wyrażeń lambda.
Typowy wzorzec polega na tym, że jedno zadanie w łańcuchu kontynuacji przypisuje do zmiennej, a inne zadanie odczytuje taką zmienną. Nie można przechwycić według wartości, ponieważ każde zadanie kontynuacji będzie przechowywać inną kopię zmiennej. W przypadku zmiennych przydzielonych do stosu nie można również przechwycić według odwołania, ponieważ zmienna może nie być już prawidłowa.
Aby rozwiązać ten problem, użyj inteligentnego wskaźnika, takiego jak std::shared_ptr, aby opakować zmienną i przekazać inteligentny wskaźnik według wartości. W ten sposób obiekt źródłowy można przypisać do obiektu i odczytać z niego oraz przeżyć zadania, które go używają. Ta technika jest używana nawet wtedy, gdy zmienna jest wskaźnikiem lub zliczonym do odwołania uchwytem (^
) do obiektu środowisko wykonawcze systemu Windows. Poniżej przedstawiono prosty przykład:
// 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
*/
Aby uzyskać więcej informacji na temat wyrażeń lambda, zobacz Wyrażenia lambda.
Klasa zadania
Za pomocą klasy concurrency::task można tworzyć zadania w zestawie operacji zależnych. Ten model kompozycji jest obsługiwany przez pojęcie kontynuacji. Kontynuacja umożliwia wykonywanie kodu po zakończeniu poprzedniego lub poprzedniego zadania. Wynik zadania przedzibowego jest przekazywany jako dane wejściowe do jednego lub większej liczby zadań kontynuacji. Po zakończeniu zadania przedzimowego wszelkie zadania kontynuacji oczekujące na nie są zaplanowane do wykonania. Każde zadanie kontynuacji otrzymuje kopię wyniku zadania przedzibowego. Z kolei te zadania kontynuacji mogą być również poprzedzane zadaniami dla innych kontynuacji, tworząc łańcuch zadań. Kontynuacje ułatwiają tworzenie łańcuchów zadań o dowolnej długości, które mają między sobą określone zależności. Ponadto zadanie może uczestniczyć w anulowaniu przed rozpoczęciem zadań lub w sposób współpracy podczas jego uruchamiania. Aby uzyskać więcej informacji na temat tego modelu anulowania, zobacz Anulowanie w PPL.
task
jest klasą szablonu. Parametr T
type jest typem wyniku, który jest generowany przez zadanie. Ten typ może być taki void
, jeśli zadanie nie zwraca wartości. T
program nie może użyć const
modyfikatora.
Podczas tworzenia zadania udostępniasz funkcję pracy, która wykonuje treść zadania. Ta funkcja pracy ma postać funkcji lambda, wskaźnika funkcji lub obiektu funkcji. Aby poczekać na zakończenie zadania bez uzyskania wyniku, wywołaj metodę concurrency::task::wait . Metoda task::wait
zwraca wartość współbieżności::task_status, która opisuje, czy zadanie zostało ukończone, czy anulowane. Aby uzyskać wynik zadania, wywołaj metodę concurrency::task::get . Ta metoda wywołuje task::wait
metodę oczekiwania na zakończenie zadania i w związku z tym blokuje wykonywanie bieżącego wątku do momentu udostępnienia wyniku.
W poniższym przykładzie pokazano, jak utworzyć zadanie, poczekać na jego wynik i wyświetlić jego wartość. Przykłady w tej dokumentacji używają funkcji lambda, ponieważ zapewniają bardziej zwięzłą składnię. Można jednak również używać wskaźników funkcji i obiektów funkcji podczas korzystania z zadań.
// 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
*/
W przypadku korzystania z funkcji concurrency::create_task można użyć auto
słowa kluczowego zamiast deklarowania typu. Rozważmy na przykład ten kod, który tworzy i drukuje macierz tożsamości:
// 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
*/
Możesz użyć create_task
funkcji , aby utworzyć równoważną operację.
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;
});
Jeśli podczas wykonywania zadania zgłaszany jest wyjątek, środowisko uruchomieniowe marshaluje ten wyjątek w kolejnym wywołaniu metody task::get
lub task::wait
lub kontynuacji opartej na zadaniach. Aby uzyskać więcej informacji na temat mechanizmu obsługi wyjątków zadań, zobacz Obsługa wyjątków.
Aby zapoznać się z przykładem , task
współbieżność::task_completion_event, anulowanie, zobacz Przewodnik: łączenie przy użyciu zadań i żądań HTTP XML. (Klasa została opisana task_completion_event
w dalszej części tego dokumentu).
Napiwek
Aby dowiedzieć się więcej o zadaniach w aplikacjach platformy UWP, zobacz Programowanie asynchroniczne w języku C++ i Tworzenie operacji asynchronicznych w języku C++ dla aplikacji platformy UWP.
Zadania kontynuacji
W programowaniu asynchronicznym bardzo często zdarza się, że jedna operacja asynchroniczna, po zakończeniu, wywołuje drugą operację i przekazuje do niej dane. Tradycyjnie odbywa się to przy użyciu metod wywołania zwrotnego. W środowisku uruchomieniowym współbieżności te same funkcje są udostępniane przez zadania kontynuacji. Zadanie kontynuacji (znane również jako kontynuacja) jest asynchronicznym zadaniem wywoływanym przez inne zadanie, znane jako antecedent, gdy poprzedza. Korzystając z kontynuacji, można wykonywać następujące czynności:
Przekaż dane z poprzednich do kontynuacji.
Określ dokładne warunki, w których jest wywoływana kontynuacja lub nie jest wywoływana.
Anuluj kontynuację przed rozpoczęciem lub współpracy podczas jego działania.
Podaj wskazówki dotyczące sposobu planowania kontynuacji. (Dotyczy to tylko aplikacji platformy platforma uniwersalna systemu Windows (UWP). Aby uzyskać więcej informacji, zobacz Creating Asynchronous Operations in C++ for UWP Apps (Tworzenie operacji asynchronicznych w języku C++ dla aplikacji platformy UWP).
Wywołaj wiele kontynuacji z tego samego antycedentu.
Wywołaj jedną kontynuację po zakończeniu wszystkich lub dowolnego z wielu przeddentów.
Łańcuch kontynuacji jeden po drugim do dowolnej długości.
Użyj kontynuacji do obsługi wyjątków zgłaszanych przez antecedent.
Te funkcje umożliwiają wykonywanie co najmniej jednego zadania po zakończeniu pierwszego zadania. Można na przykład utworzyć kontynuację, która kompresuje plik po pierwszym zadaniu odczyta go z dysku.
Poniższy przykład modyfikuje poprzedni, aby użyć współbieżności::task::then metody, aby zaplanować kontynuację, która wyświetla wartość zadania przedzibowego, gdy jest dostępna.
// 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
*/
Zadania można łączyć i zagnieżdżać do dowolnej długości. Zadanie może również mieć wiele kontynuacji. Poniższy przykład ilustruje podstawowy łańcuch kontynuacji, który zwiększa wartość poprzedniego zadania trzy razy.
// 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
*/
Kontynuacja może również zwrócić inne zadanie. Jeśli nie ma anulowania, to zadanie zostanie wykonane przed kolejną kontynuacją. Ta technika jest znana jako asynchroniczne rozpasanie. Asynchroniczne rozpasanie jest przydatne, gdy chcesz wykonać dodatkową pracę w tle, ale nie chcesz, aby bieżące zadanie blokowało bieżący wątek. (Jest to powszechne w aplikacjach platformy UNIWERSALNEJ systemu Windows, gdzie kontynuacje mogą być uruchamiane w wątku interfejsu użytkownika). W poniższym przykładzie przedstawiono trzy zadania. Pierwsze zadanie zwraca inne zadanie, które jest uruchamiane przed zadaniem kontynuacji.
// 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
*/
Ważne
Gdy kontynuacja zadania zwraca zagnieżdżone zadanie typu N
, wynikowe zadanie ma typ N
, a nie task<N>
, i kończy się po zakończeniu zagnieżdżonego zadania. Innymi słowy kontynuacja wykonuje rozpasanie zagnieżdżonego zadania.
Kontynuacje oparte na wartościach a oparte na zadaniach
Biorąc pod uwagę obiekt, którego zwracany task
typ to T
, można podać wartość typu T
lub task<T>
jego podzadania kontynuacji. Kontynuacja, która przyjmuje typT
, jest nazywana kontynuacją opartą na wartości. Kontynuacja oparta na wartości jest zaplanowana do wykonania, gdy zadanie antecedent zakończy się bez błędu i nie zostanie anulowane. Kontynuacja, która przyjmuje typ task<T>
jako jego parametr, jest nazywana kontynuacją opartą na zadaniach. Kontynuacja oparta na zadaniach jest zawsze zaplanowana do wykonania, gdy zadanie przedterminowe zostanie zakończone, nawet gdy zadanie wcześniejsze zostanie anulowane lub zgłosi wyjątek. Następnie możesz wywołać metodę task::get
, aby uzyskać wynik zadania przeddentowego. Jeśli zadanie antecedent zostało anulowane, task::get
zgłasza współbieżność::task_canceled. Jeśli zadanie antycedentowe zgłosiło wyjątek, task::get
ponownie przewróci ten wyjątek. Kontynuacja oparta na zadaniu nie jest oznaczona jako anulowana po anulowaniu zadania.
Tworzenie zadań
W tej sekcji opisano funkcje współbieżności::when_all i współbieżności::when_any , które mogą ułatwić tworzenie wielu zadań w celu zaimplementowania typowych wzorców.
Funkcja when_all
Funkcja when_all
tworzy zadanie, które jest wykonywane po zakończeniu zestawu zadań. Ta funkcja zwraca obiekt std::vector zawierający wynik każdego zadania w zestawie. W poniższym przykładzie podstawowym użyto when_all
metody do utworzenia zadania reprezentującego ukończenie trzech innych zadań.
// 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.
*/
Uwaga
Zadania przekazywane when_all
muszą być jednolite. Innymi słowy, wszystkie te elementy muszą zwracać ten sam typ.
Składnię &&
można również użyć do utworzenia zadania, które zostanie wykonane po zakończeniu zestawu zadań, jak pokazano w poniższym przykładzie.
auto t = t1 && t2; // same as when_all
Często używa się kontynuacji wraz z when_all
wykonywaniem akcji po zakończeniu zestawu zadań. Poniższy przykład modyfikuje poprzedni, aby wyświetlić sumę trzech zadań, które każdy z nich generuje int
wynik.
// 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.
*/
W tym przykładzie można również określić task<vector<int>>
, aby utworzyć kontynuację opartą na zadaniach.
Jeśli jakiekolwiek zadanie w zestawie zadań zostanie anulowane lub zgłosi wyjątek, when_all
natychmiast zostanie ukończone i nie czeka na zakończenie pozostałych zadań. Jeśli zostanie zgłoszony wyjątek, środowisko uruchomieniowe ponownie wywróci wyjątek podczas wywoływania task::get
lub task::wait
obiektu zadania, który when_all
zwraca. Jeśli zostanie zgłoszonych więcej niż jedno zadanie, środowisko uruchomieniowe wybierze jedno z nich. W związku z tym upewnij się, że wszystkie wyjątki są obserwowane po zakończeniu wszystkich zadań; nieobsługiwany wyjątek zadania powoduje zakończenie działania aplikacji.
Oto funkcja narzędzia, której można użyć, aby upewnić się, że program obserwuje wszystkie wyjątki. Dla każdego zadania w podanym zakresie wyzwala wszelkie wyjątki, observe_all_exceptions
które wystąpiły, zostaną ponownie wyrzucone, a następnie połyka ten wyjątek.
// 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.
}
});
});
}
Rozważmy aplikację platformy UNIWERSALNEJ systemu Windows, która używa języków C++ i XAML oraz zapisuje zestaw plików na dysku. W poniższym przykładzie pokazano, jak używać programu when_all
i observe_all_exceptions
upewnić się, że program obserwuje wszystkie wyjątki.
// 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();
}
});
}
Aby uruchomić ten przykład
- W pliku MainPage.xaml dodaj kontrolkę
Button
.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
- W pliku MainPage.xaml.h dodaj te deklaracje
private
do sekcjiMainPage
deklaracji klasy.
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);
- W MainPage.xaml.cpp zaimplementuj
Button_Click
program obsługi zdarzeń.
// 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.
});
}
- W MainPage.xaml.cpp zaimplementuj
WriteFilesAsync
go, jak pokazano w przykładzie.
Napiwek
when_all
jest funkcją nieblokającą, która generuje task
element w wyniku. W przeciwieństwie do task::wait, można bezpiecznie wywołać tę funkcję w aplikacji platformy UWP w wątku ASTA (Application STA).
Funkcja when_any
Funkcja when_any
tworzy zadanie, które jest wykonywane po zakończeniu pierwszego zadania w zestawie zadań. Ta funkcja zwraca obiekt std::p air zawierający wynik ukończonego zadania i indeks tego zadania w zestawie.
Funkcja jest szczególnie przydatna when_any
w następujących scenariuszach:
Operacje nadmiarowe. Należy wziąć pod uwagę algorytm lub operację, które mogą być wykonywane na wiele sposobów. Możesz użyć
when_any
funkcji , aby wybrać operację, która zakończy się najpierw, a następnie anulować pozostałe operacje.Operacje z przeplotem. Możesz uruchomić wiele operacji, które muszą zostać zakończone, i użyć funkcji do przetwarzania wyników po zakończeniu
when_any
każdej operacji. Po zakończeniu jednej operacji można uruchomić jedno lub więcej dodatkowych zadań.Operacje ograniczane. Za pomocą
when_any
funkcji można rozszerzyć poprzedni scenariusz, ograniczając liczbę operacji współbieżnych.Wygasłe operacje. Za pomocą
when_any
funkcji można wybrać jedno lub więcej zadań i zadanie, które kończy się po określonym czasie.
Podobnie jak w przypadku when_all
programu , często należy użyć kontynuacji, która musi when_any
wykonać akcję po zakończeniu pierwszego w zestawie zadań. W poniższym podstawowym przykładzie użyto when_any
metody do utworzenia zadania, które zostanie wykonane po zakończeniu pierwszego z trzech innych zadań.
// 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.
*/
W tym przykładzie można również określić task<pair<int, size_t>>
, aby utworzyć kontynuację opartą na zadaniach.
Uwaga
Podobnie jak w przypadku when_all
programu , przekazane zadania muszą zwracać when_any
ten sam typ.
Składnię ||
można również użyć do utworzenia zadania, które zostanie wykonane po pierwszym zadaniu w zestawie zadań podrzędnych, jak pokazano w poniższym przykładzie.
auto t = t1 || t2; // same as when_any
Napiwek
Podobnie jak w przypadku when_all
programu , when_any
nie blokuje się i można bezpiecznie wywoływać w aplikacji platformy UNIWERSALNEJ systemu Windows w wątku ASTA.
Opóźnione wykonanie zadania
Czasami konieczne jest opóźnienie wykonywania zadania do momentu spełnienia warunku lub uruchomienia zadania w odpowiedzi na zdarzenie zewnętrzne. Na przykład w programowaniu asynchronicznym może być konieczne uruchomienie zadania w odpowiedzi na zdarzenie ukończenia we/wy.
Dwa sposoby wykonania tego zadania to użycie kontynuacji lub uruchomienie zadania i oczekiwanie na zdarzenie wewnątrz funkcji roboczej zadania. Istnieją jednak przypadki, w których nie można użyć jednej z tych technik. Aby na przykład utworzyć kontynuację, musisz mieć zadanie antecedent. Jeśli jednak nie masz zadania przeddent, możesz utworzyć zdarzenie ukończenia zadania, a później utworzyć łańcuch tego zdarzenia ukończenia do zadania przeddent, gdy stanie się dostępny. Ponadto, ponieważ zadanie oczekujące blokuje również wątek, można użyć zdarzeń ukończenia zadań do wykonania pracy po zakończeniu operacji asynchronicznej, a tym samym zwolnić wątek.
Klasa concurrency::task_completion_event ułatwia takie tworzenie zadań. Podobnie jak klasa task
, parametr T
typu jest typem wyniku generowanego przez zadanie. Ten typ może być taki void
, jeśli zadanie nie zwraca wartości. T
program nie może użyć const
modyfikatora. task_completion_event
Zazwyczaj obiekt jest dostarczany do wątku lub zadania, które zasygnalizuje go, gdy wartość zostanie udostępniona. Jednocześnie co najmniej jedno zadanie jest ustawione jako odbiorniki tego zdarzenia. Po ustawieniu zdarzenia zadania odbiornika zostaną ukończone, a ich kontynuacje zostaną uruchomione.
Aby zapoznać się z przykładem używanym task_completion_event
do implementowania zadania, które kończy się po opóźnieniu, zobacz Instrukcje: tworzenie zadania, które kończy się po opóźnieniu.
Grupy zadań
Grupa zadań organizuje kolekcję zadań. Grupy zadań wypychają zadania do kolejki kradzieży pracy. Harmonogram usuwa zadania z tej kolejki i wykonuje je na dostępnych zasobach obliczeniowych. Po dodaniu zadań do grupy zadań można poczekać na zakończenie lub anulowanie zadań, które nie zostały jeszcze uruchomione.
PPL używa klas współbieżności::task_group i współbieżności::structured_task_group do reprezentowania grup zadań oraz klasy concurrency::task_handle do reprezentowania zadań uruchamianych w tych grupach. Klasa task_handle
hermetyzuje kod, który wykonuje pracę. task
Podobnie jak klasa, funkcja pracy jest w postaci funkcji lambda, wskaźnika funkcji lub obiektu funkcji. Zazwyczaj nie trzeba bezpośrednio pracować z obiektami task_handle
. Zamiast tego przekazujesz funkcje robocze do grupy zadań, a grupa zadań tworzy obiekty i zarządza nimi task_handle
.
PPL dzieli grupy zadań na następujące dwie kategorie: grupy zadań bez struktury i ustrukturyzowane grupy zadań. PPL używa task_group
klasy do reprezentowania nieustrukturyzowanych grup zadań i structured_task_group
klasy do reprezentowania ustrukturyzowanych grup zadań.
Ważne
PPL definiuje również algorytm concurrency::p arallel_invoke , który używa structured_task_group
klasy do równoległego wykonywania zestawu zadań. parallel_invoke
Ponieważ algorytm ma bardziej zwięzłą składnię, zalecamy użycie jej zamiast structured_task_group
klasy, gdy jest to możliwe. W temacie Parallel Algorithms (Algorytmy równoległe ) opisano parallel_invoke
bardziej szczegółowo.
Użyj parallel_invoke
polecenia , gdy masz kilka niezależnych zadań, które chcesz wykonać w tym samym czasie, i musisz poczekać na zakończenie wszystkich zadań przed kontynuowaniem. Ta technika jest często określana jako rozwidlenie i równoległość sprzężenia . Użyj task_group
polecenia , gdy masz kilka niezależnych zadań, które chcesz wykonać w tym samym czasie, ale chcesz poczekać na zakończenie zadań w późniejszym czasie. Można na przykład dodać zadania do task_group
obiektu i poczekać na zakończenie zadań w innej funkcji lub z innego wątku.
Grupy zadań obsługują koncepcję anulowania. Anulowanie umożliwia sygnalizację wszystkich aktywnych zadań, które chcesz anulować ogólną operację. Anulowanie uniemożliwia również uruchamianie zadań, które nie zostały jeszcze uruchomione. Aby uzyskać więcej informacji na temat anulowania, zobacz Anulowanie w PPL.
Środowisko uruchomieniowe udostępnia również model obsługi wyjątków, który umożliwia zgłaszanie wyjątku z zadania i obsługę tego wyjątku po oczekiwaniu na zakończenie skojarzonej grupy zadań. Aby uzyskać więcej informacji na temat tego modelu obsługi wyjątków, zobacz Obsługa wyjątków.
Porównywanie task_group z structured_task_group
Mimo że zalecamy użycie klasy lub parallel_invoke
zamiast structured_task_group
niej, istnieją przypadki, w których chcesz użyć task_group
structured_task_group
metody , na przykład podczas pisania algorytmu równoległego, który wykonuje zmienną liczbę zadań lub wymaga obsługi anulowania. W tej sekcji opisano różnice między klasami task_group
i structured_task_group
.
Klasa task_group
jest bezpieczna wątkowo. W związku z tym można dodawać zadania do task_group
obiektu z wielu wątków i czekać lub anulować task_group
obiekt z wielu wątków. Budowa i niszczenie structured_task_group
obiektu musi odbywać się w tym samym zakresie leksykalnym. Ponadto wszystkie operacje na structured_task_group
obiekcie muszą być wykonywane w tym samym wątku. Wyjątkiem od tej reguły jest współbieżność::structured_task_group::cancel i współbieżność::structured_task_group::is_canceling metody. Podrzędne zadanie może wywołać te metody, aby anulować nadrzędną grupę zadań lub sprawdzić anulowanie w dowolnym momencie.
Dodatkowe zadania można uruchamiać na task_group
obiekcie po wywołaniu metody concurrency::task_group::wait lub concurrency::task_group::run_and_wait . Z drugiej strony, jeśli uruchamiasz dodatkowe zadania na structured_task_group
obiekcie po wywołaniu współbieżności::structured_task_group::wait lub concurrency::structured_task_group::run_and_wait metod, zachowanie jest niezdefiniowane.
structured_task_group
Ponieważ klasa nie synchronizuje się między wątkami, ma mniejsze obciążenie wykonywania niż task_group
klasa. W związku z tym jeśli problem nie wymaga planowania pracy z wielu wątków i nie można użyć algorytmu parallel_invoke
, structured_task_group
klasa może pomóc w pisaniu kodu o lepszej wydajności.
Jeśli używasz jednego structured_task_group
obiektu wewnątrz innego structured_task_group
obiektu, obiekt wewnętrzny musi zostać zakończony i zniszczony przed zakończeniem obiektu zewnętrznego. Klasa task_group
nie wymaga zakończenia zagnieżdżonych grup zadań przed zakończeniem grupy zewnętrznej.
Grupy zadań bez struktury i ustrukturyzowane grupy zadań współpracują z uchwytami zadań na różne sposoby. Funkcje robocze można przekazać bezpośrednio do task_group
obiektu. task_group
Obiekt utworzy uchwyt zadania i będzie nim zarządzać. Klasa structured_task_group
wymaga zarządzania obiektem task_handle
dla każdego zadania. Każdy task_handle
obiekt musi pozostać prawidłowy przez cały okres istnienia skojarzonego structured_task_group
obiektu. Użyj funkcji concurrency::make_task, aby utworzyć task_handle
obiekt, jak pokazano w poniższym przykładzie podstawowym:
// 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);
}
Aby zarządzać obsługą zadań w przypadkach, w których masz zmienną liczbę zadań, należy użyć procedury alokacji stosu, takiej jak _malloca lub klasy kontenera, takiej jak std::vector.
Zarówno, jak task_group
i structured_task_group
obsługa anulowania. Aby uzyskać więcej informacji na temat anulowania, zobacz Anulowanie w PPL.
Przykład
W poniższym podstawowym przykładzie pokazano, jak pracować z grupami zadań. W tym przykładzie użyto algorytmu parallel_invoke
do współbieżnego wykonywania dwóch zadań. Każde zadanie dodaje podzadania do task_group
obiektu. Należy pamiętać, że task_group
klasa umożliwia wykonywanie wielu zadań w celu jednoczesnego dodawania do niego zadań.
// 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();
}
Poniżej przedstawiono przykładowe dane wyjściowe dla tego przykładu:
Message from task: Hello
Message from task: 3.14
Message from task: 42
parallel_invoke
Ponieważ algorytm uruchamia zadania jednocześnie, kolejność komunikatów wyjściowych może się różnić.
Aby zapoznać się z kompletnymi przykładami, które pokazują, jak używać algorytmu parallel_invoke
, zobacz How to: Use parallel_invoke to Write a Parallel Sort Routine ( Instrukcje: używanie parallel_invoke do wykonywania operacji równoległych). Kompletny przykład, który używa task_group
klasy do implementowania asynchronicznych futures, zobacz Przewodnik: implementowanie przyszłości.
Niezawodne programowanie
Upewnij się, że rozumiesz rolę anulowania i obsługi wyjątków podczas korzystania z zadań, grup zadań i algorytmów równoległych. Na przykład w drzewie równoległej pracy anulowane zadanie uniemożliwia uruchamianie zadań podrzędnych. Może to spowodować problemy, jeśli jedno z zadań podrzędnych wykonuje operację, która jest ważna dla aplikacji, na przykład zwalniając zasób. Ponadto jeśli zadanie podrzędne zgłasza wyjątek, ten wyjątek może być propagowany za pomocą destruktora obiektu i powodować niezdefiniowane zachowanie w aplikacji. Aby zapoznać się z przykładem tych punktów, zobacz sekcję Informacje o tym, jak anulowanie i obsługa wyjątków wpływają na niszczenie obiektów w dokumencie Best Practices in the Parallel Patterns Library (Najlepsze rozwiązania w bibliotece wzorców równoległych). Aby uzyskać więcej informacji na temat modeli anulowania i obsługi wyjątków w PPL, zobacz Anulowanie i obsługa wyjątków.
Tematy pokrewne
Nazwa | opis |
---|---|
Instrukcje: używanie parallel_invoke do napisania procedury sortowania równoległego | Pokazuje, jak za pomocą algorytmu parallel_invoke poprawić wydajność algorytmu sortowania bitoicznego. |
Instrukcje: korzystanie z parallel_invoke podczas przeprowadzania operacji równoległych | Pokazuje, jak za pomocą algorytmu parallel_invoke zwiększyć wydajność programu, który wykonuje wiele operacji na udostępnionym źródle danych. |
Instrukcje: tworzenie zadania kończonego po opóźnieniu | Pokazuje, jak za pomocą task klas , cancellation_token_source , cancellation_token i task_completion_event utworzyć zadanie, które zostanie ukończone po opóźnieniu. |
Przewodnik: wdrażanie przyszłych operacji | Pokazuje, jak połączyć istniejące funkcje w środowisku uruchomieniowym współbieżności w coś, co robi więcej. |
Biblioteka równoległych wzorców (PLL) | Opisuje PPL, który zapewnia model programowania imperatywnego do tworzenia współbieżnych aplikacji. |