共用方式為


PPL 中的取消

更新:2011 年 3 月

本主題說明如何取消平行模式程式庫 (PPL) 中的角色、如何取消平行工作,以及如何判斷工作群組是否已取消。

章節

  • 平行工作樹狀結構

  • 取消平行工作

  • 取消平行演算法

  • 不使用取消的時機

平行工作樹狀結構

PPL 可使用工作群組來管理更精細的工作和計算。 您可以將工作群組巢狀化,以形成平行工作的「樹狀結構」(Tree)。 下圖顯示平行工作樹狀結構。 在此圖例中,tg1tg2 代表工作群組;t1t2t3t4t5 代表工作。

平行工作樹狀結構

下列範例顯示在圖例中建立樹狀結構時所需的程式碼。 在此範例中,tg1tg2Concurrency::structured_task_group 物件;t1t2t3t4t5Concurrency::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 方法。 另一種方式是在工作的工作函式主體中擲回例外狀況。

在取消平行工作樹狀結構時,cancel 方法比例外狀況處理來得有效率。 cancel 方法會以由上而下的方式取消工作群組和任何子工作群組。 相反地,例外狀況處理則會由下而上執行,且必須在例外狀況往上傳播時個別取消每一個子工作群組。

下列各節說明如何使用 cancel 方法和例外狀況處理來取消平行工作。 如需取消平行工作的相關範例,請參閱 HOW TO:使用取消來中斷平行迴圈HOW TO:使用例外狀況處理來中斷平行迴圈

使用取消方法來取消平行工作

Concurrency::task_group::cancelConcurrency::structured_task_group::cancel 方法會將工作群組設為已取消的狀態。

注意事項注意事項

執行階段使用例外處理來實作取消作業。 請不要攔截或處理程式碼中的這些例外狀況。 此外,建議您函式主體中為您的工作撰寫無例外狀況之虞的程式碼。 例如,當工作的主體中擲回例外狀況時,您可以使用「資源擷取為初始設定」(Resource Acquisition Is Initialization,RAII) 模式確保正確處理資源。 如需使用 RAII 模式在可取消的工作中清除資源的完整範例,請參閱逐步解說:從使用者介面執行緒中移除工作

在您呼叫 cancel 之後,工作群組即不會啟動未來的工作。 cancel 方法可由多項子工作來呼叫。 工作取消後,會使 Concurrency::task_group::waitConcurrency::structured_task_group::wait 方法傳回 Concurrency::canceled

cancel 方法只會影響子工作。 例如,如果您在平行工作樹狀結構的圖例中取消了工作群組 tg1,則樹狀結構中的所有工作 (t1t2t3t4t5) 都會受到影響。 如果您取消了巢狀工作群組 tg2,則只有 t4t5 工作會受到影響。

當您呼叫 cancel 方法時,所有的子工作群組也都會被取消。 但是,取消並不會影響平行工作樹狀結構中的工作群組的任何父代。 下列範例透過建置平行工作樹狀結構圖例加以說明。

其中的第一個範例會建立工作 t4 的工作函式,此工作是工作群組 tg2 的子系。 此工作函式會以迴圈呼叫函式 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。 這會影響樹狀結構中的所有工作 (t1t2t3t4t5)。

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 方法取消平行工作,處理例外狀況顯得較沒效率。

當您在傳遞至工作群組的工作函式主體中擲回例外狀況時,執行階段會儲存該例外狀況,並將其封送處理至等候工作群組完成的內容。 與使用 cancel 方法時相同,執行階段會捨棄任何尚未啟動的工作,並且不會接受新的工作。

第三個範例與第二個類似,不同之處在於工作 t4 會擲回例外狀況,以取消工作群組 tg2。 這個範例會使用 try-catch 區塊,在工作群組 tg2 等候其子工作完成時檢查取消的狀態。 與第一個範例相同,這會使工作群組 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;
}

第四個範例會使用例外狀況處理,將整個工作樹狀結構取消。 此範例會在工作群組 tg1 等候其子工作完成時攔截例外狀況,而不會在工作群組 tg2 等候其子工作執行時進行攔截。 如同第二個範例,這會使樹狀結構中的兩個工作群組 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 方法會在子工作擲回例外狀況時擲回,因此您不會收到它們所傳回的值。

判斷工作何時取消

取消是一項合作式作業。 因此,它並不會立即執行。 在工作群組遭到取消時,執行階段中每個從子工作發出的呼叫將可觸發「中斷點」(Interruption Point),而使執行階段擲回和攔截內部例外狀況類型,以取消作用中的工作。 並行執行階段並未定義特定的中斷點;中斷點可透過執行階段中的任何呼叫來產生。 執行階段必須處理它所擲回的例外狀況,以執行取消作業。 因此,請不要處理工作主體中的未知例外狀況。

如果子工作正在執行耗時的作業,且未在執行階段中發出呼叫,則必須定期檢查取消的狀態,並適時地結束作業。 下列範例說明一種判斷工作於何時取消的方式。 工作 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 函式,以判斷父工作群組是否已取消。

回到頁首

取消平行演算法

PPL 中的平行演算法 (如 Concurrency::parallel_for) 建置於工作群組上。 因此,您可以使用許多相同的技術來取消平行演算法。

下列範例說明數種可取消平行演算法的方式。

下列範例會使用 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_group 類別

structured_task_group 類別

parallel_for 函式

變更記錄

日期

記錄

原因

2011 年 3 月

將另一個案例加入<不使用取消的時機>一節中。

資訊加強。