並行執行階段中的一般最佳作法
本文件說明適用於並行運行時間多個區域的最佳做法。
區段
本文件包含下列章節:
盡可能使用合作式同步處理建構
並行運行時間提供許多不需要外部同步處理物件的並行安全建構。 例如,並行 ::concurrent_vector 類別會提供並行安全附加和元素存取作業。 在這裡,並行安全表示指標或反覆運算器一律有效。 這不是元素初始化或特定周遊順序的保證。 不過,針對需要獨佔存取資源的情況,運行時間會提供並行存取::critical_section、並行::reader_writer_lock和並行::事件類別。 這些類型會合作運作;因此,當第一個工作等候數據時,工作排程器可以將處理資源重新配置至另一個內容。 可能的話,請使用這些同步處理類型,而不是其他同步處理機制,例如 Windows API 所提供的同步處理類型,這些類型不會合作運作。 如需這些同步處理類型和程式碼範例的詳細資訊,請參閱同步處理數據結構及比較同步處理數據結構與 Windows API。
[靠上]
避免不產生冗長的工作
因為工作排程器會合作運作,所以它不會在工作之間提供公平性。 因此,工作可以防止其他工作啟動。 雖然在某些情況下是可以接受的,但在其他情況下,這可能會導致死結或饑餓。
下列範例會執行比已配置處理資源數目更多的工作。 第一個任務不會屈服於工作排程器,因此第二個工作在第一個工作完成之前不會啟動。
// 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
只會在目前線程所屬的排程器上產生另一個作用中線程、輕量型工作或其他操作系統線程。 此方法不會產生排程在並行::task_group或並行存取::structured_task_group 對象中執行但尚未啟動的工作。
還有其他方法可以在長時間執行的工作之間進行合作。 您可以將大型工作分成較小的子工作。 您也可以在冗長的工作期間啟用超額訂閱。 過度訂閱可讓您建立比可用硬體執行緒數目更多的執行緒。 當長時間的工作包含大量的延遲時,過度訂閱特別有用,例如,從磁碟或網路連線讀取數據。 如需輕量型工作和超訂閱的詳細資訊,請參閱 工作排程器。
[靠上]
使用超訂閱來位移封鎖或具有高延遲的作業
並行運行時間提供同步處理基本類型,例如 並行::critical_section,讓工作能夠合作封鎖併產生彼此。 當某個工作合作封鎖或產生時,工作排程器可以在第一個工作等候數據時,將處理資源重新配置至另一個內容。
在某些情況下,您無法使用並行運行時間所提供的合作式封鎖機制。 例如,您使用的外部連結庫可能會使用不同的同步處理機制。 另一個範例是當您執行可能會有大量延遲的作業時,例如,當您使用 Windows API ReadFile
函式從網路連線讀取數據時。 在這些情況下,超額訂閱可讓其他工作在另一個工作閑置時執行。 過度訂閱可讓您建立比可用硬體執行緒數目更多的執行緒。
請考慮下列函式, download
它會在指定的 URL 下載檔案。 這個範例會使用 並行::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
式會執行潛在的潛伏作業,因此超額訂閱可讓其他工作在目前工作等候數據時執行。 如需此範例的完整版本,請參閱 如何:使用 Oversubscription 來位移延遲。
[靠上]
盡可能使用並行記憶體管理功能
當您有經常配置相對較短存留期之小型物件的精細工作時,請使用記憶體管理功能: :Alloc 和 concurrency::Free。 並行運行時間會針對每個執行中的線程保留個別的記憶體快取。 和 Free
函Alloc
式會從這些快取配置和釋放記憶體,而不需要使用鎖定或記憶體屏障。
如需這些記憶體管理功能的詳細資訊,請參閱 工作排程器。 如需使用這些函式的範例,請參閱 如何:使用 Alloc 和 Free 來改善記憶體效能。
[靠上]
使用RAII來管理並行物件的存留期
並行運行時間會使用例外狀況處理來實作取消等功能。 因此,當您呼叫運行時間或呼叫另一個呼叫運行時間的連結庫時,撰寫例外狀況安全程序代碼。
資源 擷取是初始化 (RAII) 模式的其中一種方式,可安全地管理指定範圍內並行物件的存留期。 在RAII模式下,會在堆疊上配置數據結構。 該數據結構會在建立資源時初始化或取得資源,並在數據結構終結時終結或釋放該資源。 RAII 模式保證解構函式會在封入範圍結束之前呼叫。 當函式包含多個 return
語句時,此模式很有用。 此模式也可協助您撰寫例外狀況安全程序代碼。 throw
當語句導致堆疊回溯時,會呼叫 RAII 對象的解構函式;因此,資源一律會正確刪除或釋放。
運行時間會定義數個使用RAII模式的類別,例如 concurrency::critical_section::scoped_lock 和 concurrency::reader_writer_lock::scoped_lock。 這些協助程式類別稱為 範圍鎖定。 當您使用 並行::critical_section 或 並行存取::reader_writer_lock 物件時,這些類別提供數個優點。 這些類別的建構函式會取得所提供 critical_section
或 reader_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;
}
}
此範例會產生下列範例輸出:
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模式來管理並行物件的存留期的其他範例,請參閱 逐步解說:從使用者介面線程移除工作、 如何:使用內容類別實作合作號誌,以及如何 :使用超訂閱來位移延遲。
[靠上]
請勿在全域範圍建立並行物件
當您在全域範圍建立並行物件時,可能會造成應用程式中發生像是死結或記憶體存取違規這類問題。
例如,當您建立並行執行階段物件時,執行階段會自動建立預設排程器 (如果尚未建立)。 於是在建構全域物件期間建立的執行階段物件會導致執行階段建立這種預設排程器。 不過,這個處理序會採用內部鎖定,而這可能會妨礙支援並行執行階段基礎結構的其他物件初始化。 另一個尚未初始化的基礎結構物件可能需要這個內部鎖定,所以可能造成應用程式中發生死結。
下列範例示範如何建立全域 並行::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
對象的正確方式範例,請參閱 工作排程器。
[靠上]
請勿在共享數據區段中使用並行物件
並行運行時間不支援在共享數據區段中使用並行物件,例如,data_seg#pragma
指示詞所建立的數據區段。 跨進程界限共用的並行物件可能會使運行時間處於不一致或無效的狀態。
[靠上]
另請參閱
並行執行階段最佳做法
平行模式程式庫 (PPL)
非同步代理程式程式庫
工作排程器
同步處理資料結構
比較同步處理資料結構與 Windows API
如何:使用 Alloc 和 Free 改善記憶體效能
如何:使用過度訂閱使延遲產生位移
如何:使用內容類別實作合作式信號
逐步解說:從使用者介面執行緒中移除工作
平行模式程式庫中的最佳做法
非同步代理程式程式庫中的最佳做法