C++并发编程实战:简化代码的艺术
函数化编程与并发
在并发编程中,函数化编程(FP)范式提供了一种简化代码的优雅方式。FP的核心思想是纯函数——函数的输出仅依赖于输入参数,不修改任何外部状态。这种特性使得FP天然适合并发环境,因为它消除了共享数据带来的竞争条件问题。
快速排序的FP实现
让我们通过快速排序算法来展示FP在并发编程中的应用。传统的快速排序是命令式风格,而FP版本则通过递归和不可变数据结构实现:
template<typename T>
std::list<T> sequential_quick_sort(std::list<T> input) {
if(input.empty()) return input;
std::list<T> result;
result.splice(result.begin(), input, input.begin());
T const& pivot = *result.begin();
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const& t){ return t < pivot; });
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(), divide_point);
auto new_lower = sequential_quick_sort(std::move(lower_part));
auto new_higher = sequential_quick_sort(std::move(input));
result.splice(result.end(), new_higher);
result.splice(result.begin(), new_lower);
return result;
}
这个实现的关键特点:
- 不修改原始输入,而是创建新列表
- 使用递归而非循环
- 通过移动语义提高效率
并行化FP快速排序
将FP版本的快速排序并行化非常简单,只需将递归调用改为异步执行:
template<typename T>
std::list<T> parallel_quick_sort(std::list<T> input) {
if(input.empty()) return input;
std::list<T> result;
result.splice(result.begin(), input, input.begin());
T const& pivot = *result.begin();
auto divide_point = std::partition(input.begin(), input.end(),
[&](T const& t){ return t < pivot; });
std::list<T> lower_part;
lower_part.splice(lower_part.end(), input, input.begin(), divide_point);
std::future<std::list<T>> new_lower(
std::async(¶llel_quick_sort<T>, std::move(lower_part)));
auto new_higher = parallel_quick_sort(std::move(input));
result.splice(result.end(), new_higher);
result.splice(result.begin(), new_lower.get());
return result;
}
这种并行化方式利用了硬件并发能力,每个递归分支都可以在独立线程中执行。注意使用std::async
会自动管理线程资源,避免创建过多线程。
基于消息传递的并发模型
另一种简化并发编程的方式是消息传递模型,也称为参与者模式(Actor Model)。在这种模型中:
- 每个线程是独立的"参与者"
- 参与者之间通过消息通信,不共享数据
- 每个参与者维护自己的状态机
ATM状态机示例
考虑一个ATM机的简化实现,我们可以将其逻辑建模为状态机:
struct card_inserted {
std::string account;
};
class atm {
messaging::receiver incoming;
messaging::sender bank;
messaging::sender interface_hardware;
void (atm::*state)();
std::string account;
std::string pin;
void waiting_for_card() {
interface_hardware.send(display_enter_card());
incoming.wait()
.handle<card_inserted>([&](card_inserted const& msg) {
account = msg.account;
pin = "";
interface_hardware.send(display_enter_pin());
state = &atm::getting_pin;
});
}
void getting_pin() {
incoming.wait()
.handle<digit_pressed>([&](digit_pressed const& msg) {
pin += msg.digit;
if(pin.length() == 4) {
bank.send(verify_pin(account, pin, incoming));
state = &atm::verifying_pin;
}
})
.handle<clear_last_pressed>([&](clear_last_pressed const& msg) {
if(!pin.empty()) pin.resize(pin.length()-1);
})
.handle<cancel_pressed>([&](cancel_pressed const& msg) {
state = &atm::done_processing;
});
}
// 其他状态函数...
public:
void run() {
state = &atm::waiting_for_card;
try {
for(;;) {
(this->*state)();
}
} catch(messaging::close_queue const&) {}
}
};
这种设计模式的优点:
- 明确的线程职责划分
- 无共享数据,避免竞争条件
- 状态转换清晰可见
- 易于测试和维护
持续性并发
C++并发技术扩展规范引入了持续性(continuations)概念,允许我们以更优雅的方式处理异步操作的结果。
基本持续性
持续性通过.then()
方法附加到future上,当future就绪时自动执行:
std::experimental::future<int> find_the_answer;
auto fut = find_the_answer();
auto fut2 = fut.then(find_the_question);
用户登录示例
考虑一个用户登录流程,传统异步实现可能如下:
std::future<void> process_login(std::string const& username,
std::string const& password) {
return std::async(std::launch::async, [=](){
try {
user_id const id = backend.authenticate_user(username, password);
user_data const info = backend.request_current_info(id);
update_display(info);
} catch(std::exception& e) {
display_error(e);
}
});
}
使用持续性可以将其改写为更清晰的链式调用:
std::experimental::future<void> process_login(
std::string const& username, std::string const& password) {
return backend.async_authenticate_user(username, password)
.then([](std::experimental::future<user_id> id) {
return backend.async_request_current_info(id.get());
})
.then([](std::experimental::future<user_data> info_to_display) {
try {
update_display(info_to_display.get());
} catch(std::exception& e) {
display_error(e);
}
});
}
这种方式的优势:
- 避免回调地狱
- 异常传播更自然
- 代码逻辑更线性化
多future等待
当需要等待多个future完成时,可以使用when_all
:
std::experimental::future<FinalResult> process_data(std::vector<MyData>& vec) {
std::vector<std::experimental::future<ChunkResult>> results;
// 启动多个异步任务...
return std::experimental::when_all(results.begin(), results.end())
.then([](std::future<std::vector<std::experimental::future<ChunkResult>>> ready_results) {
std::vector<ChunkResult> v;
for(auto& f: ready_results.get()) {
v.push_back(f.get());
}
return gather_results(v);
});
}
这种方式比轮询多个future更高效,资源利用率更高。
总结
通过函数化编程、消息传递和持续性这三种技术,我们可以显著简化并发代码:
- 函数化编程:通过纯函数和不可变数据消除共享状态
- 消息传递:通过明确的通信渠道协调线程活动
- 持续性:通过链式调用组织异步操作
这些方法共同降低了并发编程的复杂度,使代码更易于理解、维护和扩展。在实际项目中,根据具体需求选择合适的模式或组合使用这些技术,可以构建出既高效又可靠的并发系统。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考