다음을 통해 공유


작업 병렬 처리(동시성 런타임)

동시성 런타임에서 작업은 특정 작업을 수행하고 다른 작업과 병렬로 실행되는 작업 단위입니다. 작업 그룹으로 구성된 더 세분화된 추가 작업으로 작업을 분해할 수 있습니다.

비동기 코드를 작성하고 비동기 작업이 완료된 후 일부 작업이 발생하도록 할 때 작업을 사용합니다. 예를 들어, 파일을 비동기적으로 읽는 작업을 사용한 다음 사용 가능하게 된 후 데이터를 처리하는 다른 작업인 이 문서 뒷부분에서 설명하는 연속 작업을 사용할 수 있습니다. 반대로, 작업 그룹을 사용하여 병렬 작업을 더 작은 조각으로 분해할 수 있습니다. 예를 들어 남은 작업을 두 개의 파티션으로 나누는 재귀 알고리즘이 있다고 가정해 봅니다. 작업 그룹을 사용하여 이러한 파티션을 동시에 실행한 다음 구분된 작업이 완료되기를 대기할 수 있습니다.

컬렉션의 모든 요소에 동일한 루틴을 병렬로 적용하려면 작업 또는 작업 그룹 대신 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::getconcurrency::task_canceled를 throw합니다. 선행 작업에서 예외를 throw한 경우 task::get이 해당 예외를 다시 throw합니다. 작업 기반 연속 작업은 선행 작업이 취소되면 취소된 것으로 표시되지 않습니다.

[맨 위]

작업 작성

이 단원에서는 concurrency::when_allconcurrency::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_allobserve_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();
        }
    });
}

이 예제를 실행하려면

  1. MainPage.xaml에 Button 컨트롤을 추가합니다.

    <Button x:Name="Button1" Click="Button_Click">Write files</Button>
    
  2. 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);
    
  3. 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.
        });
    }
    
  4. 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_groupconcurrency::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::cancelconcurrency::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_groupstructured_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 알고리즘을 사용하여 바이토닉 정렬 알고리즘의 성능을 향상시키는 방법을 보여 줍니다.

방법: parallel_invoke를 사용하여 병렬 작업 실행

parallel_invoke 알고리즘을 사용하여 공유 데이터 소스에 대해 여러 작업을 수행하는 프로그램의 성능을 향상시키는 방법을 보여 줍니다.

방법: 지연 후 완료되는 작업 만들기

task, cancellation_token_source, cancellation_tokentask_completion_event 클래스를 사용하여 지연 후 완료되는 작업을 만드는 방법을 보여줍니다.

연습: 미래 구현

동시성 런타임의 기존 기능을 결합하여 더 많은 작업을 수행하는 방법을 보여 줍니다.

PPL(병렬 패턴 라이브러리)

동시 응용 프로그램 개발을 위해 명령형 프로그래밍 모델을 제공하는 PPL에 대해 설명합니다.

참고 항목

작업 클래스(동시성 런타임)

task_completion_event 클래스

when_all 함수

when_any 함수

task_group 클래스

parallel_invoke 함수

structured_task_group 클래스