boost_asio/example/allocation/server.cpp
这个sample演示的是如何自定义handler的分配/释放。
asio中大量使用了handler,几乎所有异步函数中都带handler参数,例如io_service::post,async_read_some等等。在调用这些函数时,asio会分配一块内存拷贝保存这个handler,以便在异步完成时调用这个handler。一般的socket程序都会有很多次异步过程,例如循环调用async_read/write等等,这将导致频繁的分配释放小内存(一般情况下,handler的大小不会超过128 bytes,里面只有几个参数和shared_ptr),从而降低效率并且导致内存碎片。
为了避免这种情况的发生,可以自定义这个分配过程。可以在每个session(tcp连接)中预分配一块或几块小内存,让asio内部代码在分配并拷贝handler时使用这些内存。
为了达到这个效果,传入的handler object除了必须支持operator()(...)之外,还必须支持如下2个函数:asio_handler_allocate和asio_handler_deallocate。
有了这两个函数之后,asio在进行handler拷贝时会调用这两个函数来分配释放内存。
template <typename Handler>
inline custom_alloc_handler<Handler> make_custom_alloc_handler(
handler_allocator& a, Handler h)
{
return custom_alloc_handler<Handler>(a, h);
}
socket_.async_read_some(boost::asio::buffer(data_),
make_custom_alloc_handler(allocator_,
boost::bind(&session::handle_read,
shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred)));
以上代码调用make_custom_alloc_handler构造了一个custom_alloc_handler,并且把这个handler传给asio的async_read_some函数。async_read_some会调用custom_alloc_handler::asio_handler_allocate分配一块内存并保存这个custom_alloc_handler。这个custom_alloc_handler就是前面所说的必须支持operator()(...)以及asio_handler_allocate和asio_handler_deallocate的class object。
使用strand的情况下:
考虑如下代码:
void connection::start()
{
socket_.async_read_some(boost::asio::buffer(buffer_),make_custom_alloc_handler(allocator_,strand_.wrap(
boost::bind(&connection::handle_read, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred))));
}
首先用bind构造了一个handler,再用strand_.wrap包裹这个handler并产生一个新handler,然后再用make_custom_alloc_handler包裹一次。传入async_read_some时,asio使用自定义的分配函数拷贝这个custom_handler,执行custom_handler之后用自定义函数释放。执行custom_handler实际上执行的是strand.dispatch(),这会导致bind构造的handler被分配拷贝一次。
所以更为彻底的做法是:
void connection::start()
{
socket_.async_read_some(boost::asio::buffer(buffer_),make_custom_alloc_handler(allocator_,strand_.wrap(
make_custom_alloc_handler(allocator_,boost::bind(&connection::handle_read, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred)))));
}
注意如果可能在多个线程上发出异步消息(分配并拷贝handler)或者可能在多个线程上执行handler(释放handler),则asio可能在多个线程上调用asio_handler_allocate和asio_handler_deallocate。这两个函数内部必须考虑同步问题。前面使用strand的代码并不能保证不在多个线程上发出异步消息和在多个线程上执行custom_handler,strand只保证handler的执行串行化,实际上它也是wrap了一个strand_handler,并发执行strand_handler,在strand_handler中再dispatch一次消息来实现handler的串行化的。
在多个线程环境中,按我的理解,如果同一块内存的分配和释放可能在多个线程中同时进行,那么应该使用锁。如果能够保证同一块内存的分配和释放在同一个线程中,则比较简洁的方案是在每个thread的tls中分配一批小内存,每次使用make_custom_alloc_handler(tls_allocator_,boost::bind(func,param))。这种方案的内存利用率相当高,而且非常简洁。如果是多个线程绑在同一个io_service,可能很难做到这一点。每线程一个io_service则很容易做到。因为post给一个io_service的完成函数肯定也由该io_service在同一个线程调用。这种情况下也无需使用strand,因此也不存在2次使用make_custom_alloc_handler的情况。但这种方式的缺点在于很难处理多个io_service之间的平衡,也即不知道哪些post应该发给哪个io_service,很难做到负载均衡。有可能发生某些thread很忙,有些thread一直没事干的情况。与之相比,多个thread绑在一个io_service上就不存在这个问题,多个thread之间可以由系统很好的平衡负载均衡性,但内存的分配和释放可能就需要锁保护。注意这种情况下的锁应该使用InitializeCriticalSectionAndSpinCount而不是普通的CriticalSession,因为内存分配和释放操作都很快,宁可spinlock也不应该让线程进入等待状态。相比较而言,线程进入等待状态以及唤醒操作都是很耗时的。
浙公网安备 33010602011771号