並行執行階段中的一般最佳作法

本文件說明並行執行階段多個方面適用的最佳作法。

章節

本文件包括下列章節:

  • 盡可能使用合作式同步處理建構

  • 避免使用不讓渡的冗長工作

  • 使用過度訂閱使封鎖或有高延遲的作業位移

  • 盡可能使用並行記憶體管理函式

  • 使用 RAII 管理並行物件的存留期

  • 不要在全域範圍建立並行物件

  • 不要在共用資料區段中使用並行物件

盡可能使用合作式同步處理建構

並行執行階段提供許多不需要外部同步處理物件的並行安全建構。例如, concurrency::concurrent_vector 類別會提供並行存取之附加和存取作業的項目。不過,對於需要資源的獨佔存取的情況下,執行階段提供 concurrency::critical_sectionconcurrency::reader_writer_lock,以及 concurrency::event 類別。這些型別是以合作方式運作,因此工作排程器可以在第一個工作等候資料時,將處理資源重新配置給另一個內容。盡可能使用這些同步處理型別,而不要使用例如 Windows API 所提供、不會以合作方式運作的其他同步處理機制。如需這些同步處理型別的詳細資訊和程式碼範例,請參閱同步處理資料結構比較同步處理資料結構與 Windows API

Top

避免使用不讓渡的冗長工作

因為工作排程器是以合作方式運作,它不會在工作之間提供公平性。因此,某個工作可能會防止其他工作開始。雖然在某些案例中,這是可以接受的,但在其他案例中可能就會造成死結或耗盡。

下列範例所執行的工作多於配置的處理資源數目。第一個工作不會讓給工作排程器,因此在第一個工作完成之前,第二個工作不會開始。

// cooperative-tasks.cpp
// compile with: /EHsc
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

// Data that the application passes to lightweight tasks.
struct task_data_t
{
   int id;  // a unique task identifier.
   event e; // signals that the task has finished.
};

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

int wmain()
{
   // For illustration, limit the number of concurrent 
   // tasks to one.
   Scheduler::SetDefaultSchedulerPolicy(SchedulerPolicy(2, 
      MinConcurrency, 1, MaxConcurrency, 1));

   // Schedule two tasks.

   task_data_t t1;
   t1.id = 0;
   CurrentScheduler::ScheduleTask(task, &t1);

   task_data_t t2;
   t2.id = 1;
   CurrentScheduler::ScheduleTask(task, &t2);

   // Wait for the tasks to finish.

   t1.e.wait();
   t2.e.wait();
}

這個範例會產生下列輸出:

1: 250000000
1: 500000000
1: 750000000
1: 1000000000
2: 250000000
2: 500000000
2: 750000000
2: 1000000000

有數個方式可在兩個工作之間啟用合作。一個方式是在長時間執行的工作中偶爾讓給工作排程器。下列範例會修改task函式呼叫 concurrency::Context::Yield 方法以產生工作排程器的執行,另一項工作可以執行。

// A lightweight task that performs a lengthy operation.
void task(void* data)
{   
   task_data_t* task_data = reinterpret_cast<task_data_t*>(data);

   // Create a large loop that occasionally prints a value to the console.
   int i;
   for (i = 0; i < 1000000000; ++i)
   {
      if (i > 0 && (i % 250000000) == 0)
      {
         wstringstream ss;
         ss << task_data->id << L": " << i << endl;
         wcout << ss.str();

         // Yield control back to the task scheduler.
         Context::Yield();
      }
   }
   wstringstream ss;
   ss << task_data->id << L": " << i << endl;
   wcout << ss.str();

   // Signal to the caller that the thread is finished.
   task_data->e.set();
}

這個範例會產生下列輸出:

1: 250000000
2: 250000000
1: 500000000
2: 500000000
1: 750000000
2: 750000000
1: 1000000000
2: 1000000000

Context::Yield 方法只會讓給目前執行緒所屬的排程器上的另一個使用中執行緒、輕量型工作或另一個作業系統執行緒。這個方法不會產生工作排定為執行的 concurrency::task_groupconcurrency::structured_task_group 物件,但尚未啟動。

有其他方式可在長時間執行的工作之間啟用合作。您可以將大型工作細分為較小的子任務。您也可以在冗長工作期間啟用過度訂閱。過度訂閱可讓您建立比可用硬體執行緒數目更多的執行緒。當冗長工作的延遲性較高 (例如從磁碟或網路連接讀取資料),過度訂閱特別實用。如需輕量型工作和過度訂閱的詳細資訊,請參閱工作排程器 (並行執行階段)

Top

使用過度訂閱使封鎖或有高延遲的作業位移

並行執行階段提供同步處理原始物件,例如 concurrency::critical_section,能讓共同合作地封鎖,並產生彼此的工作。當某個工作以合作方式封鎖或讓渡時,工作排程器可以在第一個工作等候資料時,將處理資源重新配置給另一個內容。

在某些案例中,您無法使用並行執行階段所提供的合作式封鎖機制。例如,您所使用的外部程式庫可能使用不同的同步處理機制。另一個例子是當您執行延遲性較高的作業,例如使用 Windows API ReadFile 函式,從網路連接讀取資料時。在這些案例中,過度訂閱可以在某個工作閒置時讓其他工作執行。過度訂閱可讓您建立比可用硬體執行緒數目更多的執行緒。

考慮下列 download 函式下載位於給定 URL 的檔案。這個範例會使用 concurrency::Context::Oversubscribe 方法來暫時增加的執行緒數目。

// Downloads the file at the given URL.
string download(const string& url)
{
   // Enable oversubscription.
   Context::Oversubscribe(true);

   // Download the file.
   string content = GetHttpFile(_session, url.c_str());

   // Disable oversubscription.
   Context::Oversubscribe(false);

   return content;
}

因為 GetHttpFile 函式執行潛在延遲的作業,過度訂閱可以在目前工作等候資料時,讓其他工作執行。如需此範例的完整版本,請參閱 HOW TO:使用過度訂閱使延遲產生位移

Top

盡可能使用並行記憶體管理函式

使用記憶體的管理功能, concurrency::Allocconcurrency::Free,當您有經常配置小型物件擁有相對較短的存留期的微調工作。並行執行階段會針對每個執行中的執行緒保存不同的記憶體快取。AllocFree 函式會在這些快取中配置及釋放記憶體,而不使用鎖定或記憶體屏障。

如需這些記憶體管理函式的詳細資訊,請參閱工作排程器 (並行執行階段)。如需使用這些函式的範例,請參閱 HOW TO:使用 Alloc 和 Free 改善記憶體效能

Top

使用 RAII 管理並行物件的存留期

並行執行階段使用例外狀況處理來實作取消等功能。因此,當您呼叫執行階段或呼叫另一個會呼叫執行階段的程式庫時,請撰寫無例外狀況之虞的程式碼。

「資源擷取為初始設定」(Resource Acquisition Is Initialization,RAII) 模式是在給定範圍下安全管理並行物件存留期的一種方式。在 RAII 模式下,資料結構會配置於堆疊上。該資料結構會在建立時初始化或擷取資源,並在資料結構終結時終結或釋放該資源。RAII 模式可保證在封閉範圍結束之前呼叫解構函式。當函式包含多個 return 陳述式時,這種模式很實用。這個模式也有助於您撰寫無例外狀況之虞的程式碼。當 throw 陳述式導致堆疊回溯時,就會呼叫 RAII 物件的解構函式,因此一定會正確刪除或釋放資源。

執行階段會定義幾個類別使用 RAII 模式,例如, concurrency::critical_section::scoped_lockconcurrency::reader_writer_lock::scoped_lock。這些 Helper 類別稱為「範圍鎖定」(Scoped Lock)。這些類別提供數個好處,當您使用 concurrency::critical_sectionconcurrency::reader_writer_lock 物件。這些類別的建構函式會取得對所提供 critical_sectionreader_writer_lock 物件的存取,而解構函式則會釋放對該物件的存取。因為範圍鎖定會在終結時自動釋放其互斥物件的存取權,所以您不會以手動方式解除鎖定基礎物件。

請考慮下列類別 account,這是由外部程式庫所定義,因此無法修改。

// account.h
#pragma once
#include <exception>
#include <sstream>

// Represents a bank account.
class account
{
public:
   explicit account(int initial_balance = 0)
      : _balance(initial_balance)
   {
   }

   // Retrieves the current balance.
   int balance() const
   {
      return _balance;
   }

   // Deposits the specified amount into the account.
   int deposit(int amount)
   {
      _balance += amount;
      return _balance;
   }

   // Withdraws the specified amount from the account.
   int withdraw(int amount)
   {
      if (_balance < 0)
      {
         std::stringstream ss;
         ss << "negative balance: " << _balance << std::endl;
         throw std::exception((ss.str().c_str()));
      }

      _balance -= amount;
      return _balance;
   }

private:
   // The current balance.
   int _balance;
};

下列範例會在 account 物件上平行執行多個交易。範例使用 critical_section 物件,以同步處理對 account 物件的存取,因為 account 類別不是並行安全的。每個平行作業都會使用 critical_section::scoped_lock 物件,以確保 critical_section 物件會在作業成功或失敗時解除鎖定。當帳戶餘額為負數時,withdraw 作業會藉由擲回例外狀況而失敗。

// account-transactions.cpp
// compile with: /EHsc
#include "account.h"
#include <ppl.h>
#include <iostream>
#include <sstream>

using namespace concurrency;
using namespace std;

int wmain()
{
   // Create an account that has an initial balance of 1924.
   account acc(1924);

   // Synchronizes access to the account object because the account class is 
   // not concurrency-safe.
   critical_section cs;

   // Perform multiple transactions on the account in parallel.   
   try
   {
      parallel_invoke(
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before deposit: " << acc.balance() << endl;
            acc.deposit(1000);
            wcout << L"Balance after deposit: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(50);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         },
         [&acc, &cs] {
            critical_section::scoped_lock lock(cs);
            wcout << L"Balance before withdrawal: " << acc.balance() << endl;
            acc.withdraw(3000);
            wcout << L"Balance after withdrawal: " << acc.balance() << endl;
         }
      );
   }
   catch (const exception& e)
   {
      wcout << L"Error details:" << endl << L"\t" << e.what() << endl;
   }
}

這個範例 (Example) 會產生下列範例 (Sample) 輸出:

Balance before deposit: 1924
Balance after deposit: 2924
Balance before withdrawal: 2924
Balance after withdrawal: -76
Balance before withdrawal: -76
Error details:
        negative balance: -76

如需使用 RAII 模式管理並行物件存留期的其他範例,請參閱逐步解說:從使用者介面執行緒中移除工作HOW TO:使用內容類別實作合作式信號HOW TO:使用過度訂閱使延遲產生位移

Top

不要在全域範圍建立並行物件

當您建立在全域範圍的並行存取物件時您可能會造成死結或記憶體這類的問題在您的應用程式中發生存取違規。

例如,當您建立並行執行階段物件,執行階段會建立為您的預設排程器如果其中一個還未建立。全域物件建構期間建立的執行階段物件據以會使執行階段建立這個預設排程器。不過,這項程序會將內部的鎖定,可能會干擾其他支援的並行執行階段基礎結構之物件的初始設定。這個內部鎖定必須遵守還尚未初始化,而且可能會因此導致發生鎖死應用程式中的另一個基礎結構物件。

下列範例示範如何建立全域 concurrency::Scheduler 物件。這種模式不只適用於Scheduler類別,但是所有其他並行執行階段所提供的型別。我們建議您沒有遵循此模式,因為會在應用程式中導致無法預期的行為。

// global-scheduler.cpp
// compile with: /EHsc
#include <concrt.h>

using namespace concurrency;

static_assert(false, "This example illustrates a non-recommended practice.");

// Create a Scheduler object at global scope.
// BUG: This practice is not recommended because it can cause deadlock.
Scheduler* globalScheduler = Scheduler::Create(SchedulerPolicy(2,
   MinConcurrency, 2, MaxConcurrency, 4));

int wmain() 
{   
}

如需如何正確建立 Scheduler 物件的範例,請參閱工作排程器 (並行執行階段)

Top

不要在共用資料區段中使用並行物件

並行執行階段不支援並行物件用於共用資料區段,例如 data_seg#pragma 指示詞所建立的資料區段。跨處理序界限共用的並行物件可能會導致執行階段處於不一致或無效狀態。

Top

請參閱

工作

HOW TO:使用 Alloc 和 Free 改善記憶體效能

HOW TO:使用過度訂閱使延遲產生位移

HOW TO:使用內容類別實作合作式信號

逐步解說:從使用者介面執行緒中移除工作

概念

平行模式程式庫 (PPL)

非同步代理程式程式庫

工作排程器 (並行執行階段)

同步處理資料結構

比較同步處理資料結構與 Windows API

平行模式程式庫中的最佳作法

非同步代理程式程式庫中的最佳作法

其他資源

並行執行階段最佳作法