작업 병렬 처리(동시성 런타임)
동시성 런타임에서 작업은 특정 작업을 수행하고 다른 작업과 병렬로 실행되는 작업 단위입니다. 작업 그룹으로 구성된 더 세분화된 추가 작업으로 작업을 분해할 수 있습니다.
비동기 코드를 작성하고 비동기 작업이 완료된 후 일부 작업이 발생하도록 할 때 작업을 사용합니다. 예를 들어, 파일을 비동기적으로 읽는 작업을 사용한 다음 사용 가능하게 된 후 데이터를 처리하는 다른 작업인 이 문서 뒷부분에서 설명하는 연속 작업을 사용할 수 있습니다. 반대로, 작업 그룹을 사용하여 병렬 작업을 더 작은 조각으로 분해할 수 있습니다. 예를 들어 남은 작업을 두 개의 파티션으로 나누는 재귀 알고리즘이 있다고 가정해 봅니다. 작업 그룹을 사용하여 이러한 파티션을 동시에 실행한 다음 구분된 작업이 완료되기를 대기할 수 있습니다.
팁
컬렉션의 모든 요소에 동일한 루틴을 병렬로 적용하려면 작업 또는 작업 그룹 대신 concurrency::parallel_for와 같은 병렬 알고리즘을 사용합니다.병렬 알고리즘에 대한 자세한 내용은 병렬 알고리즘을 참조하십시오.
주요 사항
참조를 통해 변수를 람다 식에 전달하는 경우 해당 변수의 수명이 작업 완료 시까지 유지되도록 해야 합니다.
비동기 코드를 작성할 때 작업(concurrency::task 클래스)을 사용합니다.
병렬 작업을 더 작은 조각으로 분해한 다음 해당 조각이 완료될 때까지 대기하려는 경우 작업 그룹(concurrency::task_group 클래스 또는 concurrency::parallel_invoke 알고리즘)을 사용합니다.
concurrency::task::then 메서드를 사용하여 연속 작업을 만듭니다. 연속 작업은 다른 작업이 완료된 후에 비동기적으로 실행되는 작업입니다. 임의 개수의 연속 작업을 연결하여 비동기 작업 체인을 생성할 수 있습니다.
작업 기반 연속은 선행 작업이 취소되거나 예외를 throw할 때라도 항상 선행 작업이 완료될 때 실행되도록 예약됩니다.
작업 집합의 모든 멤버가 완료된 후 완료되는 작업을 만들려면 concurrency::when_all을 사용합니다. 작업 집합의 멤버 하나가 완료된 후 완료되는 작업을 만들려면 concurrency::when_any를 사용합니다.
작업 및 작업 그룹은 라이브러리 PPL(병렬 패턴) 취소 메커니즘에 참여할 수 있습니다. 자세한 내용은 PPL에서의 취소을 참조하십시오.
작업 및 작업 그룹에서 throw된 예외가 런타임에서 처리되는 방법은 동시성 런타임에서 예외 처리를 참조하십시오.
이 문서에서 다루는 내용
람다 식 사용
작업 클래스
연속 작업
값 기반 연속 작업과 작업 기반 연속 작업의 비교
작업 작성
when_all 함수
when_any 함수
지연된 작업 실행
작업 그룹
task_group을 structured_task_group과 비교
예제
강력한 프로그래밍
람다 식 사용
간결한 구문 때문에 람다 식은 작업 및 작업 그룹에서 수행되는 작업을 정의하는 일반적인 방식입니다. 다음은 몇 가지 사용 팁입니다.
작업은 일반적으로 백그라운드 스레드에서 실행되므로 람다 식에서 변수를 캡처할 때는 개체 수명에 주의하십시오. 값으로 변수를 캡처할 때 람다 식 본문에 해당 변수의 복사본이 만들어집니다. 참조로 캡처할 때는 복사본이 생성되지 않습니다. 따라서 참조로 캡처하는 변수의 수명이 이를 사용하는 작업보다 더 오래 지속되는지 확인합니다.
람다 식을 작업에 전달하면 참조로 스택에 할당되는 변수는 캡처되지 않습니다.
값으로 캡처하거나 참조로 캡처하고 있는 내용을 식별할 수 있도록 람다 식에서 캡처하는 변수에 대해 명시합니다. 이런 이유로 람다 식에 대해 [=] 또는 [&] 옵션을 사용하지 않는 것이 좋습니다.
일반적인 패턴은 연속 체인에서 한 작업을 변수에 할당하고 다른 작업은 해당 변수를 읽을 때입니다. 각 연속 작업에는 서로 다른 변수 복사본이 포함되기 때문에 값을 기준으로 캡처할 수 없습니다. 스택에 할당된 변수의 경우 역시 변수가 더 이상 유효하지 않을 수 있기 때문에 참조로 캡처할 수 없습니다.
이 문제를 해결하려면 std::shared_ptr과 같은 스마트 포인터를 사용하여 변수를 래핑하고 기본값으로 스마트 포인터를 전달합니다. 이렇게 하면 기본 개체가 할당되고 읽혀지며 이 개체를 사용하는 작업보다 오래 유지됩니다. 변수가 포인터이거나 Windows 런타임 개체에 대한 참조 계산 핸들(^)인 경우에도 이 기법을 사용합니다. 기본 예제는 다음과 같습니다.
// 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
*/
람다 식에 대한 자세한 내용은 C++의 람다 식을 참조하십시오.
[맨 위]
작업 클래스
concurrency::task 클래스를 사용해서 작업을 종속 작업 집합으로 구성할 수 있습니다. 이 컴퍼지션 모델은 연속 작업의 개념으로 지원됩니다. 연속 작업으로 이전 또는 선행 작업을 완료할 때 코드를 실행할 수 있습니다. 선행 작업의 결과가 하나 이상의 연속 작업에 입력으로 전달됩니다. 선행 작업이 완료되면 여기에서 대기 중인 연속 작업의 실행이 예약됩니다. 각 연속 작업은 선행 작업 결과의 복사본을 받습니다. 또한 이러한 연속 작업은 다른 연속 작업을 위한 선행 작업이므로 연쇄 작업을 만들 수 있습니다. 연속 작업은 서로 특수하게 종속성을 갖는 작업들의 임의 길이를 갖는 체인을 만드는 것을 지원합니다. 또한 작업이 시작되기 전이나 실행되는 동안 협조적 방식으로 작업이 취소에 참가할 수 있습니다. 이 취소 모델에 대한 자세한 내용은 PPL에서의 취소를 참조하십시오.
task는 템플릿 클래스입니다. 형식 매개 변수 T는 작업의 결과로 생성되는 형식입니다. 작업이 값을 반환하지 않을 경우 이 형식은 void일 수 있습니다. T는 const 한정자를 사용할 수 없습니다.
작업을 만들 때는 작업 본문을 수행하는 작업 함수를 제공합니다. 이 작업 함수는 람다 함수, 함수 포인터 또는 함수 개체의 형식으로 제공됩니다. 결과를 가져오지 않고 작업이 완료될 때까지 대기하려면 concurrency::task::wait 메서드를 호출합니다. task::wait 메서드가 작업이 완료되거나 취소 여부를 나타내는 concurrency::task_status 값을 반환합니다. 작업 결과를 가져오려면 concurrency::task::get 메서드를 호출합니다. 이 메서드는 task::wait를 호출하여 작업이 완료될 때까지 대기하므로 결과를 사용할 수 있을 때까지 현재 스레드 실행을 차단합니다.
다음 예제에서는 작업을 만들고, 그 결과를 기다리고, 그 해당 값을 표시하는 방법을 보여줍니다. 설명서의 예제는 좀 더 간결한 구문을 제공하는 람다 함수를 사용합니다. 그러나 작업을 사용할 때 함수 포인터 및 함수 개체도 사용할 수 있습니다.
// 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
*/
concurrency::create_task 함수를 사용할 때 형식을 선언하는 대신 auto 키워드를 사용할 수 있습니다. 예를 들어 항등 매트릭스를 만들고 인쇄하는 다음 코드를 고려합니다.
// 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
*/
create_task 함수를 사용해서 동일한 작업을 만들 수 있습니다.
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;
});
작업을 실행하는 동안 예외가 throw되는 경우 런타임에서 task::get 또는 task::wait 또는 작업 기반 연속에 대한 후속 호출에서 해당 예외를 마샬링합니다. 작업 예외 처리 메커니즘에 대한 자세한 내용은 동시성 런타임에서 예외 처리를 참조하십시오.
task, concurrency::task_completion_event, 취소를 사용하는 예제는 연습: 작업 및 XML HTTP 요청을 사용하여 연결를 참조하십시오. (task_completion_event 클래스는 이 문서의 뒷부분에 설명되어 있습니다.)
팁
Windows 스토어 응용 프로그램의 작업에 관련된 자세한 내용을 보려면 Asynchronous programming in C++ 및 C++로 Windows 스토어 앱용 비동기 작업 만들기를 참조하십시오.
[맨 위]
연속 작업
비동기 프로그래밍에서는 하나의 비동기 작업에서 해당 작업이 완료될 때 두 번째 작업을 호출하고 이 작업에 데이터를 전달하는 것이 매우 일반적입니다. 일반적으로 이 작업은 콜백 메서드를 사용하여 수행됩니다. 동시성 런타임에서는 연속 작업을 통해 동일한 기능을 제공합니다. 연속 작업은 선행 작업이라고 하는 다른 작업이 완료될 때 선행 작업에 의해 호출되는 비동기 작업입니다. 연속을 사용하여 다음을 수행할 수 있습니다.
선행 작업의 데이터를 연속 작업에 전달합니다.
연속 작업이 호출되거나 호출되지 않을 정확한 조건을 지정합니다.
연속 작업을 해당 작업이 시작되기 전에 취소하거나 해당 작업이 실행 중일 경우 협조적으로 취소할 수 있습니다.
연속 작업을 예약하는 방법에 대한 힌트를 제공합니다. (이 항목은 Windows 스토어 앱에만 적용됩니다.) 자세한 내용은 C++로 Windows 스토어 앱용 비동기 작업 만들기를 참조하십시오.
한 선행 작업에서 여러 연속 작업을 호출합니다.
여러 선행 작업이 모두 또는 하나라도 완료될 때 하나의 연속 작업을 호출합니다.
길이에 관계없이 여러 연속 작업을 체인으로 연결할 수 있습니다.
선행 작업에서 throw된 예외를 처리하려면 연속 작업을 사용합니다.
이러한 기능을 사용하여 첫 번째 작업이 완료되면 하나 이상의 작업을 실행할 수 있습니다. 예를 들어, 디스크에서 첫 번째 작업을 읽은 후에 파일을 압축하는 연속 작업을 만들 수 있습니다.
다음 예제는 concurrency::task::then 메서드를 사용하여 선행 작업 값이 사용 가능한 경우 이를 인쇄하는 연속 작업의 일정을 예약하도록 이전 예제를 수정합니다.
// 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
*/
임의 길이로 작업을 연결하고 중첩시킬 수 있습니다. 작업에는 여러 연속 작업이 있을 수도 있습니다. 다음 예제에서는 이전 작업의 값이 세 배 증가하는 기본 연속 체인에 대해 설명합니다.
// 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
*/
연속 작업에서 다른 작업이 반환될 수도 있습니다. 취소하지 않으면 이어지는 연속 작업을 수행하기 전에 이 작업이 실행됩니다. 이러한 기술을 비동기 래핑 해제라고 부릅니다. 비동기 래핑 해제는 백그라운드에서 추가 작업을 수행하려하지만 현재 작업이 현재 스레드를 차단하는 것을 원하지 않을 때 유용합니다. (이것은 UI 스레드에서 연속 작업을 실행할 수 있는 Windows 스토어 응용 프로그램에서는 일반적입니다). 다음 예제에서는 세 가지 작업을 보여줍니다. 첫 번째 작업은 연속 작업 전에 실행되는 다른 작업을 반환합니다.
// 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
*/
중요
연속 작업에서 N 형식의 중첩된 작업을 반환하면 결과 작업에는 task<N>이 아닌 N 형식이 있고 중첩된 작업이 완료될 때 완료됩니다.즉, 연속 작업은 중첩된 작업의 래핑 해제를 수행합니다.
[맨 위]
값 기반 연속 작업과 작업 기반 연속 작업의 비교
반환 형식이 T인 task 개체인 경우 형식 T 또는 **task<T>**의 값을 연속 작업에 제공할 수 있습니다. 형식 T를 사용하는 연속 작업은 값 기반 연속 작업이라고 합니다. 값 기반 연속은 선행 작업이 오류 없이 완료되고 취소되지 않을 때 실행하도록 예약됩니다. 매개 변수로 형식 **task<T>**를 사용하는 연속 작업은 작업 기반 연속 작업이라고 합니다. 작업 기반 연속은 선행 작업이 취소되거나 예외를 throw할 때라도 항상 선행 작업이 완료될 때 실행되도록 예약됩니다. 그런 후 task::get을 호출해서 선행 작업의 결과를 가져올 수 있습니다. 선행 작업이 취소된 경우 task::get이 concurrency::task_canceled를 throw합니다. 선행 작업에서 예외를 throw한 경우 task::get이 해당 예외를 다시 throw합니다. 작업 기반 연속 작업은 선행 작업이 취소되면 취소된 것으로 표시되지 않습니다.
[맨 위]
작업 작성
이 단원에서는 concurrency::when_all 및 concurrency::when_any 함수에 대해 설명합니다. 이 함수를 통해 공통 패턴을 구현하도록 여러 작업을 구성할 수 있습니다.
when_all 함수
when_all 함수에서 일련의 작업이 완료된 후 완료된 작업을 생성합니다. 이 함수는 해당 집합에 각 작업 결과가 포함된 std::vector 개체를 반환합니다. 다음 기본 예제에서는 when_all을 사용하여 다른 작업 3개가 완료됨을 나타내는 작업을 만듭니다.
// 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.
*/
참고
when_all으로 전달하는 작업은 동일해야 합니다.즉, 모두 동일한 형식을 반환합니다.
&& 다음 예제와 같이 구문을 사용하여 일련의 작업이 완료된 후에 완료되는 작업을 생성할 수 있습니다.
auto t = t1 && t2; // same as when_all
이러한 작업이 완료된 후 작업을 수행하기 위해 when_all와 함께 연속으로 사용하는 것이 일반적입니다. 다음 예제에서는 이전 예제를 수정하여 각각 int 결과를 생성하는 세 가지 작업의 합계를 인쇄합니다.
// 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.
*/
이 예제에서는 작업 기반 연속을 생성하도록 **task<vector<int>>**를 지정할 수도 있습니다.
작업 집합의 작업이 취소되거나 예외를 throw하는 경우 when_all는 즉시 완료되고 남은 작업이 완료되기를 기다리지 않습니다. 예외가 throw되는 경우 런타임은 when_all에서 반환하는 작업 개체에서 task::get 또는 task::wait를 호출할 때 예외를 다시 throw합니다. 둘 이상의 작업을 throw 하는 경우 런타임에서 둘 중 하나를 선택합니다. 따라서 모든 작업이 완료된 후 모든 예외를 관찰했는지 확인합니다. 처리되지 않은 작업 예외로 응용 프로그램이 종료됩니다.
다음은 프로그램에서 모든 예외를 확인하는 데 사용할 수 있는 유틸리티 함수입니다. 제공된 범위에 있는 각 작업에 대해 observe_all_exceptions는 다시 throw할 예외를 트리거한 다음 해당 예외를 제거합니다.
// 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.
}
});
});
}
C++ 및 XAML을 사용하고 파일 집합을 디스크에 기록하는 Windows 스토어 응용 프로그램을 고려합니다. 다음 예제에서는 when_all 및 observe_all_exceptions를 사용하여 프로그램에서 모든 예외를 관찰하도록 하는 방법을 보여 줍니다.
// 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();
}
});
}
이 예제를 실행하려면
MainPage.xaml에 Button 컨트롤을 추가합니다.
<Button x:Name="Button1" Click="Button_Click">Write files</Button>
MainPage.xaml.h에서 이러한 전방 선언을 MainPage 클래스 선언의 private 섹션에 추가합니다.
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);
MainPage.xaml.cpp에서 Button_Click 이벤트 처리기를 구현합니다.
// 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. }); }
MainPage.xaml.cpp에서 예제와 같이 WriteFilesAsync을 구현합니다.
팁
when_all는 해당 결과로 task를 생성하는 비차단 함수입니다.task::wait와 다르게 ASTA(응용 프로그램 STA) 스레드에 대한 Windows 스토어 응용 프로그램에서 이 함수를 호출하는 것이 안전합니다.
[맨 위]
when_any 함수
when_any 함수는 작업 집합의 첫 번째 작업이 완료될 때 완료되는 작업을 생성합니다. 이 함수는 완료된 작업 결과 및 집합의 해당 작업에 대한 인덱스를 포함하는 std::pair 개체를 반환합니다.
when_any 함수는 다음과 같은 시나리오에서 특히 유용합니다.
중복 작업입니다. 알고리즘 또는 여러 방법으로 수행할 수 있는 작업을 고려합니다. when_any 함수를 사용하여 먼저 완료되는 작업을 선택한 다음 나머지 작업을 취소할 수 있습니다.
인터리브 작업입니다. 모두 완료되어야 하는 여러 작업을 시작하고 when_any 함수를 사용하여 각 작업이 완료되면 결과를 처리할 수 있습니다. 하나의 작업이 완료되면 하나 이상의 추가 작업을 시작할 수 있습니다.
제한된 작업입니다. when_any 함수를 사용하여 동시 작업 수를 제한하여 이전 시나리오를 확장할 수 있습니다.
만료된 작업입니다. when_any 함수를 사용하여 하나 이상의 작업 및 특정 시간 이후 완료되는 작업 중에서 선택할 수 있습니다.
when_all와 마찬가지로 작업 집합에서 첫 번째 작업이 완료될 때 when_any이 작업을 수행한 연속을 사용하는 것이 일반적입니다. 다음 기본 예제에서는 when_any를 사용하여 다른 작업 중 처음 3개가 완료됨을 나타내는 작업을 만듭니다.
// 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.
*/
이 예제에서는 작업 기반 연속을 생성하도록 **task<pair<int, size_t>>**를 지정할 수도 있습니다.
참고
when_all과 마찬가지로, when_any에 전달하는 작업은 모두 동일한 형식을 반환해야 합니다.
다음 예제와 같이 || 구문을 사용하여 일련의 작업 중 첫 번째 작업이 완료된 후에 완료되는 작업을 생성할 수 있습니다.
auto t = t1 || t2; // same as when_any
팁
when_all와 마찬가지로 when_any은 비차단되며 ASTA 스레드에서 Windows 스토어 응용 프로그램을 호출하는 것이 안전합니다.
[맨 위]
지연된 작업 실행
일부 경우에는 조건을 충족할 때까지 작업 실행을 미루거나 외부 이벤트에 대한 응답으로 작업을 시작해야 합니다. 예를 들어, 비동기 프로그래밍에서 I/O 완료 이벤트에 응답하여 작업을 시작해야 할 수 있습니다.
이를 수행하는 두 가지 방법은 연속 작업을 사용하거나 작업을 시작하고 작업의 작업 함수 내의 이벤트를 기다리는 방법입니다. 그러나 다음 방법 중 하나를 사용할 수 없는 경우가 있습니다. 예를 들어 연속 작업을 만들려면 선행 작업이 있어야 합니다. 그러나 선행 작업이 없는 경우 작업 완료 이벤트를 만들고 나중에 사용 가능해지면 선행 작업에 해당 완료 이벤트를 연결할 수 있습니다. 또한 대기 작업이 스레드를 차단하기 때문에 비동기 작업이 완료될 때 작업을 수행하도록 작업 완료 이벤트를 사용하고 그로 인해 스레드를 해제할 수 있습니다.
concurrency::task_completion_event 클래스는 작업의 컴퍼지션을 단순화합니다. task 클래스와 마찬가지로 형식 매개 변수 T는 작업의 결과로 생성되는 형식입니다. 작업이 값을 반환하지 않을 경우 이 형식은 void일 수 있습니다. T는 const 한정자를 사용할 수 없습니다. 일반적으로 task_completion_event 개체는 해당 값을 사용할 수 있게 될 때 신호를 제공하는 스레드 또는 작업에 제공됩니다. 동시에 하나 이상의 작업이 해당 이벤트의 수신기로 설정됩니다. 이벤트가 설정되었으면 수신기 작업이 완료되고 해당 연속 작업의 실행이 예약됩니다.
task_completion_event를 사용하여 지연 후 완료되는 작업을 구현하는 예는 방법: 지연 후 완료되는 작업 만들기를 참조하십시오.
[맨 위]
작업 그룹
작업 그룹은 작업 컬렉션을 구성합니다. 작업 그룹은 작업 가로채기 큐에 작업을 넣습니다. 스케줄러는 이 큐에서 작업을 제거하고 사용 가능한 컴퓨팅 리소스에서 해당 작업을 실행합니다. 작업 그룹에 작업을 추가한 후 모든 작업이 끝날 때까지 기다리거나 아직 시작되지 않은 작업을 취소할 수 있습니다.
PPL은 concurrency::task_group 및 concurrency::structured_task_group 클래스를 사용하여 작업 그룹을 나타내고 concurrency::task_handle 클래스를 사용하여 이러한 그룹에서 실행되는 작업을 나타냅니다. task_handle 클래스는 작업을 수행하는 코드를 캡슐화합니다. task 클래스와 마찬가지로 이 작업 함수는 람다 함수, 함수 포인터 또는 함수 개체의 형식으로 제공됩니다. 일반적으로 task_handle 개체를 직접 사용할 필요는 없습니다. 대신 작업 함수를 작업 그룹에 전달하면 작업 그룹은 task_handle 개체를 만들고 관리합니다.
PPL은 작업 그룹을 비구조적 작업 그룹 및 구조적 작업 그룹이라는 두 개의 범주로 분할합니다. PPL은 task_group 클래스를 사용하여 비구조적 작업 그룹을 나타내고 structured_task_group 클래스를 사용하여 구조적 작업 그룹을 나타냅니다.
중요
PPL은 structured_task_group 클래스를 사용하여 작업 집합을 병렬로 실행하는 concurrency::parallel_invoke 알고리즘도 정의합니다.parallel_invoke 알고리즘의 구문이 더 간결하므로 가능하면 structured_task_group 클래스 대신 이 알고리즘을 사용하는 것이 좋습니다.병렬 알고리즘 항목에서 parallel_invoke에 대해 더 자세히 설명합니다.
동시에 실행할 독립적인 작업이 여러 개 있고 계속하지 않고 모든 작업이 끝날 때까지 기다려야 하는 경우 parallel_invoke를 사용합니다. 이 기술은 일반적으로 분기 및 조인 병렬 처리라고 부릅니다. 동시에 실행할 독립적인 작업이 여러 개 있지만 나중에 작업이 끝날 때까지 기다리려는 경우 task_group을 사용합니다. 예를 들어 task_group개체에 작업을 추가하고 다른 함수 또는 다른 스레드에서 작업이 끝날 때까지 기다릴 수 있습니다.
작업 그룹에서는 취소 개념을 지원합니다. 취소를 사용하면 전체 작업(operation)을 취소하려는 모든 활성 작업(task)에 신호를 보낼 수 있습니다. 취소를 사용하면 아직 시작되지 않은 작업을 시작되지 않게 할 수도 있습니다. 취소에 대한 자세한 내용은 PPL에서의 취소를 참조하십시오.
런타임에서는 관련된 작업 그룹이 끝나기를 기다릴 때 작업에서 예외를 throw하고 해당 예외를 처리할 수 있게 하는 예외 처리 모델을 제공합니다. 예외 처리 모델에 대한 자세한 내용은 동시성 런타임에서 예외 처리를 참조하십시오.
[맨 위]
task_group을 structured_task_group과 비교
structured_task_group 클래스 대신 task_group 또는 parallel_invoke를 사용하는 것이 좋지만, 예를 들어 여러 가지 작업을 수행하거나 취소에 대한 지원이 필요한 병렬 알고리즘을 작성할 때 structured_task_group을 사용하는 경우도 있습니다. 이 단원에서는 task_group 클래스와 structured_task_group 클래스의 차이점에 대해 설명합니다.
task_group 클래스는 스레드로부터 안전합니다. 따라서 여러 스레드의 task_group 개체에 작업을 추가하고 여러 스레드의 task_group 개체를 기다리거나 취소할 수 있습니다. structured_task_group 개체의 생성과 소멸은 동일한 어휘 범위에서 발생해야 합니다. 또한 structured_task_group 개체에 대한 모든 작업은 동일한 스레드에서 발생해야 합니다. 이 규칙에 대한 예외는 concurrency::structured_task_group::cancel 및 concurrency::structured_task_group::is_canceling 메서드입니다. 자식 작업은 언제든지 이러한 메서드를 호출하여 부모 작업 그룹을 취소하거나 취소를 확인할 수 있습니다.
concurrency::task_group::wait 또는 concurrency::task_group::run_and_wait 메서드를 호출한 후 task_group 개체에서 추가 작업을 실행할 수 있습니다. 반대로 concurrency::structured_task_group::wait 또는 concurrency::structured_task_group::run_and_wait 메서드를 호출한 후 structured_task_group 개체에 대한 추가 작업을 실행하는 경우 동작이 정의되지 않습니다.
structured_task_group 클래스는 스레드 간에 동기화하지 않으므로 task_group 클래스보다 실행 오버헤드가 적습니다. 따라서 여러 스레드에서 작업을 예약할 필요가 없고 parallel_invoke 알고리즘을 사용할 수 없는 경우 structured_task_group 클래스를 사용하면 성능이 높은 코드를 작성할 수 있습니다.
다른 structured_task_group 개체 내에 있는 structured_task_group 개체를 사용하는 외부 개체를 완료하기 전에 먼저 내부 개체를 완료하고 소멸시켜야 합니다. task_group 클래스를 사용하는 경우에는 외부 그룹을 완료하기 전에 중첩된 작업 그룹을 완료할 필요가 없습니다.
비구조적 작업 그룹과 구조적 작업 그룹은 다양한 방식으로 작업 핸들을 사용합니다. 작업 함수를 task_group 개체에 직접 전달할 수 있으며 그러면 task_group 개체에서 자동으로 작업을 만들고 관리합니다. structured_task_group 클래스를 사용하려면 각 작업의 task_handle 개체를 관리해야 합니다. 관련된 structured_task_group 개체의 수명이 지속되는 동안 모든 task_handle 개체가 유효한 상태로 유지되어야 합니다. 다음 기본 예제에서와 같이 concurrency::make_task 함수를 사용하여 task_handle 개체를 만듭니다.
// 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);
}
여러 가지 작업이 있는 경우에 사용할 작업 핸들을 관리하려면 _malloca와 같은 스택 할당 루틴 또는 std::vector와 같은 컨테이너 클래스를 사용합니다.
task_group과 structured_task_group 모두 취소 기능을 지원합니다. 취소에 대한 자세한 내용은 PPL에서의 취소를 참조하십시오.
[맨 위]
예제
다음 기본 예제에서는 작업 그룹을 사용하는 방법을 보여 줍니다. 이 예제에서는 parallel_invoke 알고리즘을 사용하여 두 작업을 병렬로 수행합니다. 각 작업은 하위 작업을 task_group 개체에 추가합니다. task_group 클래스를 사용하면 여러 작업이 이 개체에 작업을 동시에 추가할 수 있습니다.
// 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();
}
이 예제를 실행하면 다음과 같은 샘플 결과가 출력됩니다.
parallel_invoke 알고리즘은 작업을 동시에 실행하므로 출력 메시지의 순서가 다를 수 있습니다.
parallel_invoke 알고리즘을 사용하는 방법을 보여 주는 전체 예제를 보려면 방법: parallel_invoke를 사용하여 병렬 정렬 루틴 작성 및 방법: parallel_invoke를 사용하여 병렬 작업 실행을 참조하십시오. task_group 클래스를 사용하여 비동기 미래를 구현하는 전체 예제를 보려면 연습: 미래 구현을 참조하십시오.
[맨 위]
강력한 프로그래밍
작업, 작업 그룹 및 병렬 알고리즘을 사용할 때는 취소 및 예외 처리의 역할을 이해하고 있어야 합니다. 예를 들어 병렬 작업 트리에서 취소된 작업으로 인해 자식 작업이 실행되지 않게 됩니다. 자식 작업 중 하나가 사용자의 응용 프로그램에 중요한 작업(예: 리소스 해제)을 수행하는 경우 이로 인해 문제가 발생할 수 있습니다. 또한 자식 작업에서 예외를 throw하면 해당 예외가 개체 소멸자를 통해 전파되고 응용 프로그램에서 정의되지 않은 동작이 발생할 수 있습니다. 이러한 지점을 보여 주는 예제를 보려면 병렬 패턴 라이브러리의 유용한 정보 문서에 있는 취소 및 예외 처리가 개체 소멸에 미치는 영향 이해 단원을 참조하십시오. PPL의 취소 및 예외 처리 모델에 대한 자세한 내용은 PPL에서의 취소 및 동시성 런타임에서 예외 처리를 참조하십시오.
[맨 위]
관련 항목
제목 |
설명 |
---|---|
parallel_invoke 알고리즘을 사용하여 바이토닉 정렬 알고리즘의 성능을 향상시키는 방법을 보여 줍니다. |
|
parallel_invoke 알고리즘을 사용하여 공유 데이터 소스에 대해 여러 작업을 수행하는 프로그램의 성능을 향상시키는 방법을 보여 줍니다. |
|
task, cancellation_token_source, cancellation_token 및 task_completion_event 클래스를 사용하여 지연 후 완료되는 작업을 만드는 방법을 보여줍니다. |
|
동시성 런타임의 기존 기능을 결합하여 더 많은 작업을 수행하는 방법을 보여 줍니다. |
|
동시 응용 프로그램 개발을 위해 명령형 프로그래밍 모델을 제공하는 PPL에 대해 설명합니다. |