PPL 中的取消

本文件說明取消在平行模式程式庫 (PPL) 中的角色、如何取消平行工作,以及如何判斷平行工作取消的時間。

注意

執行階段使用例外狀況處理來實作取消。 請勿在您的程式碼中攔截或處理這些例外狀況。 此外,我們建議您在工作的函式主體中撰寫例外狀況安全的程式碼。 例如,您可以使用資源 擷取 為初始化 (RAII) 模式,以確保在工作主體中擲回例外狀況時,正確地處理資源。 如需使用 RAII 模式清除可取消工作中資源的完整範例,請參閱 逐步解說:從使用者介面執行緒 移除工作。

重點

本文內容

平行工作樹狀結構

PPL 使用工作和工作群組來管理細部的工作和計算。 您可以巢狀工作組來形成 平行工作的樹狀結構 。 下圖顯示平行工作樹狀。 在此圖中,tg1tg2 代表工作群組;t1t2t3t4t5 代表工作群組執行的工作。

A parallel work tree.

下列範例顯示在圖中建立樹狀結構時需要的程式碼。 在此範例中, tg1tg2 並行::structured_task_group 物件; t1t2t3t4t5 並行::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 類別來建立類似的工作樹狀結構。 並行 ::task 類別也支援工作樹狀結構的概念。 不過,task 樹狀是相依性樹狀。 在 task 樹狀結構中,未來工作會在目前工作之後完成。 在工作群組樹狀中,內部工作會在外部工作之前完成。 如需工作和工作組之間差異的詳細資訊,請參閱 工作平行處理原則

[靠上]

取消平行工作

有多種方式可取消平行工作。 慣用的方法是使用取消語彙基元。 工作組也支援 並行::task_group::cancel 方法和 並行::structured_task_group::cancel 方法。 最後一種方式是在工作的工作函式主體中擲回例外狀況。 無論您選擇哪一個方法,請了解取消不會立即發生。 雖然取消工作或工作組時不會啟動新的工作,但作用中工作必須檢查並回應取消。

如需取消平行工作的更多範例,請參閱 逐步解說:連線使用工作和 XML HTTP 要求 如何:使用取消來中斷平行迴圈 ,以及如何 :使用例外狀況處理從平行迴圈 中斷。

使用取消權杖取消平行工作

tasktask_groupstructured_task_group 類別可透過使用取消語彙基元來支援取消。 PPL 會 為此定義並行::cancellation_token_source 並行::cancellation_token 類別。 當您使用取消語彙基元取消工作時,執行階段就不會啟動訂閱這個語彙基元的新工作。 已作用中的工作可以使用 is_canceled 成員函式來監視取消權杖,並在可以時停止。

若要起始取消,請呼叫 concurrency::cancellation_token_source::cancel 方法。 您回應取消的方式如下:

  • 針對 task 物件,請使用 並行::cancel_current_task 函式。 cancel_current_task 會取消目前的工作及任何其以值為基礎的接續。 (它不會取消與工作或其接續相關聯的取消 標記

  • 針對工作組和平行演算法,請使用 並行::is_current_task_group_canceling 函式偵測取消,並在此函式傳回時,儘快從工作主體傳回 true 。 (請勿從工作群組呼叫 cancel_current_task。)

下列範例顯示工作取消的第一個基本模式。 工作主體偶爾會在迴圈內檢查取消。

// task-basic-cancellation.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <concrt.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

bool do_work()
{
    // Simulate work.
    wcout << L"Performing work..." << endl;
    wait(250);
    return true;
}

int wmain()
{
    cancellation_token_source cts;
    auto token = cts.get_token();

    wcout << L"Creating task..." << endl;

    // Create a task that performs work until it is canceled.
    auto t = create_task([&]
    {
        bool moreToDo = true;
        while (moreToDo)
        {
            // Check for cancellation.
            if (token.is_canceled())
            {
                // TODO: Perform any necessary cleanup here...

                // Cancel the current task.
                cancel_current_task();
            }
            else 
            {
                // Perform work.
                moreToDo = do_work();
            }
        }
    }, token);

    // Wait for one second and then cancel the task.
    wait(1000);

    wcout << L"Canceling task..." << endl;
    cts.cancel();

    // Wait for the task to cancel.
    wcout << L"Waiting for task to complete..." << endl;
    t.wait();

    wcout << L"Done." << endl;
}

/* Sample output:
    Creating task...
    Performing work...
    Performing work...
    Performing work...
    Performing work...
    Canceling task...
    Waiting for task to complete...
    Done.
*/

cancel_current_task 函式會擲回;因此,您不需要明確地從目前的迴圈或函式傳回。

提示

或者,您可以呼叫 concurrency::interruption_point 函式,而不是 cancel_current_task

當您回應取消時務必要呼叫 cancel_current_task,因為它會將工作轉換成已取消的狀態。 如果您提早傳回,而不是呼叫 cancel_current_task,則作業會轉換成已完成狀態,並且會執行任何以值為基礎的接續。

警告

永遠不會從您的程式碼擲回 task_canceled。 請改為呼叫 cancel_current_task

當工作以取消狀態結束時 ,並行::task::get 方法會 擲回並行::task_canceled 。 (相反地, 並行::task::wait 傳回task_status::canceled 且不會擲回。下列範例說明工作型接續的這個行為。 一律會呼叫以工作為基礎的接續,即使前項工作已取消亦然。

// task-canceled.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    auto t1 = create_task([]() -> int
    {
        // Cancel the task.
        cancel_current_task();
    });

    // Create a continuation that retrieves the value from the previous.
    auto t2 = t1.then([](task<int> t)
    {
        try
        {
            int n = t.get();
            wcout << L"The previous task returned " << n << L'.' << endl;
        }
        catch (const task_canceled& e)
        {
            wcout << L"The previous task was canceled." << endl;
        }
    });

    // Wait for all tasks to complete.
    t2.wait();
}
/* Output:
    The previous task was canceled.
*/

由於以值為基礎的接續會繼承其前項工作的語彙基元 (除非接續是以明確的語彙基元建立),因此即使前項工作仍在執行中,接續也會立即進入已取消的狀態。 因此,在取消之後,前項工作所擲回的任何例外狀況都不會散佈到接續工作。 取消一律會覆寫前項工作的狀態。 下列範例類似於上一個範例,但說明了以值為基礎的接續的行為。

auto t1 = create_task([]() -> int
{
    // Cancel the task.
    cancel_current_task();
});

// Create a continuation that retrieves the value from the previous.
auto t2 = t1.then([](int n)
{
    wcout << L"The previous task returned " << n << L'.' << endl;
});

try
{
    // Wait for all tasks to complete.
    t2.get();
}
catch (const task_canceled& e)
{
    wcout << L"The task was canceled." << endl;
}
/* Output:
    The task was canceled.
*/

警告

如果您未將取消權杖傳遞至建 task 構函式或 並行::create_task 函式,則無法取消該工作。 此外,您必須傳遞相同的取消語彙基元給任何巢狀工作的建構函式 (也就是在另一個工作主體內建立的工作),以便同時取消所有工作。

您可能會想要在取消取消語彙基元時,執行任意程式碼。 例如,如果使用者在使用者介面上選擇 [ 取消 ] 按鈕來取消作業,您可以停用該按鈕,直到使用者啟動其他作業為止。 下列範例示範如何使用 concurrency::cancellation_token::register_callback 方法來註冊取消取消權杖時執行的回呼函式。

// task-cancellation-callback.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>

using namespace concurrency;
using namespace std;

int wmain()
{
    cancellation_token_source cts;
    auto token = cts.get_token();

    // An event that is set in the cancellation callback.
    event e;

    cancellation_token_registration cookie;
    cookie = token.register_callback([&e, token, &cookie]()
    {
        wcout << L"In cancellation callback..." << endl;
        e.set();

        // Although not required, demonstrate how to unregister 
        // the callback.
        token.deregister_callback(cookie);
    });

    wcout << L"Creating task..." << endl;

    // Create a task that waits to be canceled.
    auto t = create_task([&e]
    {
        e.wait();
    }, token);

    // Cancel the task.
    wcout << L"Canceling task..." << endl;
    cts.cancel();

    // Wait for the task to cancel.
    t.wait();

    wcout << L"Done." << endl;
}
/* Sample output:
    Creating task...
    Canceling task...
    In cancellation callback...
    Done.
*/

檔「工作平行處理原則 」說明以值為基礎的接續與工作型接續之間的差異。 如果您未提供 cancellation_token 物件給接續工作,接續會以下列方式繼承前項工作的取消語彙基元:

  • 以值為基礎的接續一律會繼承前項工作的取消語彙基元。

  • 以工作為基礎的接續絕不會繼承前項工作的取消語彙基元。 明確地傳遞取消語彙基元是使得以工作為基礎的接續可取消的唯一方式。

這些行為不會受到錯誤工作 (也就是擲回例外狀況的工作) 影響。 在此情況下,會取消以值為基礎的接續;工作型接續不會取消。

警告

在另一項工作中建立的工作 (也就是巢狀工作) 不會繼承父工作的取消語彙基元。 只有以值為基礎的接續才會繼承其前項工作的取消語彙基元。

您也可以提供取消語彙基元給 task_groupstructured_task_group 物件的建構函式。 其中的一個重要環節是子工作群組會繼承這個取消語彙基元。 如需示範此概念的範例,請使用 並行::run_with_cancellation_token 函式執行來呼叫 parallel_for ,請參閱 本檔稍後的取消平行演算法

[靠上]

取消語彙基元和工作組合

並行::when_all 並行::when_any 函式可協助您撰寫多個工作來實作一般模式。 本章節描述這些函式如何使用取消語彙基元。

當您為 和 when_any 函式提供取消權杖時,該函式只會在取消該取消權杖 when_all 時取消,或當其中一個參與者工作以取消狀態結束或擲回例外狀況時才會取消。

當您未提供取消語彙基元給 when_all 函式時,它會從組合整體作業之每個工作繼承取消語彙基元。 當取消任何這些權杖,且至少有一個參與者工作尚未啟動或正在執行時,會取消從 傳回 when_all 的工作。 當其中一個工作擲回例外狀況時,就會發生類似的行為- 從 when_all 傳回的工作會立即取消該例外狀況。

when_any 函式傳回的工作完成時,執行階段會選擇該工作的取消語彙基元。 如果參與者工作都未在已完成狀態中完成,而且一個或多個工作擲回例外狀況,則其中一項擲回的工作會被選來完成 when_any,其語彙基元則會被選為最終工作的語彙基元。 如果多個工作在已完成狀態中完成,從 when_any 工作傳回的工作便會在已完成狀態中結束。 執行階段會嘗試挑選已完成的工作,該工作的語彙基元在完成時不是已取消狀態,因此即使其他正在執行的工作可能會在稍後才完成,從 when_any 傳回的工作也不會立即取消。

[靠上]

使用 cancel 方法取消平行工作

concurrency ::task_group::cancel concurrency::structured_task_group::cancel 方法會將工作組設定為已取消的狀態。 在您呼叫 cancel 之後,工作群組不會啟動未來工作。 有多個子工作可以呼叫 cancel 方法。 已取消的工作會導致 平行存取::task_group::wait concurrency::structured_task_group::wait 方法傳回 並行::canceled

如果取消工作組,從每個子工作到執行時間的呼叫可能會觸發 中中斷點 ,這會導致執行時間擲回並攔截內部例外狀況類型來取消作用中工作。 並行執行階段不會定義特定的中斷點;中斷點可以發生在對執行階段的任何呼叫。 執行階段必須處理其所擲回的例外狀況,才能執行取消。 因此,請不要在工作的主體中處理未知例外狀況。

如果子工作會執行耗時的作業,而且不會呼叫執行階段,則其必須定期檢查取消,並及時結束。 下列範例顯示判斷工作何時取消的一個方法。 工作 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 函式來判斷父工作組是否已取消。

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::cancel concurrency::structured_task_group::is_canceling 方法。 子工作可以呼叫這些方法來取消父工作群組,並檢查取消。

警告

雖然您可以使用取消語彙基元來取消工作群組所執行的工作,且此工作群組是執行作為 task 物件的子系,但您不能使用 task_group::cancelstructured_task_group::cancel 方法來取消在工作群組中執行的 task 物件。

[靠上]

使用例外狀況取消平行工作

在取消平行工作樹狀時,使用取消語彙基元和 cancel 方法會比例外狀況處理更有效率。 取消語彙基元和 cancel 方法使用由上而下的方式取消工作以及任何子工作。 相反地,例外狀況處理則使用由下而上的方式執行,且必須在例外狀況往上傳播時個別取消每一個子工作群組。 例外狀況處理 主題 說明並行執行時間如何使用例外狀況來傳達錯誤。 不過,並非所有例外狀況都表示發生錯誤。 比方說,搜尋演算法在找到結果時,可能會取消其相關聯的工作。 不過,如先前所述,例外狀況處理的效率會比使用 cancel 方法來取消平行工作來得低。

警告

我們建議您只在必要時使用例外狀況取消平行工作。 取消語彙基元和工作群組 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 方法會在子工作擲回例外狀況時擲回,所以您不會收到其傳回值。

[靠上]

取消平行演算法

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

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

下列範例使用 run_with_cancellation_token 函式來呼叫 parallel_for 演算法。 run_with_cancellation_token 函式採用取消語彙基元作為引數,並以同步方式呼叫所提供的工作函式。 由於平行演算法是建置在工作上,所以其會繼承父工作的取消語彙基元。 因此,parallel_for 可以回應取消。

// cancel-parallel-for.cpp
// compile with: /EHsc
#include <ppltasks.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
    // Call parallel_for in the context of a cancellation token.
    cancellation_token_source cts;
    run_with_cancellation_token([&cts]() 
    {
        // Print values to the console in parallel.
        parallel_for(0, 20, [&cts](int n)
        {
            // For demonstration, cancel the overall operation 
            // when n equals 11.
            if (n == 11)
            {
                cts.cancel();
            }
            // Otherwise, print the value.
            else
            {
                wstringstream ss;
                ss << n << endl;
                wcout << ss.str();
            }
        });
    }, cts.get_token());
}
/* Sample output:
    15
    16
    17
    10
    0
    18
    5
*/

下列範例會使用 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 類別來撰寫基本樹狀的搜尋演算法。
例外狀況處理 描述執行階段如何處理工作群組、輕量型工作和非同步代理程式所擲回的例外狀況,以及如何回應您的應用程式中的例外狀況。
工作平行處理原則 描述工作如何與工作群組產生關聯,以及如何在應用程式中使用非結構化和結構化工作。
平行演算法 描述平行演算法,這會同時對資料集合執行工作。
平行模式程式庫 (PPL) 提供平行模式程式庫的總覽。

參考

task 類別 (並行執行階段)

cancellation_token_source 類別

cancellation_token 類別

task_group 類別

structured_task_group 類別

parallel_for函式