工作平行處理原則 (並行執行階段)

在並行執行階段中,「工作」(Task) 是執行特定工作 (Work) 的單位,通常與其他工作 (Task) 平行執行。 可以將工作分解為其他更精細且歸類為工作群組的工作。

在撰寫非同步程式碼並且想要讓某一個作業在非同步作業完成之後發生時,您可以使用工作。 例如,您可以使用工作以非同步方式讀取檔案,然後使用另一個工作,也就是「接續工作」(Continuation Task) 在資料提供使用之後處理資料 (接續工作將在本文件稍後進行說明)。 相反地,您可以使用工作群組將平行工作分解成較小部分。 例如,假設您有個將剩餘工作分成兩塊的遞迴演算法。 您可以使用工作群組同時執行這兩塊分割,然後等候分割的工作完成。

提示

當您要將相同的常式平行套用至集合的每個項目時,請使用平行演算法 (例如 Concurrency::parallel_for),而不使用工作或工作群組。如需平行演算法的詳細資訊,請參閱平行演算法

重點

  • 當您以傳址方式將變數傳遞給 Lambda 運算式時,必須保證該變數的存留期會保存直到工作完成為止。

  • 當您撰寫非同步程式碼時,使用工作 (concurrency::task 類別)。

  • 當您要將平行工作分解為較小的部分,然後等候這些較小的部分完成時,使用工作群組 (concurrency::task_group 類別或 concurrency::parallel_invoke 演算法)。

  • 使用 concurrency::task::then 方法建立接續。 接續是在另一個工作完成後非同步執行的工作。 您可以連接任意數目的接續,形成非同步工作鏈結。

  • 工作架構的接續一律排程在前項工作完成時執行,即使前項工作被取消或擲回例外狀況。

  • 使用 concurrency::when_all 建立工作,在工作集合中的每個成員都完成之後,這個工作才會完成。 使用 concurrency::when_any 建立工作,在工作集合中的一個成員完成之後,這個工作就完成了。

  • 工作和工作群組可以參與平行模式程式庫 (PPL) 取消機制。 如需詳細資訊,請參閱PPL 中的取消

  • 若要了解執行階段如何處理工作及工作群組所擲回的例外狀況,請參閱並行執行階段的例外狀況處理

本文內容

  • 使用 Lambda 運算式

  • 工作類別

  • 接續工作

  • 比較以值為基礎與以工作為基礎的接續

  • 組成工作

    • when_all 函式

    • when_any 函式

  • 延遲執行工作

  • 工作群組

  • 比較 task_group 與 structured_task_group

  • 範例

  • 健全的程式設計

使用 Lambda 運算式

由於它們的簡潔語法, Lambda 運算式常用於定義工作和工作群組所執行的工作。 以下提供一些使用技巧:

  • 由於工作通常會在背景執行緒執行,擷取 lambda 運算式中的變數時請注意物件存留期。 當您以傳值方式擷取變數時,該變數的複本會在 Lambda 主體內建立。 當您以傳址方式擷取時,不會建立複本。 因此,請確定您以傳址方式擷取之任何變數的存留期比使用它的工作更長。

  • 當您將 Lambda 運算式傳遞至工作時,不要擷取以傳址方式配置於堆疊上的變數。

  • 請確定您在 Lambda 運算式所擷取的變數,以便識別您要以值或以參考擷取。 因此,建議您針對 Lambda 運算式不要使用 [=] 或 [&] 選項。

一般模式是當接續鏈結中的工作指派給變數,而另一個工作則讀取該變數。 因為每個接續工作會保留變數的不同複本,您無法以傳值方式擷取。 如果是堆疊配置的變數,您同樣無法依參考擷取,因為變數可能不再有效。

若要解決這個問題,請使用智慧型指標,例如 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
*/

如需 Lambda 運算式的詳細資訊,請參閱 C++ 中的 Lambda 運算式

[上方]

工作類別

您可以使用 concurrency::task 類別將工作組成一組相依的作業。 這個組合模型受到接續概念支援。 上一個或前項工作完成時,接續可讓程式碼執行。 前項工作的結果會做為輸入傳遞至一個或多個接續工作。 當前項工作完成時,等候它的任何接續工作會排程執行。 每個接續工作都會收到前項工作的結果複本。 這些接續工作也可能依次為其他接續工作的前項工作,藉此建立工作的鏈結。 接續可幫助您建立彼此之間存在特定相依性的任意長度工作鏈結。 此外,工作也可以在工作開始之前,或是在其執行時以合作方式參與取消。 如需此取消模型的詳細資訊,請參閱PPL 中的取消

task 是樣板類別。 類型參數 T 是工作所產生之結果的類型。 如果工作不會傳回值,這個類型可以是 void。 T 無法使用 const 修飾詞。

當您建立工作時,會提供執行工作主體的工作函式。 這個工作函式採用 Lambda 函式、函式指標或函式物件的形式。 若要等候工作完成而不取得結果,請呼叫 concurrency::task::wait 方法。 task::wait 方法會傳回 concurrency::task_status 值,指出工作已完成或已取消。 若要取得工作的結果,請呼叫 concurrency::task::get 方法。 這個方法會呼叫 task::wait 等候工作完成,因此會封鎖目前執行緒的執行,直到有結果為止。

下列範例示範如何建立工作,等待其結果並顯示其值。 本文件的範例會使用 Lambda 函式,因為這些函式提供更為簡潔的語法。 不過,當您使用工作時,也可以使用函式指標或函式物件。

// 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;
});

如果在工作執行期間擲回例外狀況,執行階段會在後續對 task::gettask::wait 或是對工作接續的呼叫中,封送處理該例外狀況。 如需工作例外狀況處理機制的詳細資訊,請參閱並行執行階段的例外狀況處理

如需使用 taskconcurrency::task_completion_event、取消的範例,請參閱逐步解說:使用工作和 XML HTTP 要求連線。(本文件稍後會說明 task_completion_event 類別)。

提示

若要了解 Windows 市集應用程式的工作特有的詳細資訊,請參閱 Asynchronous programming in C++使用 C++ 為 Windows 市集應用程式建立非同步作業

[上方]

接續工作

在非同步程式設計中,一個非同步作業在完成時叫用第二個作業,並將資料傳遞給該作業,是很常見的情形。 傳統上是以回呼方法執行這些動作。 在並行執行階段中,「接續工作」(Continuation Task) 提供相同的功能。 接續工作 (也簡稱為「接續」) 是指在前項完成時,由另一項工作 (稱為「前項」(Antecedent)) 所叫用的非同步工作。 藉由使用接續,您可以:

  • 將資料從前項傳遞至接續。

  • 精確指定是否要叫用接續的條件。

  • 在接續啟動之前加以取消,或在它執行時以合作方式加以取消。

  • 提供有關如何排程接續的提示。(這只適用於 Windows 市集 應用程式。 如需詳細資訊,請參閱 使用 C++ 為 Windows 市集應用程式建立非同步作業)。

  • 從相同的前項叫用多個接續。

  • 在多個前項全部或有任何一個完成時,叫用一個接續。

  • 將接續逐一鏈結成任何長度。

  • 使用接續處理由前項擲回的例外狀況。

這些功能可讓您在第一個工作完成時執行一項或多項工作。 例如,您可以建立在第一個工作從磁碟讀取檔案之後,壓縮該檔案的接續。

下列範例修改前一個範例,以使用 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 的巢狀工作時,產生的工作具有 N 類型 (而不是 task<N>),而且在巢狀工作完成時就完成了。換句話說,接續會執行巢狀工作的解除包裝動作。

[上方]

比較以值為基礎與以工作為基礎的接續

假設某個 task 物件的傳回類型為 T,您可以提供對齊接續工作提供 Ttask<T> 類型的值。 類型為 T 的接續稱為 數值接續。 排程在前項工作在沒有發生錯誤的情況下完成且未被取消時執行值架構的接續。 類型與其參數一樣為 task<T> 的接續稱為 工作接續。 工作架構的接續一律排程在前項工作完成時執行,即使前項工作被取消或擲回例外狀況。 然後您可以呼叫 task::get 取得前項工作的結果。 如果前項工作取消,task::get 會擲回 concurrency::task_canceled。 如果前項工作擲回例外狀況,task::get 會重新擲回該例外狀況。 在接續的前項工作取消時,工作架構的接續未標記為已取消。

[上方]

組成工作

本節說明 concurrency::when_allconcurrency::when_any 函式,可協助您撰寫多個工作來實作一般模式。

when_all 函式

when_all 函式產生的工作,在一組工作完成之後完成。 這個函式會傳回 std::vector 物件,包含工作集合中每個工作的結果。 下列基本範例使用 when_all 建立表示其他三個工作完成的工作。

// 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>> 以產生工作的接續。

如果在一組工作中有任何工作取消或擲回例外狀況,when_all 會立即完成且不會等待剩餘工作完成。 如果擲回例外狀況,當您在 when_all 傳回的工作物件上呼叫 task::gettask::wait 時,執行階段就會重新擲回例外狀況。 如果擲出多項工作,執行階段會選擇其中一項。 因此,請確定您觀察在所有工作完成後發生的所有例外狀況;未處理的例外狀況會造成應用程式終止。

這裡提供的公用程式函式可用來確保您的程式會觀察所有例外狀況。 針對所提供範圍內的每個工作,observe_all_exceptions 會觸發重新擲回所發生的任何例外狀況,然後納入該例外狀況。

// 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 建立會在其他三個工作的第一個完成時完成的工作。

// 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 完成事件。

完成這項工作有兩種方式:使用接續,或者啟動工作並在工作的 work 函式中等候事件。 不過,有些情況下無法使用這些技術的其中一種。 例如,若要建立接續,您必須擁有前項工作。 不過,如果您沒有前項工作,則可以建立「工作完成事件」(Task Completion Event),並於之後有可用的前項工作時,將該完成事件鏈結至前項工作。 此外,因為等候的工作也會封鎖執行緒,您可以在非同步作業完成時使用工作完成事件執行工作,因而釋放執行緒。

concurrency::task_completion_event 類別有助於簡化工作的這種組合。 與 task 類別相同,類型參數 T 是由工作產生之結果的類型。 如果工作不會傳回值,這個類型可以是 void。 T 無法使用 const 修飾詞。 一般來說,task_completion_event 物件提供給執行緒或工作,當值可用時通知它。 同時會將一個或多個工作設定為該事件的接聽程式。 當事件設定時,接聽程式的工作完成而且其接續排定執行。

如需使用 task_completion_event 實作延遲後完成之工作的範例,請參閱如何:建立在延遲之後才會完成的工作

[上方]

工作群組

「工作群組」(Task Group) 會組織工作集合。 工作群組會將工作推入至工作竊取佇列。 排程器會將工作從這個佇列中移除,並使用可用的運算資源執行這些工作。 將工作加入至工作群組之後,您可以等候所有工作完成或取消尚未開始的工作。

PPL 以 concurrency::task_groupconcurrency::structured_task_group 類別表示工作群組,並以 concurrency::task_handle 類別表示這些群組中執行的工作。 task_handle 類別會封裝執行工作的程式碼。 與 task 類別相同,工作函式採用 Lambda 函式、函式指標或函式物件的形式。 您通常不必直接使用 task_handle 物件, 而是將工作函式傳遞給工作群組,而工作群組就會建立及管理 task_handle 物件。

PPL 將工作群組分為兩類:「非結構化工作群組」(Unstructured Task Group) 和「結構化工作群組」(Structured Task Group)。 PPL 以 task_group 類別表示非結構化工作群組,並以 structured_task_group 類別表示結構化工作群組。

重要

PPL 也會定義 concurrency::parallel_invoke 演算法,這個演算法使用 structured_task_group 類別,以平行方式執行一組工作。因為 parallel_invoke 演算法的語法比較簡潔,建議您盡可能使用它,而不要使用 structured_task_group 類別。平行演算法主題將更詳細說明 parallel_invoke

當您擁有數個要同時執行的獨立工作,而且必須等候所有工作完成才能繼續時,使用 parallel_invoke。 這個技術通常稱為分岔和聯結平行處理原則。 當您擁有數個要同時執行的獨立工作,但是要等候工作稍後完成時,使用 task_group。 例如,您可以將工作加入至 task_group 物件,並在另一個函式中或從另一個執行緒等候這些工作完成。

工作群組支援取消的概念。 取消可讓您對所有使用中工作表示您要取消整體作業。 取消也可避免啟動尚未啟動的工作。 如需取消的詳細資訊,請參閱 PPL 中的取消

執行階段也提供例外處理模型,可讓您從工作擲回例外狀況,並在等候相關工作群組完成時處理該例外狀況。 如需此例外狀況處理模型的詳細資訊,請參閱並行執行階段的例外狀況處理

[上方]

比較 task_group 與 structured_task_group

雖然我們建議您使用 task_groupparallel_invoke,而不使用 structured_task_group 類別,但在某些情況下,您會想要使用 structured_task_group,例如當您撰寫可執行可變數目的工作或需要支援取消的平行演算法時。 本節說明 task_groupstructured_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::waitconcurrency::task_group::run_and_wait 方法之後,您還可以在 task_group 物件上執行其他工作。 相反地,如果您在呼叫 concurrency::structured_task_group::waitconcurrency::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 物件。 每個 task_handle 物件在其相關 structured_task_group 物件的整個存留期都必須維持有效。 使用 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();
}

下列是這個範例 (Example) 的範例 (Sample) 輸出:

  

因為 parallel_invoke 演算法會並行執行工作,所以輸出訊息的順序可能會不同。

如需示範如何使用 parallel_invoke 演算法的完整範例,請參閱 如何:使用 parallel_invoke 來撰寫平行排序常式如何:使用 parallel_invoke 執行平行作業。 如需使用 task_group 類別來實作非同步未來的完整範例,請參閱逐步解說:實作未來

[上方]

健全的程式設計

在使用工作、工作群組和平行演算法時,請確定您了解取消和例外狀況處理的角色。 例如,在平行工作的樹狀結構中,已取消的工作會導致子工作無法執行。 如果其中一個子工作所執行的作業對應用程式很重要,例如釋放資源,這可能會造成問題。 此外,如果子工作擲回例外狀況,該例外狀況可能會透過物件解構函式傳播,而在應用程式中造成未定義的行為。 如需說明這些重點的範例,請參閱<平行模式程式庫中的最佳作法>文件的了解取消和例外狀況處理對物件解構的影響一節。 如需 PPL 中取消和例外狀況處理模型的詳細資訊,請參閱 PPL 中的取消並行執行階段的例外狀況處理

[上方]

相關主題

標題

描述

如何:使用 parallel_invoke 來撰寫平行排序常式

顯示如何使用 parallel_invoke 演算法改善雙調排序演算法的效能。

如何:使用 parallel_invoke 執行平行作業

顯示如何使用 parallel_invoke 演算法,改善對共用資料來源執行多個作業的程式效能。

如何:建立在延遲之後才會完成的工作

示範如何使用 taskcancellation_token_sourcecancellation_tokentask_completion_event 類別來建立延遲之後完成的工作。

逐步解說:實作未來

顯示如何將並行執行階段中的現有功能合併成可完成更多工作的單一項目。

平行模式程式庫 (PPL)

說明 PPL,PPL 提供開發並行應用程式時的重要程式設計模型。

參考資料

task 類別 (並行執行階段)

task_completion_event 類別

when_all 函式

when_any 函式

task_group 類別

parallel_invoke 函式

structured_task_group 類別