我是C ++的新手。我发现以下编程风格对我来说非常有趣。我在这里写了一个简化版本。
#include <iostream>
using namespace std;
class MyClass {
public :
MyClass(int id_) : id(id_) {
cout<<"I am a constructor"<<endl;
}
bool error = false;
void run() {
//do something ...
if (!error) {
read();
}
}
void read() {
//do something ...
if (!error) {
write();
}
}
void write() {
//do something ...
if (!error) {
read();
}
}
private :
int id;
};
int main() {
MyClass mc(1);
mc.run();
return 0;
}
此处的示例是可编译的,但我没有运行它,因为我必须进入无限循环。但是,我希望将此作为参考。 read()和write()互相调用。我第一次在boost.asio遇到了这种编程风格。当服务器在do_read()中收到消息时,它调用do_write()来回显客户端,然后在do_write()的末尾再次调用do_read()。
我对这种编码有两个问题。
这会导致堆栈溢出吗?因为函数一直在调用自己,函数结束只发生错误。
它有什么优势?为什么我不能使用函数有序地循环它们并在遇到错误时打破循环。
bool replied = true;
while (!error) {
if (replied) read();
else {
write();
replied = !replied;
}
}
答案 0 :(得分:2)
您的简化版本遗漏了最重要的方面:write()
和read()
来电是异步。
因此,函数实际上不会导致递归,请参阅最近的答案:Do "C++ boost::asio Recursive timer callback" accumulate callstack?
关于async_read(...)
和async_write(...)
的“不寻常”的事情是函数在IO操作实际执行之前返回,更不用说完成了。实际执行是按照不同的时间表进行的。
要将完成信号发送回“调用者”,异步调用通常会使用完成处理程序,然后使用IO操作的结果调用它。
在该完成处理程序中,通常会看到通信通道的末尾或正在调度的下一个IO操作。这称为异步调用链接,并且在支持异步操作的许多语言中非常突出²
需要一些时间来习惯,但最终你会习惯这种模式。
考虑到这一点,重新访问其中一个增强样本并查看便士是否会下降:
Documentation sample Chat Client
void handle_connect(const boost::system::error_code& error)
{
if (!error)
{
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
boost::bind(&chat_client::handle_read_header, this,
boost::asio::placeholders::error));
}
}
void handle_read_header(const boost::system::error_code& error)
{
if (!error && read_msg_.decode_header())
{
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.body(), read_msg_.body_length()),
boost::bind(&chat_client::handle_read_body, this,
boost::asio::placeholders::error));
}
else
{
do_close();
}
}
void handle_read_body(const boost::system::error_code& error)
{
if (!error)
{
std::cout.write(read_msg_.body(), read_msg_.body_length());
std::cout << "\n";
boost::asio::async_read(socket_,
boost::asio::buffer(read_msg_.data(), chat_message::header_length),
boost::bind(&chat_client::handle_read_header, this,
boost::asio::placeholders::error));
}
else
{
do_close();
}
}
void do_write(chat_message msg)
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
boost::bind(&chat_client::handle_write, this,
boost::asio::placeholders::error));
}
}
void handle_write(const boost::system::error_code& error)
{
if (!error)
{
write_msgs_.pop_front();
if (!write_msgs_.empty())
{
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
boost::bind(&chat_client::handle_write, this,
boost::asio::placeholders::error));
}
}
else
{
do_close();
}
}
void do_close()
{
socket_.close();
}
异步IO对于更基于事件的IO模型非常有用。当扩展到大量IO操作时,它们也会移除第一个“天花板”。在传统的命令式代码模式中,许多客户端/连接需要许多线程才能同时为它们提供服务。但实际上,线程无法扩展(因为典型的服务器具有少量的逻辑CPU),这意味着IO操作会相互阻塞³。
使用异步IO,您通常可以在单个线程上执行所有IO操作,从而大大提高效率 - 从而提高程序设计的某些方面(因为需要涉及的线程问题更少)。
¹存在许多选择,但假设io_service::run()
正在一个单独的线程上运行,这将导致IO操作实际执行,可能在需要时恢复并在该线程上完成
²我认为javascript对于这种模式来说是臭名昭着的
³一个典型的例子是远程过程调用在等待例如等待线程时保持线程占用的情况。要完成的数据库查询
答案 1 :(得分:0)
这是我的意见:
导致堆栈溢出的一种方法是让函数递归调用自身,溢出调用堆栈。一组以循环方式相互调用的函数将等同于此,所以是的,你的直觉是正确的。
算法的迭代版本(例如您描述的循环)可以防止这种情况。
现在,另一个可以防止堆栈溢出的事情是存在可以针对尾递归进行优化的代码。尾递归优化需要编译器实现此功能。大多数主要编译器都实现它。您提到的Boost.Asio功能似乎从这种优化中受益。
现在,C ++实现了许多编程范例。这些范例也由许多其他编程语言实现。与您正在讨论的内容相关的编程范例将是:
从结构化编程的角度来看,您应该尝试通过在最小化冗余代码的子例程中使用代码来尽可能地强调代码重用。
从面向对象的角度来看,您应该以尽可能封装其逻辑的方式对类进行建模。
到目前为止,您提供的逻辑似乎已经足够封装,但是,您可能需要检查方法write
和read
是否应该保留public
,或者它们是否应该{{1}相反。最大限度地减少公共方法的数量有助于实现更高级别的封装。