并发运行时中的常规最佳做法
更新:2011 年 3 月
本文档描述应用于并发运行时的多个领域的最佳实践。
各节内容
本文档包含以下几节:
尽可能使用协作同步构造
避免不让步的长任务
使用过度订阅抵消停滞或高延迟的操作
尽可能使用并发内存管理函数
使用 RAII 管理并发对象的生存期
不要在全局范围内创建并发对象
不要在共享数据段中使用并发对象
尽可能使用协作同步构造
并发运行时提供了许多无需外部同步对象的并发安全的构造。 例如,Concurrency::concurrent_vector 类可提供并发安全的追加和元素访问操作。 但是,如果您需要对资源进行独占访问,则运行时可提供 Concurrency::critical_section、Concurrency::reader_writer_lock 和 Concurrency::event 类。 这些类型以协作方式工作;因此,当第一个任务等待数据时,任务计划程序可以将处理资源重新分配给另一个上下文。 请尽可能使用这些同步类型而不是其他同步机制,例如 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 方法只对当前线程所属的计划程序中的其他活动线程、轻量级任务或另一个操作系统线程做出让步。 此方法不会对 Concurrency::task_group 或 Concurrency::structured_task_group 对象中计划运行但尚未开始的工作做出让步。
还可以通过其他方式在长期运行的任务之间实现协作。 您可以将大任务划分为较小的子任务, 还可以在执行长期任务期间启用过度订阅。 您可以通过过度订阅创建比可用硬件线程数更多的线程。 如果长期任务包含大量延迟(例如,从磁盘或网络连接读取数据),则过度订阅会很有用。 有关轻量级任务和过度订阅的更多信息,请参见任务计划程序(并发运行时)。
[转到页首]
使用过度订阅抵消停滞或高延迟的操作
并发运行时可提供同步基元(如 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 函数执行潜在延迟操作,所以在当前任务等待数据时,过度订阅可以使其他任务能够运行。 有关此示例的完整版本,请参见如何:使用过度订阅抵消延迟。
[转到页首]
尽可能使用并发内存管理函数
如果您具有经常分配生存期相对较短的小型对象的精细任务时,可使用内存管理函数 Concurrency::Alloc 和 Concurrency::Free。 并发运行时为每个正在运行的线程保留单独的内存缓存。 在不使用锁或内存屏障的情况下,Alloc 和 Free 函数从这些缓存分配和释放内存。
有关这些内存管理函数的更多信息,请参见任务计划程序(并发运行时)。 有关使用这些函数的示例,请参见如何:使用 Alloc 和 Free 提高内存性能。
[转到页首]
使用 RAII 管理并发对象的生存期
并发运行时使用异常处理实现取消等功能。 因此,在您调用运行时或调用另一个调用该运行时的库时,可编写异常安全代码。
“资源获取即初始化”(RAII) 模式是一种在给定范围内安全管理并发对象的生存期的方式。 在 RAII 模式下,将在堆栈上分配一个数据结构。 该数据结构在创建时将会初始化或获取一个资源,而且该数据结构在销毁时将会销毁或释放该资源。 RAII 模式可确保在封闭范围退出之前调用析构函数。 在函数包含多个 return 语句时,此模式很有用。 此模式还可帮助您编写异常安全代码。 在 throw 语句导致堆栈展开时,将会调用 RAII 对象的析构函数;因此,始终会正确删除或释放资源。
运行时定义了几种使用 RAII 模式的类,例如,Concurrency::critical_section::scoped_lock 和 Concurrency::reader_writer_lock::scoped_lock。 这些帮助器类称作“范围锁”。 在使用 Concurrency::critical_section 或 Concurrency::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 模式管理并发对象生存期的其他示例,请参见演练:从用户界面线程中移除工作、如何:使用上下文类实现协作信号量和如何:使用过度订阅抵消延迟。
[转到页首]
不要在全局范围内创建并发对象
如果在全局范围内创建并发对象,则您的应用程序可能发生死锁。
在创建并发运行时对象时,如果尚未创建计划程序,那么运行时会创建一个默认计划程序。 还会为在全局对象构造期间创建的运行时对象创建计划程序。 但是,此过程采用了内部锁,这可能会干扰支持并发运行时基础结构的其他对象的初始化过程。 因为另一个尚未初始化的基础结构对象可能需要此内部锁,所以您的应用程序可能发生死锁。
以下示例演示了如何创建全局 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 对象的正确方式的示例,请参见任务计划程序(并发运行时)。
[转到页首]
不要在共享数据段中使用并发对象
并发运行时不支持在共享数据段(例如,由 data_seg #pragma 指令创建的数据段)中使用并发对象。 跨进程边界共享的并发对象可使运行时处于不一致或无效的状态。
[转到页首]
请参见
任务
概念
其他资源
修订记录
日期 |
修订记录 |
原因 |
---|---|---|
2011 年 3 月 |
添加了有关可能在全局范围内发生死锁的信息。 |
客户反馈 |