Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ Boost ASIO async_send_to memory leak

I am currently working on a UDP socket client. I am currently noticing a memory leak and I've tried several things in hopes to squash it, but it still prevails. In my main, I have a char* that has been malloc'd. I then call the below function to send the data:

void Send(const char* data, const int size) {
    Socket.async_send_to(boost::asio::buffer(data, size), Endpoint, boost::bind(&MulticastSender::HandleSendTo, this, boost::asio::placeholders::error));   
}

If I run this code, it will always leak memory. However, if I comment out the async_send_to call, the memory stays consistent. I have tried several variations(see below) on this, but they all only appear to speed up the memory leak.

A couple notes, there is a chance that the char* that gets passed to Send may get free'd before the call completes. However, in my variations, I have taken precaution to do handle that.

Variation 1:

void Send(const char* data, const int size) {
    char* buf = (char*)malloc(size);
    memcpy(buf, data, size);
    Socket.async_send_to(boost::asio::buffer(buf, size), Endpoint, boost::bind(&MulticastSender::HandleSendTo, this, boost::asio::placeholders::error, buf));   
}

void HandleSendTo(const boost::system::error_code& ec, const char* buf) {
    free(buf);
}

Variation 2:

class MulticastSender {
    char* Buffer;

    public:
    void Send(const char* data, const int size) {
        Buffer = (char*)malloc(size);
        memcpy(Buffer, data, size);
        Socket.async_send_to(boost::asio::buffer(Buffer, size), Endpoint, boost::bind(&MulticastSender::HandleSendTo, this, boost::asio::placeholders::error));
    }

    void HandleSendTo(const boost::system::error_code& ec) {
        free(Buffer);
    }
}

However, both variations seem to only speed up the memory leak. I have also tried removing the async_send_to and just calling boost::asio::buffer(data, size), but as has been explained in other questions, the buffer does not own the memory and thus it is up to the user to safely manage it. Any thoughts on what could be causing this issue and how to resolve it?

EDIT 1:

As suggested in the comments, I have preallocated a single buffer (for test purposes) and I am never deallocating it, however, the memory leak still persists.

class MulticastSender {
    char* Buffer;
    const int MaxSize = 16384;

    public:
    MulticastSender() {
        Buffer = (char*)malloc(MaxSize);
    }

    void Send(const char* data, const int size) {
        memcpy(Buffer, data, size);
        Socket.async_send_to(boost::asio::buffer(Buffer, size), Endpoint, boost::bind(&MulticastSender::HandleSendTo, this, boost::asio::placeholders::error));
    }

    void HandleSendTo(const boost::system::error_code& ec) {

    }
}

EDIT 2: As requested here is an MCVE of the problem. In making this I have also observed an interesting behavior that I will explain below.

#include <string>
#include <iostream>
#include <functional>
#include <thread>

#include <boost/asio.hpp>
#include <boost/bind.hpp>

class MulticastSender {
private:
    boost::asio::io_service IOService;
    const unsigned short Port;
    const boost::asio::ip::address Address;
    boost::asio::ip::udp::endpoint Endpoint;
    boost::asio::ip::udp::socket Socket;
    boost::asio::streambuf Buffer;

    void HandleSendTo(const boost::system::error_code& ec) {
        if(ec) {
            std::cerr << "Error writing data to socket: " << ec.message() << '\n';
        }
    }

    void Run() {
        IOService.run();
    }

public:
    MulticastSender(const std::string& address,
                    const std::string& multicastaddress,
                    const unsigned short port) : Address(boost::asio::ip::address::from_string(address)),
                                                 Port(port),
                                                 Endpoint(Address, port),
                                                 Socket(IOService, Endpoint.protocol()) {
        std::thread runthread(&MulticastSender::Run, this);
        runthread.detach();
    }

    void Send(const char* data, const int size) {
        std::ostreambuf_iterator<char> out(&Buffer);
        std::copy(data, data + size, out);
        Socket.async_send_to(Buffer.data(), Endpoint, boost::bind(&MulticastSender::HandleSendTo, this, boost::asio::placeholders::error));
    }
};

const int SIZE = 8192;

int main() {
    MulticastSender sender("127.0.0.1", "239.255.0.0", 30000);
    while(true) {
        char* data = (char*)malloc(SIZE);
        std::memset(data, 0, SIZE);
        sender.Send(data, SIZE);
        usleep(250);
        free(data);
    }
}

The above code still produces a memory leak. I should mention that I am running this on CentOS 6.6 with kernel Linux dev 2.6.32-504.el6.x86_64 and running Boost 1.55.0. I am observing this simply by watching the process in top.

However, if I simply move the creation of the MulticastSender into the while loop, I no longer observe the memory leak. I am concerned about the speed of the application though, so this is not a valid option.

like image 966
Tsy Avatar asked Feb 09 '23 04:02

Tsy


1 Answers

Memory is not leaking, as there is still a handle to the allocated memory. However, there will be continual growth because:

  • The io_service is not running because run() is returning as there is no work. This results in completion handlers being allocated, queued into the io_service, but neither executed nor freed. Additionally, any cleanup that is expected to occur within the completion handler is not occurring. It is worth noting that during the destruction of the io_service, completion handlers will be destroyed and not invoked; hence, one cannot depend on only performing cleanup within the execution of the completion handler. For more details as to when io_service::run() blocks or unblocks, consider reading this question.
  • The streambuf's input sequence is never being consumed. Each iteration in the main loop will append to the streambuf, which will then send the prior message content and the newly appended data. See this answer for more details on the overall usage of streambuf.

A few other points:

  • The program fails to meet a requirement of async_send_to(), where ownership of the underlying buffer memory is retained by the caller, who must guarantee that it remains valid until the handler is called. In this case, when copying into the streambuf via the ostreambuf_iterator, the streambuf's input sequence is modified and invalidates the buffer returned from streambuf.data().
  • During shutdown, some form of synchronization will need to occur against threads that are running the io_service. Otherwise, undefined behavior may be invoked.

To resolve these issues, consider:

  • Using boost::asio::io_service::work to ensure that the io_service object's run() does not exit when there is no work remaining.
  • Passing ownership of the memory to the completion handler via std::shared_ptr or another class that will manage the memory via resource acquisition is initialization (RAII) idiom. This will allow for proper cleanup and meet the requirement's of the buffer validity for async_send_to().
  • Not detaching and joining upon the worker thread.

Here is a complete example based on the original that demonstrates these changes:

#include <string>
#include <iostream>
#include <thread>
#include <boost/asio.hpp>

class multicast_sender
{
public:

  multicast_sender(
    const std::string& address,
    const std::string& multicast_address,
    const unsigned short multicast_port)
  : work_(io_service_),
    multicast_endpoint_(
      boost::asio::ip::address::from_string(multicast_address),
      multicast_port),
    socket_(io_service_, boost::asio::ip::udp::endpoint(
      boost::asio::ip::address::from_string(address),
      0 /* any port */))
  {
    // Start running the io_service.  The work_ object will keep
    // io_service::run() from returning even if there is no real work
    // queued into the io_service.
    auto self = this;
    work_thread_ = std::thread([self]()
      {
        self->io_service_.run();
      });
  }

  ~multicast_sender()
  {
    // Explicitly stop the io_service.  Queued handlers will not be ran.
    io_service_.stop();

    // Synchronize with the work thread.
    work_thread_.join();
  }

  void send(const char* data, const int size)
  {
    // Caller may delete before the async operation finishes, so copy the
    // buffer and associate it to the completion handler's lifetime.  Note
    // that the completion may not run in the event the io_servie is
    // destroyed, but the the completion handler will be, so managing via
    // a RAII object (std::shared_ptr) is ideal.
    auto buffer = std::make_shared<std::string>(data, size);
    socket_.async_send_to(boost::asio::buffer(*buffer), multicast_endpoint_,
      [buffer](
        const boost::system::error_code& error,
        std::size_t bytes_transferred)
      {
        std::cout << "Wrote " << bytes_transferred << " bytes with " <<
                     error.message() << std::endl;
      });
  }

private:
  boost::asio::io_service io_service_;
  boost::asio::io_service::work work_;
  boost::asio::ip::udp::endpoint multicast_endpoint_;
  boost::asio::ip::udp::socket socket_;
  std::thread work_thread_;

};

const int SIZE = 8192;

int main()
{
  multicast_sender sender("127.0.0.1", "239.255.0.0", 30000);
  char* data = (char*) malloc(SIZE);
  std::memset(data, 0, SIZE);
  sender.send(data, SIZE);
  free(data);

  // Give some time to allow for the async operation to complete 
  // before shutting down the io_service.
  std::this_thread::sleep_for(std::chrono::seconds(2));
}

Output:

Wrote 8192 bytes with Success
like image 196
Tanner Sansbury Avatar answered Feb 13 '23 02:02

Tanner Sansbury