io_service/io_context如何run?
io_service的run接口是阻塞的,会阻塞当前线程,这对于服务端来说没什么影响,它只要异步接受客户端请求之后调用run接口即可,一直阻塞等待后续的连接到来就行了。
但是对于客户端来说阻塞当前进程会有问题,因为阻塞了当前线程的话,用户就无法自由地发送消息了。所以对于客户端来说一般是放在一个线程中run,这样就可以避免阻塞当前线程了。 其实还有另外一个原因,如果在主线程中调用run之前连续发消息,消息无法及时发送出去,因为io_service并没有启动起来,无法处理异步事件,因此,应该保证io_service在异步操作发起的时候就要run起来,放到线程中 就可以保证让io_service及时跑起来了。
连续异步发送的问题
使用asio的异步接口需要注意一些问题,比如不能同时发起多个异步操作,需要通过异步循环的方式发起异步操作。这对于异步读来说倒不是问题,因为在异步读的回调函数 里发起异步读就行了。但是对于需要连续发送消息的场景就有点问题了,因为用户并不知道异步发送何时完成,用户不可能等待前面的发送结束之后再发送消息,如果用户连续发起多个异步写操作 ,会导致消息的乱序发送,还会导致asio的未定义行为。
因此,如何让用户可以随时连续发送消息是一个需要解决的问题。
对于这个问题boost.asio文档中提供了一个经典的解决方法,在client中放一个发送队列,用户发消息的时候检查一下当前发送队列是否为空,如果为空就直接发送,否则就放到队列中。 内部的发送接口会不断从队列中取出数据发送直到发送完所有数据为止。
void write(const chat_message& msg)
{
boost::asio::post(io_context_,
[this, msg]()
{
bool write_in_progress = !write_msgs_.empty();
write_msgs_.push_back(msg);
if (!write_in_progress)
{
do_write();
}
});
}
void do_write()
{
boost::asio::async_write(socket_,
boost::asio::buffer(write_msgs_.front().data(),
write_msgs_.front().length()),
[this](boost::system::error_code ec, std::size_t /*length*/)
{
if (!ec)
{
write_msgs_.pop_front();
if (!write_msgs_.empty())
{
do_write();
}
}
else
{
socket_.close();
}
});
}
这里有一个值得注意的细节,write发送msg的时候是通过asio::post方式将msg放到发送队列中的,为什么要用asio::post, 而不是直接放到队列中呢? 之所以这样做,是为了保证发送队列的线程安全。asio::post实际上是一个线程池,把消息入队放到线程池中了,从而保证了发送队列的线程安全。那么, 为什么发送队列会有线程安全问题?前面提到,为了不阻塞当前主线程,将io_service的run放到一个线程中run,而我们发送消息是在主线程中,异步发送是在io线程中, 所以是多线程操作发送队列需要保证线程安全,通过post到线程池可以做到线程安全。除了post方式之外,我们还可以自己加锁来保证发送队列的线程安全性。
void write0(std::string msg) {
{
std::unique_lock<std::mutex> lock(mtx_);
write_msgs_.emplace_back(std::move(msg));
if (write_msgs_.size() > 1) {
return;
}
}
write0();
}
void write0() {
auto& msg = get_msg();
auto write_size = (uint32_t)msg.size();
std::array<boost::asio::const_buffer, 2> write_buffers;
write_buffers[0] = boost::asio::buffer(&write_size, sizeof(uint32_t));
write_buffers[1] = boost::asio::buffer(msg.data(), write_size);
boost::asio::async_write(socket_, write_buffers,
[this](boost::system::error_code ec, std::size_t length) {
if (ec) {
close();
std::cout << ec.message() << '\n';
return;
}
std::unique_lock<std::mutex> lock(mtx_);
write_msgs_.pop_front();
if (!write_msgs_.empty()) {
lock.unlock();
write0();
}
});
}
std::string& get_msg() {
std::unique_lock<std::mutex> lock(mtx_);
auto& msg = write_msgs_.front();
return msg;
}
两种发送方式性能比较
post方式和自己加锁的方式效率是差不多,实际上自己加锁的方式入队的速度会快一点点,因为加锁的粒度小一点,而异步发消息效率二者是相当的。
出自:purecpp 欢迎关注微信公众号: purecpp
地址: www.purecpp.org
转载请注明出处!