CppCon 2014 学习:ASYNCHRONOUS COMPUTING IN C++
理解这个关于“异步计算”的解释:
异步计算(Asynchronous Computing)是什么?
- 异步计算指的是启动某项工作(任务)后,不会立即阻塞等待它完成,而是继续执行后续代码。
- 这项工作是异步执行的,可能会产生结果(某个值),也可能只是触发某些操作。
- 你可以选择:
- 之后某个时刻等待异步任务完成,或者
- 附加一个“回调”或“后续操作”,当任务完成时自动执行这段代码。
- 这听起来像并行处理(parallelism),但异步并不一定等同于并行。
- 异步强调的是“不阻塞等待”,而并行强调的是“多任务同时执行”。
- 异步编程可以用来自动实现代码的并行化,但本质是可以在单线程环境下运行,也可以扩展到有成千上万线程的环境中。
总结
异步计算就是“启动工作,不等它结束,先继续干别的”,完成时通过等待或回调来获取结果或触发后续操作。
什么是异步计算?
- 异步计算也被称为:
- 响应式计算(reactive computing)
- Actor计算(actor computing)
- 观察者模式(observer pattern)
- 核心思想是通过数据流(dataflow),包括静态和动态数据流,实现变更的传播。
- 目前已经有很多异步计算环境,比如:
- JavaScript的异步模型
- C#的异步和响应式扩展
- 以及很多函数式编程语言中广泛采用的模式
- 本次讨论中的“异步计算”并非严格意义上的响应式计算,而是试图将数据流模型与“常规”命令式C++集成。
- 讨论内容基于HPX环境:
- HPX是一个通用的并行运行时系统,适用于各种规模的应用。
总结
异步计算是通过数据流和变更传播的方式进行任务管理和执行的编程范式,支持高效并发和并行,HPX是实现该范式的强大平台。
异步环境(Asynchronous Environments)
- 异步计算需要一个合适的运行时系统,用来支持任务的调度和管理。
- 目前所有已有的异步环境都包含这样的运行时系统。
- C++标准库本身也提供了一些基础设施支持异步计算。
- 出乎意料的是,现有的概念和工具(如
future<T>
)基本适合实现异步,但仍需一些扩展来更好地支持复杂需求。 future<T>
是C++中用于处理异步结果的主要类型。- 标准库中默认的
future<T>
实现依赖于操作系统的内核线程(kernel threads)。 - 但是,内核线程粒度较粗,开销较大,不适合高性能、细粒度的异步调度。
小结
C++的异步支持基础很好,但默认的future<T>
实现效率有限,因此需要更轻量、更高效的运行时系统(比如HPX)来满足复杂的异步计算需求。
异步环境
异步环境中的细粒度任务拆分
- 即使是相对较小的工作,也能从拆分成更小的任务中受益。
- 这可能导致产生大量的“线程”。
- 在之前的思想实验中,考虑过多达1000万线程的情况。
- 最佳的性能扩展通常在使用大约10000线程时达到(以完成10秒的工作量为例)。
遇到的问题
- 无法直接用内核线程管理这么多线程,因为内核线程开销太大,无法高效调度。
- 难以管理和理解这么多任务的执行逻辑。
- 这就需要抽象机制来管理这些细粒度任务,提高效率和可控性。
总结
大量细粒度任务的管理不能简单依赖内核线程,而是要用高效的任务调度与抽象机制来实现,比如轻量级线程、任务队列或基于事件的调度系统。
CURRENT STD::FUTURE
这段关于“future”的内容,可以理解为:
什么是 Future(未来值)?
- Future 是一个对象,代表了一个尚未计算完成的结果。
- 它可以跨线程工作,涉及两个主要“位置”(Locality):
- Locality 1: Future 对象存在的地方,消费者(使用结果的代码)可能会挂起(suspend)当前线程,等待结果。
- Locality 2: 另一个线程执行计算任务(生产者线程),计算完成后,将结果写回 Future。
- 当结果准备好后,消费者线程被恢复(resume),继续执行。
Future 的作用和好处:
- 透明同步:让消费者代码能够透明地等待结果,无需关心底层线程细节。
- 隐藏线程复杂性:使用 future 不需要直接管理线程的启动、等待和同步。
- 管理异步:让异步操作变得更易管理,避免回调地狱。
- 组合异步操作:可以把多个异步操作组合起来,形成复杂的异步流程。
- 将并发转为并行:通过 future 和底层调度,异步并发代码能够高效利用多核,执行并行计算。
简单来说,future 是异步编程的核心抽象,代表“未来某时可用的结果”,让编写异步代码更自然、更易维护。
什么是 Future?
- Future 是表示某个结果将在未来某时刻可用的对象。
- 获取一个 future 的方式有很多,这里给出最简单的例子,使用 C++ 标准库的
std::async
:
int universal_answer() { return 42; }
void deep_thought() {std::future<int> promised_answer = std::async(&universal_answer);// 这里可以做其他事情,模拟等待了很长时间
}
std::async
会异步调用universal_answer
函数,返回一个future<int>
对象。- 你可以稍后调用
promised_answer.get()
来等待结果并获取返回值:
cout << promised_answer.get() << endl; // 最终会打印出 42
关键点理解:
future
让你可以立即得到一个“承诺”,这个承诺代表了将来可用的结果。- 你不需要马上阻塞等待结果,可以继续执行其他代码。
- 只有当你真正调用
.get()
时,代码会等待结果完成(如果还没完成的话)。 - 这体现了异步编程的核心思想:先启动任务,后等待结果。
创建 Future 的几种方式
C++ 标准定义了三种创建 future
的主要方式,也就是三种不同的异步提供者(asynchronous providers):
std::async
- 就像之前例子中演示的,使用
std::async
可以很方便地启动异步任务并得到一个future
。 - 但
std::async
也有一些需要注意的限制或副作用(caveats),例如线程创建的开销、行为依赖于传入的启动策略等。
- 就像之前例子中演示的,使用
std::packaged_task
- 它把一个可调用对象(函数、lambda 等)包装起来,可以异步执行,并通过
future
获取结果。 - 适合需要更灵活地控制任务执行时机的场景。
- 它把一个可调用对象(函数、lambda 等)包装起来,可以异步执行,并通过
std::promise
- 是一种“生产者”角色,可以手动设置异步结果。
- 与
future
配对使用,允许代码中某个地方显式地给future
设置值或异常。
重点
- 这三者都是标准库提供的异步机制,不同场景下选用合适的工具可以更好地管理异步操作。
std::async
简单方便,但并非万能。packaged_task
和promise
更灵活,但需要手动管理。
包装一个 Future:std::packaged_task
std::packaged_task
是一个函数对象,它包装了一个可调用对象(如函数、lambda等)。- 当调用这个函数对象时,它会产生一个对应的
future
,这个future
代表该调用结果(异步结果)。 - 它可以作为一种同步原语(synchronization primitive),也就是说,可以用它来协调不同线程之间的执行和通信。
- 你可以把它传递给
std::thread
来异步执行包装的函数。 - 它的一个重要用途是:把传统的回调(callback)机制转换成
future
,也就是将“观察者模式”转化为可以等待的异步结果。 - 这样,代码就能以同步的方式等待回调完成,提高可读性和易用性。
这段代码是一个简单封装异步执行的函数模板 simple_async
,它利用了 std::packaged_task
和 std::thread
来实现异步调用,并返回一个 std::future
用于获取结果。
代码逐步解析:
template <typename F, typename ...Arg>
std::future<typename std::result_of<F(Arg...)>::type>
simple_async(F func, Arg&&... arg)
{// 创建一个包装了函数 func 的 packaged_taskstd::packaged_task<typename std::result_of<F(Arg...)>::type()> pt(std::bind(std::forward<F>(func), std::forward<Arg>(arg)...));// 从 packaged_task 中获得一个 future,用于异步获取结果auto f = pt.get_future();// 创建一个新线程执行这个任务,并且传递参数std::thread t(std::move(pt));// 线程分离,后台执行,不阻塞调用线程t.detach();// 返回 future 对象给调用者,调用者可以 later get() 结果return std::move(f);
}
重点解释:
std::packaged_task
将一个函数封装成一个“任务”,可以异步调用。get_future()
返回一个std::future
,它代表任务执行的结果。- 新线程执行
packaged_task
,任务在后台运行。 - 调用者拿到
future
后,可以调用get()
阻塞等待结果。 detach()
让线程分离,异步执行;否则需要join()
。
小提示:
- 这里实际代码中
std::packaged_task<F>
应该改成std::packaged_task<typename std::result_of<F(Arg...)>::type()>
,因为packaged_task
模板参数是一个可调用签名。 - 传递参数的方式可以用
std::bind
或 lambda 来解决。
std::promise
与 std::future
之间的关系,核心是它们共享一个“共享状态”(shared state),promise
用于设置结果,future
用于获取结果。
理解要点:
std::promise
是异步结果的“承诺者”,负责设置异步操作的结果。std::future
是异步结果的“持有者”,负责获取该结果。- 两者共享同一个“共享状态”(shared state),这个状态存储了最终的结果(值或异常)。
promise
负责把结果写入共享状态。future
负责从共享状态读取结果。
具体流程:
- 创建一个
std::promise<T>
,它内部会创建一个共享状态。 - 通过
promise.get_future()
得到对应的std::future<T>
,共享同一个状态。 - 在异步执行线程里调用
promise.set_value(result)
或promise.set_exception(...)
,将结果或异常写入共享状态。 - 在调用线程里使用
future.get()
等待并获取结果。
总结
promise
和 future
是配对使用的异步机制:
promise
给结果(“承诺”一个结果)future
拿结果(等待并获取结果)
C++ 标准库中 std::packaged_task
的一个简化实现,展示了如何通过封装一个函数和一个 std::promise
来创建一个异步任务包装器。
关键点理解:
simple_packaged_task
是一个模板类,它封装了一个可调用对象(函数、lambda 等)和一个std::promise
。- 模板参数
R(Args...)
表示任务的函数签名:返回类型R
,参数列表Args...
。 - 内部有两个成员:
std::function<R(Args...)> fn;
:存储要执行的函数。std::promise<R> p;
:对应的 promise,用于保存任务执行结果。
- 构造函数:
- 通过完美转发接受一个可调用对象
f
,并保存到fn
。
- 通过完美转发接受一个可调用对象
- 调用运算符
operator()
:- 执行
fn
,并把结果通过p.set_value()
传递给 promise,进而更新共享状态。
- 执行
get_future()
:- 返回和这个 promise 关联的
std::future
,调用者通过它可以等待并获取任务的执行结果。
- 返回和这个 promise 关联的
作用总结:
simple_packaged_task
把一个普通函数“打包”成一个异步任务。- 当任务执行(调用
operator()
)时,结果被设置到 promise 中。 - 通过
get_future()
获取的 future 能够在别处异步等待和得到这个结果。
关联点:
这就是 C++ 异步计算的一个核心设计:用 packaged_task
包装任务,用 promise
传递结果,用 future
获取结果。
std::future
的扩展提案:目前 C++ 标准的std::future
功能有限,社区和标准委员会提出了一些技术规范草案(TS),打算在未来标准中增强它。- 主要扩展内容包括:
- 组合(Compositional)能力:支持把多个
future
组合起来,方便管理多个异步操作的结果。 - 并行组合(Parallel composition):让多个异步任务并行执行,并能合并它们的结果。
- 顺序组合(Sequential composition):支持串行执行异步任务,类似链式调用。
- 并行算法(Parallel Algorithms):将并行算法和
future
结合起来,使得算法可以异步执行。 - 并行任务区域(Parallel Task Regions):在特定区域内并行执行任务,提高效率。
- 扩展的
async
语义,包括数据流(dataflow):不仅启动异步任务,还能定义任务之间的数据依赖关系,实现响应式编程风格。
- 组合(Compositional)能力:支持把多个
关键点:
这些扩展的目标是让异步和并行编程更易用、更灵活,并且更贴合现代复杂应用的需求。
创建一个立即准备好的 future,即这个 future 一创建就已经有结果了,而不需要等待异步计算完成。
详细理解:
make_ready_future<T>(value)
是一个工具函数,直接创建一个已经完成并且携带结果value
的future
对象。- 这个机制用来避免不必要的异步调度开销,比如当你已经有结果时,不需要异步执行,只要返回一个立即就完成的
future
即可。 - 代码示例解释:
future<int> compute(int x) {if (x < 0) return make_ready_future<int>(-1); // 如果参数小于0,立即返回一个结果为-1的futureif (x == 0) return make_ready_future<int>(0); // 如果参数等于0,立即返回一个结果为0的future// 其他情况,异步执行do_work(x),返回真正的异步futurereturn async([](int par) { return do_work(par); }, x);
}
- 这样写的好处:
- 对于简单情况,避免了异步调度的开销。
- 对调用者透明,调用接口依然是
future<int>
。 - 方便代码逻辑更清晰地分支处理。
string make_string() {future<int> f1 = async([]() -> int { return 123; });future<string> f2 = f1.then([](future<int> f) -> string { return to_string(f.get()); });
}
future 的顺序组合(Sequential composition),也就是把一个 future 的完成结果用来触发另一个操作,形成链式异步调用。
详细理解:
future<int> f1 = async([]() -> int { return 123; });
这里创建了一个异步任务,返回整数123
的future<int>
。future<string> f2 = f1.then( ... );
这是关键,.then
是一个顺序组合操作符,表示“当f1
完成时,执行传入的 lambda 函数”。- Lambda 里接收的是一个
future<int>
,即f1
的结果。调用f.get()
取出结果,这里不会阻塞,因为f1
已经完成了。 - 然后把结果转换成字符串,返回
string
。 - 于是
f2
是一个future<string>
,代表了“先得到一个 int,然后转换为 string”这个异步链的最终结果。
核心点:
.then()
把未来的异步操作串联起来,形成链式调用。- 这就是顺序组合(Sequential composition)的示例。
- 通过
future
的组合,可以写出清晰的异步流程代码。 .get()
在.then
里不会阻塞,因为调用时f
已经完成。
你想要这部分代码的 C++ 实现细节或者举个更完整的例子吗?
future 的并行组合(Parallel composition),即同时等待多个异步操作完成,然后统一处理结果。
void test_when_all() {shared_future<int> shared_future1 = async([]() -> int { return 125; });future<string> future2 = async([]() -> string { return string("hi"); });future<tuple<shared_future<int>, future<string>>> all_f = when_all(shared_future1, future2);// also: when_any, when_some, etc.future<int> result = all_f.then([](future<tuple<shared_future<int>, future<string>>> f) -> int {return do_work(f.get());});
}
``
### 详细理解:
* `shared_future<int> shared_future1 = async([]() -> int { return 125; });`这是一个异步任务,返回整数 `125`,结果用 `shared_future<int>` 包装,表示结果可以被多个地方共享访问。
* `future<string> future2 = async([]() -> string { return string("hi"); });`另一个异步任务,返回字符串 `"hi"`。
* `future<tuple<shared_future<int>, future<string>>> all_f = when_all(shared_future1, future2);`关键点:`when_all` 是并行组合操作符,表示等待 `shared_future1` 和 `future2` 都完成,产生一个包含两个 future 的元组(tuple)。
* 之后调用 `.then(...)`,当 `all_f` 完成时,lambda 函数被调用。参数 `f` 是 `future<tuple<shared_future<int>, future<string>>>` 类型,调用 `f.get()` 取出结果元组。
* `do_work(...)` 是用户自定义的函数,接受元组结果进行处理,返回一个 `int`。
* `result` 是处理后的结果 future。
### 核心点:
* **并行组合**:`when_all` 等待多个异步操作都完成。
* 返回的是包含各个 future 的元组(tuple),方便统一访问。
* `.then` 可以顺序处理并行完成的结果。
* 还有类似的组合操作 `when_any`(任意一个完成)、`when_some`(部分完成)等。
### 总结:
* 这使得可以轻松地处理多个异步任务的结果,并在它们都完成后统一处理,极大方便异步编程。
* 体现了未来式(future)和异步组合的强大表达力。
# **C++ 标准中对并行算法的扩展(Parallel Algorithms,提案 N4071)**。
### 详细理解:
* **并行算法(Parallel algorithms)**:这是对标准算法(比如 `std::sort`, `std::for_each` 等)进行扩展,支持在多核、多线程环境下并行执行,提高性能。
* **语义基本和顺序算法一致**:并行算法的行为和结果与原来顺序执行的算法语义相同,但实现上可以并行执行。
* **第一个参数是执行策略(execution\_policy)**:新增参数控制算法执行的方式,比如:* `std::execution::seq` 表示顺序执行(和原来算法一样)* `std::execution::par` 表示并行执行* 还有其他策略比如 `par_unseq`(并行且向量化)
* **任务执行策略(task\_execution\_policy)**:这是执行策略的一种,代表算法会异步执行,返回 `future<>`。
* **算法返回 `future<>`**:当使用任务执行策略时,算法会立即返回一个 `future`,代表异步执行结果,程序可以稍后等待或继续做其他事情。
### 核心点总结:
* 传统算法升级为支持并行执行
* 通过添加 `execution_policy` 参数控制执行方式
* 任务执行策略支持异步执行,返回 `future`
* 提升性能且接口兼容标准算法
### 举个简化示例:
```cpp
#include <execution>
#include <future>
#include <vector>
#include <algorithm>
int main() {std::vector<int> v = {5, 3, 2, 4, 1};// 并行排序,等待完成std::sort(std::execution::par, v.begin(), v.end());// 异步并行排序,返回 futureauto fut = std::sort(std::execution::task, v.begin(), v.end());fut.get(); // 等待排序完成return 0;
}
对 C++ 标准库中 async
的扩展,特别是支持“数据流”(dataflow)式的异步组合。
详细理解:
- 问题场景:
当调用async
启动一个异步任务时,如果传入的参数本身是future
(还未完成的异步结果),怎么处理? - 正常行为(未扩展前):
async
不会自动等待这些future
参数完成,而是直接传递future
对象本身给异步函数。 - 扩展行为(引入 dataflow):
扩展的dataflow
模板函数会先等待所有future
类型的参数准备好(即对应的异步操作完成),然后再调用传入的函数F
。 dataflow
模板签名示例:template <typename F, typename... Arg> future<typename std::result_of<F(Arg...)>::type> dataflow(F&& f, Arg&&... arg);
- 行为说明:
- 如果某个参数
ArgN
是一个future
,dataflow
会自动等待这个future
完成,得到它的值,然后传递给函数F
。 - 非
future
类型的参数会直接传递给F
,不做等待。
- 如果某个参数
- 结果:
dataflow
返回的也是一个future
,代表最终异步调用的结果。
总结:
dataflow
是对async
的增强,用于**“异步函数调用的依赖管理”**。- 它可以把多个异步操作的结果组合起来,当所有输入准备好后再调用函数。
- 这样就实现了数据驱动的异步计算流程(dataflow),提高了异步编程的表达能力和组合能力。
简单示例(伪代码):
future<int> f1 = async([]{ return 10; });
future<int> f2 = async([]{ return 20; });
auto f3 = dataflow([](int a, int b) {return a + b;
}, f1, f2);
// f3 是一个future,只有当 f1 和 f2 都准备好后,才会调用lambda计算结果
int result = f3.get(); // result = 30
对并行算法的扩展,介绍了一个新的算法 gather
,并给出了其模板实现。
理解 gather
算法
gather
是一个对区间元素重新排列的算法,基于两个条件分区(partition)。- 模板参数:
BiIter
:双向迭代器类型,表示要处理的元素区间的迭代器。Pred
:谓词函数类型,用于判断元素是否满足某个条件。
- 参数说明:
[f, l)
:要处理的整个元素范围(begin 到 end)。p
:区间内的一个迭代器,分割范围为[f, p)
和[p, l)
两部分。pred
:谓词函数,用于判断元素是否满足条件。
- 函数逻辑:
return make_pair(stable_partition(f, p, not1(pred)), // 对区间 [f, p) 内元素进行稳定分区,把不满足 pred 的元素放到前面stable_partition(p, l, pred) // 对区间 [p, l) 内元素进行稳定分区,把满足 pred 的元素放到前面 );
- 返回值:
返回一个pair
,包含两个迭代器,分别是两次分区操作后的分界点。
具体含义
- 先对区间
[f, p)
使用stable_partition
,将不满足pred
的元素移到前面,满足的移到后面。返回新的分割点。 - 再对区间
[p, l)
使用stable_partition
,将满足pred
的元素移到前面,不满足的移到后面。返回新的分割点。 - 这样整体上相当于把区间
[f, l)
的元素按pred
分成三部分,且保持了原始顺序的稳定性。
应用场景
gather
适用于需要根据某个条件把数据聚合(gather)到中间点p
附近的场景。- 通过两次稳定分区,把满足条件和不满足条件的元素分布到特定区域,且不破坏元素的原有顺序。
简单举例
假设有数组 [1, 2, 3, 4, 5, 6, 7, 8, 9]
,取 p
指向中间元素 5
,pred
是判断元素是否是偶数。
- 第一次分区
[1, 2, 3, 4, 5)
里,把奇数移到前面,偶数移到后面。 - 第二次分区
[5, 6, 7, 8, 9)
里,把偶数移到前面,奇数移到后面。
最终实现了把偶数聚集在p
附近的效果。
这段代码是在扩展并行算法,引入了一个异步版本的 gather
,即 gather_async
。它基于前面介绍的 gather
,但做了异步并行处理,并返回一个 future
,表示异步执行的结果。
理解 gather_async
template <typename BiIter, typename Pred>
future<pair<BiIter, BiIter>> gather_async(BiIter f, BiIter l, BiIter p, Pred pred)
{return dataflow(unwrapped([](BiIter r1, BiIter r2) { return make_pair(r1, r2); }),parallel::stable_partition(task, f, p, not1(pred)),parallel::stable_partition(task, p, l, pred));
}
详细解释
- 返回类型:
future<pair<BiIter, BiIter>>
返回一个future
,表示异步执行完后得到的结果是两个迭代器组成的pair
。 - 参数:
f, l
:整个区间的起止迭代器。p
:区间中间的分割迭代器。pred
:判定函数。
- 执行流程:
- 使用
parallel::stable_partition
对两个区间分别进行异步稳定分区操作:[f, p)
区间按not1(pred)
分区(把不满足pred
的元素移到前面)。[p, l)
区间按pred
分区。
- 这两个
stable_partition
都返回future<BiIter>
,表示异步执行后的分区点。 - 使用
dataflow
函数将这两个future
作为输入,当两个异步操作都完成时,调用提供的 lambda:
该 lambda 将两个迭代器结果打包成unwrapped([](BiIter r1, BiIter r2) { return make_pair(r1, r2); })
pair
返回。
- 使用
dataflow
:
是一个组合异步操作的工具,它等待所有输入future
完成后,执行一个回调,并返回新的future
。unwrapped
:
是一个辅助函数,表示对输入future
解包(unwrap),将future
中的值作为普通参数传给 lambda。
整体效果
- 该函数实现了异步的
gather
操作,两个stable_partition
会并行执行。 - 返回值是一个
future
,允许调用者在后续代码中异步等待操作完成,或者进一步组合操作。 - 适合高性能异步并行场景,提高并行效率和资源利用率。
1D 热方程的异步并行求解实现,结合了传统数值方法和C++异步编程(基于future
/dataflow
)技术。下面帮你系统总结和理解:
1D HEAT EQUATION(1维热方程)
1. 数学背景
- 热方程是描述热量(温度)随时间和空间扩散的偏微分方程。
- 1维热方程描述沿一条线(空间维度只有一个方向)上的温度变化。
- 离散化后,我们将空间划分为若干点(格点),时间也分步计算。
2. 离散化与迭代公式
- 每个格点的温度在下一个时间点由它自己及左右相邻格点的温度计算得出。
- 公式(离散三点模板):
T j i + 1 = T j i + k ⋅ Δ t Δ x 2 ( T j − 1 i − 2 T j i + T j + 1 i ) T_j^{i+1} = T_j^i + \frac{k \cdot \Delta t}{\Delta x^2} (T_{j-1}^i - 2 T_j^i + T_{j+1}^i) Tji+1=Tji+Δx2k⋅Δt(Tj−1i−2Tji+Tj+1i)
其中:- T j i T_j^i Tji 表示时间步 i i i 时,空间位置 j j j 的温度
- k k k 是扩散系数
- Δ t \Delta t Δt 是时间步长, Δ x \Delta x Δx 是空间步长
数学模型和数值解法
- 模拟一维热扩散,使用简单的**三点模板(3-point stencil)**迭代更新
- 核心计算(热扩散核函数):
double heat(double left, double middle, double right) {return middle + (k * dt / (dx * dx)) * (left - 2 * middle + right);
}
表示某点温度随时间变化受相邻左右两点影响。
传统同步实现
- 一步时间迭代,使用周期边界条件
void heat_timestep(std::vector<double>& next, std::vector<double> const& curr) {#pragma omp parallel forfor (std::size_t i = 0; i != nx; ++i)next[i] = heat(curr[idx(i-1, nx)], curr[i], curr[idx(i+1, nx)]);
}
idx
是计算周期边界的索引(例如环绕)- 时间循环迭代:
std::array<std::vector<double>, 2> U = { std::vector<double>(nx), std::vector<double>(nx) };
for (std::size_t t = 0; t != nt; ++t) {auto const& curr = U[t % 2];auto& next = U[(t + 1) % 2];heat_timestep(next, curr);
}
异步版本(Futurized)
- 用
shared_future<double>
包装数据,实现异步数据流传播 - 以时间步长为单位,异步计算每个格点的温度
void heat_timestep(std::vector<shared_future<double>>& next, std::vector<shared_future<double>> const& curr) {for (std::size_t i = 1; i != nx - 1; ++i) {next[i] = dataflow(unwrapped(heat),curr[idx(i - 1, nx)], curr[i], curr[idx(i + 1, nx)]);}
}
dataflow
:等待输入future
准备好后,调用heat
计算,返回新的future
颗粒度控制与分块计算(Partitioning)
- 将数据按块划分,控制每个异步任务的粒度,避免任务过小开销过大
- 时间步迭代:
std::array<std::vector<shared_future<std::vector<double>>>, 2> U { ... };
for (std::size_t t = 0; t != nt; ++t) {auto const& curr = U[t % 2];auto& next = U[(t + 1) % 2];heat_timestep(next, curr);
}
- 分块异步时间步:
void heat_timestep(std::vector<shared_future<std::vector<double>>>& next,std::vector<shared_future<std::vector<double>>> const& curr) {for (std::size_t i = 0; i != np; ++i) {next[i] = dataflow(unwrapped(heat_partition),curr[idx(i - 1, np)], curr[i], curr[idx(i + 1, np)]);}
}
总结理解
- 传统热方程数值解法基于同步循环+OpenMP并行
- 利用C++异步机制(
shared_future
,dataflow
)将时间步计算转为异步依赖计算(dataflow graph) - 通过分块控制粒度,避免生成大量微小任务,保证性能
- 异步版本允许任务自动调度,隐式处理依赖,适合复杂异步并行环境
- 本质是用异步编程模式来表达数值算法的数据依赖和并行,结合
future
实现透明同步