windows C++-并发中的最佳做法(一)
本文档介绍适用于并发运行时多个区域的最佳做法。
尽可能使用协作同步构造
并发运行时提供许多不需要外部同步对象的并发安全构造。 例如,concurrency::concurrent_vector 类可提供并发安全的追加和元素访问操作。 在这里,并发安全意味着指针或迭代器始终有效。 它不保证元素初始化或特定的遍历顺序。 但是,如果你需要对资源进行独占访问,则运行时可提供 concurrency::critical_section、concurrency::reader_writer_lock 和 concurrency::event 类。 这些类型以协作的形式工作;因此,当第一个任务等待数据时,任务计划程序可将处理资源重新分配到另一个上下文。 如果可能,请使用这些同步类型而不是其他同步机制(例如 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
ontext::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 函数从这些缓存中分配和释放内存,而不使用锁或内存屏障。