다음을 통해 공유


PPL에서의 취소

업데이트: 2011년 3월

이 항목에서는 PPL(병렬 패턴 라이브러리)에서 취소의 역할, 병렬 작업을 취소하는 방법, 작업 그룹이 취소되는 시기를 결정하는 방법 등에 대해 설명합니다.

단원

  • 병렬 작업 트리

  • 병렬 작업 취소

  • 병렬 알고리즘 취소

  • 취소를 사용하지 않는 경우

병렬 작업 트리

PPL에서는 세분화된 작업 및 계산을 관리할 때 작업 그룹을 사용합니다. 작업 그룹을 중첩시켜 병렬 작업의 트리를 구성할 수 있습니다. 다음 그림에서는 병렬 작업 트리를 보여 줍니다. 이 그림에서 tg1tg2는 작업 그룹을 나타내고 t1, t2, t3, t4t5는 작업을 나타냅니다.

병렬 작업 트리

다음 예제에서는 그림의 트리를 만드는 데 필요한 코드를 보여 줍니다. 이 예제에서 tg1tg2Concurrency::structured_task_group 개체이고 t1, t2, t3, t4t5Concurrency::task_handle 개체입니다.

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

using namespace Concurrency;
using namespace std;

void create_task_tree()
{   
   // Create a task group that serves as the root of the tree.
   structured_task_group tg1;

   // Create a task that contains a nested task group.
   auto t1 = make_task([&] {
      structured_task_group tg2;

      // Create a child task.
      auto t4 = make_task([&] {
         // TODO: Perform work here.
      });

      // Create a child task.
      auto t5 = make_task([&] {
         // TODO: Perform work here.
      });

      // Run the child tasks and wait for them to finish.
      tg2.run(t4);
      tg2.run(t5);
      tg2.wait();
   });

   // Create a child task.
   auto t2 = make_task([&] {
      // TODO: Perform work here.
   });

   // Create a child task.
   auto t3 = make_task([&] {
      // TODO: Perform work here.
   });

   // Run the child tasks and wait for them to finish.
   tg1.run(t1);
   tg1.run(t2);
   tg1.run(t3);
   tg1.wait();   
}

[맨 위로 이동]

병렬 작업 취소

병렬 작업을 취소할 수 있는 두 가지 방법이 있습니다. 첫 번째 방법은 Concurrency::task_group::cancel 또는 Concurrency::structured_task_group::cancel 메서드를 호출하는 것입니다. 다른 방법은 작업 함수 본문에서 예외를 throw하는 것입니다.

cancel 메서드는 병렬 작업 트리를 취소할 때 예외 처리보다 효율성이 큽니다. cancel 메서드는 작업 그룹 및 자식 작업 그룹을 위에서 아래로 취소합니다. 반대로 예외 처리는 아래에서 위로 동작하며, 예외가 위쪽으로 전파할 때 각 자식 작업 그룹을 독립적으로 취소해야 합니다.

다음 단원에서는 cancel 메서드 및 예외 처리를 사용하여 병렬 작업을 취소하는 방법을 보여 줍니다. 병렬 작업을 취소하는 추가 예제를 보려면 방법: 취소를 사용하여 병렬 루프 중단방법: 예외 처리를 사용하여 병렬 루프 중단을 참조하십시오.

cancel 메서드를 사용하여 병렬 작업 취소

Concurrency::task_group::cancelConcurrency::structured_task_group::cancel 메서드는 작업 그룹을 취소된 상태로 설정합니다.

참고

런타임은 예외 처리를 사용하여 취소를 구현합니다. 코드에서 이러한 예외를 catch하거나 처리하지 마십시오. 또한 작업을 위한 함수 본문에 예외로부터 안전한 코드를 작성하는 것이 좋습니다. 예를 들어 작업 본문에서 예외가 throw될 때 리소스가 올바르게 처리되도록 하기 위해 RAII(Resource Acquisition Is Initialization) 패턴을 사용할 수 있습니다. RAII 패턴을 사용하여 취소 가능한 작업에서 리소스를 정리하는 예제를 전체적으로 보려면 연습: 사용자 인터페이스 스레드에서 작업 제거를 참조하십시오.

cancel을 호출한 후에는 작업 그룹에서 이후 작업이 시작되지 않습니다. cancel 메서드는 여러 자식 작업에 의해 호출될 수 있습니다. 취소된 작업은 Concurrency::task_group::waitConcurrency::structured_task_group::wait 메서드에서 Concurrency::canceled를 반환하도록 합니다.

cancel 메서드는 자식 작업에만 영향을 줍니다. 예를 들어 병렬 작업 트리 그림에서 tg1 작업 그룹을 취소하면 트리의 모든 작업(t1, t2, t3, t4t5)에 영향을 줍니다. 중첩 작업 그룹인 tg2를 취소할 경우에는 t4t5 작업에만 영향을 줍니다.

cancel 메서드를 호출하면 모든 자식 작업 그룹도 취소됩니다. 그러나 병렬 작업 트리에서 해당 작업 그룹의 부모에는 취소가 적용되지 않습니다. 다음 예제에서는 병렬 작업 트리 그림에서 빌드하여 이를 보여 줍니다.

첫 번째 예제에서는 tg2 작업 그룹의 자식인 t4 작업에 대한 작업 함수를 만듭니다. 이 작업 함수는 루프에서 work 함수를 호출합니다. work에 대한 호출이 실패하면 작업이 부모 작업 그룹을 취소합니다. 이렇게 되면 tg2 작업 그룹이 취소된 상태로 설정되지만 tg1 작업 그룹은 취소되지 않습니다.

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }         
});

두 번째 예제는 첫 번째 예제와 유사하지만 작업이 tg1 작업 그룹을 취소한다는 점이 다릅니다. 이렇게 하면 트리의 모든 작업(t1, t2, t3, t4t5)에 영향을 줍니다.

auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel all tasks in the tree.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg1.cancel();
         break;
      }
   }   
});

structured_task_group 클래스는 스레드로부터 안전하지 않으므로 자식 작업에서 부모 structured_task_group 개체의 메서드를 호출하면 지정되지 않은 동작이 발생합니다. 그러나 structured_task_group::cancelConcurrency::structured_task_group::is_canceling 메서드에는 이 규칙이 적용되지 않습니다. 자식 작업은 이러한 메서드를 호출하여 부모 작업을 취소하고 취소를 확인할 수 있습니다.

예외를 사용하여 병렬 작업 취소

동시성 런타임에서 예외 처리 항목에서는 동시성 런타임에서 예외를 사용하여 오류를 전달하는 방법에 대해 설명합니다. 그러나 모든 예외가 오류를 나타내는 것은 아닙니다. 예를 들어 검색 알고리즘은 결과를 발견하면 관련 작업 그룹을 취소할 수 있습니다. 그러나 앞에서 언급한 대로 예외 처리는 병렬 작업을 취소할 때 cancel 메서드를 사용하는 것보다 효율성이 떨어지는 방법입니다.

작업 그룹에 전달하는 작업 함수의 본문에서 예외를 throw하는 경우 런타임에서는 해당 예외를 저장하고, 작업 그룹이 끝날 때까지 기다리는 컨텍스트로 예외를 마샬링합니다. cancel 메서드와 마찬가지로 런타임에서는 아직 시작되지 않은 작업을 취소하고 새 작업을 수락하지 않습니다.

세 번째 예제는 두 번째 예제와 유사하지만 t4 작업이 예외를 throw하여 tg2 작업 그룹을 취소한다는 점이 다릅니다. 이 예제에서는 tg2 작업 그룹에서 자식 작업이 끝날 때까지 기다리는 경우 try-catch 블록을 사용하여 취소를 확인합니다. 이렇게 되면 첫 번째 예제와 마찬가지로 tg2 작업 그룹이 취소된 상태로 설정되지만 tg1 작업 그룹은 취소되지 않습니다.

structured_task_group tg2;

// Create a child task.      
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, throw an exception to 
      // cancel the parent task.
      bool succeeded = work(i);
      if (!succeeded)
      {
         throw exception("The task failed");
      }
   }         
});

// Create a child task.
auto t5 = make_task([&] {
   // TODO: Perform work here.
});

// Run the child tasks.
tg2.run(t4);
tg2.run(t5);

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg2.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

이 네 번째 예제에서는 예외 처리를 사용하여 전체 작업 트리를 취소합니다. 이 예제에서는 tg2 작업 그룹이 자식 작업을 기다리는 경우 대신 tg1 작업 그룹이 자식 작업이 끝날 때까지 기다리는 경우에 예외를 catch합니다. 이렇게 되면 두 번째 예제와 마찬가지로 트리에서 tg1tg2 작업 그룹이 둘 다 취소된 상태로 설정됩니다.

// Run the child tasks.
tg1.run(t1);
tg1.run(t2);
tg1.run(t3);   

// Wait for the tasks to finish. The runtime marshals any exception
// that occurs to the call to wait.
try
{
   tg1.wait();
}
catch (const exception& e)
{
   wcout << e.what() << endl;
}

task_group::waitstructured_task_group::wait 메서드는 자식 작업이 예외를 throw할 때 throw되므로 두 메서드로부터 반환 값을 받을 수 없습니다.

작업이 취소되는 시기 결정

취소는 협조적입니다. 따라서 즉시 실행되지 않습니다. 작업 그룹이 취소되는 경우 각 자식 작업에서 런타임을 호출하면 중단 지점이 트리거되어 런타임에서 내부 예외 형식을 throw하고 catch하여 활성 작업을 취소할 수 있습니다. 동시성 런타임에서는 특정 중단 지점을 정의하지 않습니다. 런타임에 대한 어떤 호출에서나 중단 지점이 발생할 수 있습니다. 런타임에서는 취소를 수행하기 위해 런타임에서 throw하는 예외를 처리해야 합니다. 따라서 작업 본문에서 알 수 없는 예외를 처리하지 마십시오.

자식 작업에서 시간이 오래 걸리는 작업을 수행하는 경우 런타임을 호출하지 않으면 주기적으로 취소 여부를 확인하고 적시에 종료해야 합니다. 다음 예제에서는 작업 취소 시기를 결정할 수 있는 한 가지 방법을 보여 줍니다. t4 작업은 부모 작업 그룹에서 오류가 발생하면 취소합니다. t5 작업은 때때로 structured_task_group::is_canceling 메서드를 호출하여 취소 여부를 확인합니다. 부모 작업 그룹이 취소되면 t5 작업은 메시지를 출력하고 종료합니다.

structured_task_group tg2;

// Create a child task.
auto t4 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // Call a function to perform work.
      // If the work function fails, cancel the parent task
      // and break from the loop.
      bool succeeded = work(i);
      if (!succeeded)
      {
         tg2.cancel();
         break;
      }
   }
});

// Create a child task.
auto t5 = make_task([&] {
   // Perform work in a loop.
   for (int i = 0; i < 1000; ++i)
   {
      // To reduce overhead, occasionally check for 
      // cancelation.
      if ((i%100) == 0)
      {
         if (tg2.is_canceling())
         {
            wcout << L"The task was canceled." << endl;
            break;
         }
      }

      // TODO: Perform work here.
   }
});

// Run the child tasks and wait for them to finish.
tg2.run(t4);
tg2.run(t5);
tg2.wait();

이 예제에서는 해당 작업 루프의 100번째 반복마다 취소 여부를 확인합니다. 취소 여부를 확인하는 빈도는 수행하는 작업의 양과 작업이 취소에 응답해야 하는 속도에 따라 달라집니다.

부모 작업 그룹 개체에 대한 액세스 권한이 없는 경우에는 Concurrency::is_current_task_group_canceling 함수를 호출하여 부모 작업 그룹이 취소되는지 여부를 확인합니다.

[맨 위로 이동]

병렬 알고리즘 취소

Concurrency::parallel_for와 같이 PPL의 병렬 알고리즘은 작업 그룹을 기반으로 합니다. 따라서 같은 기법을 대부분 사용하여 병렬 알고리즘을 취소할 수 있습니다.

다음 예제에서는 병렬 알고리즘을 취소하는 몇 가지 방법을 보여 줍니다.

다음 예제에서는 Concurrency::structured_task_group::run_and_wait 메서드를 사용하여 parallel_for 알고리즘을 호출합니다. structured_task_group::run_and_wait 메서드는 제공된 작업이 끝날 때까지 기다립니다. structured_task_group 개체는 작업 함수가 작업을 취소할 수 있도록 합니다.

// To enable cancelation, call parallel_for in a task group.
structured_task_group tg;

task_group_status status = tg.run_and_wait([&] {
   parallel_for(0, 100, [&](int i) {
      // Cancel the task when i is 50.
      if (i == 50)
      {
         tg.cancel();
      }
      else
      {
         // TODO: Perform work here.
      }
   });
});

// Print the task group status.
wcout << L"The task group status is: ";
switch (status)
{
case not_complete:
   wcout << L"not complete." << endl;
   break;
case completed:
   wcout << L"completed." << endl;
   break;
case canceled:
   wcout << L"canceled." << endl;
   break;
default:
   wcout << L"unknown." << endl;
   break;
}

이 예제의 결과는 다음과 같습니다.

The task group status is: canceled.

다음 예제에서는 예외 처리를 사용하여 parallel_for 루프를 취소합니다. 런타임에서는 이 예외를 호출 컨텍스트로 마샬링합니다.

try
{
   parallel_for(0, 100, [&](int i) {
      // Throw an exception to cancel the task when i is 50.
      if (i == 50)
      {
         throw i;
      }
      else
      {
         // TODO: Perform work here.
      }
   });
}
catch (int n)
{
   wcout << L"Caught " << n << endl;
}

이 예제의 결과는 다음과 같습니다.

Caught 50

다음 예제에서는 부울 플래그를 사용하여 parallel_for 루프에서 취소를 조정합니다. 이 예제에서는 cancel 메서드 또는 예외 처리를 사용하여 전체 작업 집합을 취소하지 않으므로 모든 작업이 실행됩니다. 따라서 이 방법은 취소 메커니즘보다 계산 오버헤드가 더 클 수 있습니다.

// Create a Boolean flag to coordinate cancelation.
bool canceled = false;

parallel_for(0, 100, [&](int i) {
   // For illustration, set the flag to cancel the task when i is 50.
   if (i == 50)
   {
      canceled = true;
   }

   // Perform work if the task is not canceled.
   if (!canceled)
   {
      // TODO: Perform work here.
   }
});

각 취소 방법은 다른 방법보다 우수한 이점이 있습니다. 따라서 특정 요구에 맞는 방법을 선택하십시오.

[맨 위로 이동]

취소를 사용하지 않는 경우

관련된 작업 그룹의 각 멤버가 적시에 종료할 수 있는 경우 취소를 사용하는 것이 적합합니다. 그러나 취소가 응용 프로그램에 적합하지 않는 몇 가지 시나리오가 있습니다. 예를 들어 작업 취소는 협력적이므로 개별 작업이 차단되면 전체 작업 집합이 취소되지 않습니다. 예를 들어 한 작업이 아직 시작되지 않았는데 다른 활성 작업의 차단을 해제하는 경우 작업 그룹이 취소되면 해당 작업이 시작되지 않습니다. 이로 인해 응용 프로그램에서 교착 상태가 발생할 수 있습니다. 취소를 사용하는 것이 적합하지 않을 수 있는 경우의 두 번째 예는 작업이 취소되었지만 작업의 자식 작업에서 리소스 해제와 같은 중요한 작업을 수행하는 경우입니다. 부모 작업(task)을 취소하면 전체 작업(task) 집합이 취소되므로 해당 작업(operation)은 실행되지 않습니다. 이러한 지점을 보여 주는 예제를 보려면 병렬 패턴 라이브러리의 유용한 정보 항목에 있는 취소 및 예외 처리가 개체 소멸에 미치는 영향 이해 단원을 참조하십시오.

[맨 위로 이동]

관련 항목

참조

task_group 클래스

structured_task_group 클래스

parallel_for 함수

변경 기록

날짜

변경 내용

이유

2011년 3월

취소를 사용하지 않는 경우 단원에 다른 사례를 추가했습니다.

향상된 기능 관련 정보