boost :: asio :: socket线程安全

时间:2011-09-09 14:06:21

标签: c++ boost boost-asio

(这是我原始问题的简化版)

我有几个线程写入boost asio socket。这看起来效果很好,没有任何问题。

文档说共享套接字不是线程安全的(here,在底部向下)所以我想知道我是否应该使用互斥锁等保护套接字。

question坚持认为保护是必要的,但没有就如何保护提出建议。

我原来问题的所有答案也坚持我做的很危险,并且大多数人都敦促我用async_writes或更复杂的东西替换我的写作。但是,我不愿意这样做,因为它会使已经正常工作的代码变得复杂,并且没有一个回答者说服他们知道他们所说的内容 - 他们似乎已经阅读了与我相同的文档并且正在猜测,就像我一样是

所以,我编写了一个简单的程序来强调测试从两个线程写入共享套接字。

这是服务器,它只是写出从客户端收到的任何内容

int main()
{
    boost::asio::io_service io_service;

    tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 3001));

    tcp::socket socket(io_service);
    acceptor.accept(socket);

    for (;;)
    {
        char mybuffer[1256];
        int len = socket.read_some(boost::asio::buffer(mybuffer,1256));
        mybuffer[len] = '\0';
        std::cout << mybuffer;
        std::cout.flush();

    }

  return 0;
}

这是客户端,它创建两个线程,尽可能快地写入共享套接字

boost::asio::ip::tcp::socket * psocket;

void speaker1()
{
    string msg("speaker1: hello, server, how are you running?\n");
    for( int k = 0; k < 1000; k++ ) {
        boost::asio::write(
            *psocket,boost::asio::buffer(msg,msg.length()));
    }

}
void speaker2()
{
    string msg("speaker2: hello, server, how are you running?\n");
    for( int k = 0; k < 1000; k++ ) {
        boost::asio::write(
            *psocket,boost::asio::buffer(msg,msg.length()));
    }

}

int main(int argc, char* argv[])
{

    boost::asio::io_service io_service;

  // connect to server

    tcp::resolver resolver(io_service);
    tcp::resolver::query query("localhost", "3001");
    tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
    tcp::resolver::iterator end;
    psocket = new tcp::socket(io_service);
    boost::system::error_code error = boost::asio::error::host_not_found;
    while (error && endpoint_iterator != end)
    {
        psocket->close();
        psocket->connect(*endpoint_iterator++, error);
    }


    boost::thread t1( speaker1 );
    boost::thread t2( speaker2 );

    Sleep(50000);

}

这个有效!完全可以,据我所知。客户端不会崩溃。消息到达服务器时没有乱码。它们通常交替到达,每个线程一个。有时一个线程会在另一个线程之前收到两到三条消息,但只要没有乱码并且所有消息都到达,我认为这不是问题。

我的结论:在一些理论意义上,套接字可能不是线程安全的,但它很难让它失败,我不会担心它。

9 个答案:

答案 0 :(得分:7)

对非线程安全的异步处理程序使用boost::asio::io_service::strand

  

一个链被定义为严格顺序的事件调用   处理程序(即没有并发调用)。使用股线允许   在不需要的情况下在多线程程序中执行代码   显式锁定(例如使用互斥锁)。

timer tutorial可能是绕头绞尽脑汁的最简单方法。

答案 1 :(得分:5)

理解ASIO的关键是要意识到完成处理程序只能在调用io_service.run()的线程的上下文中运行,无论哪个线程调用异步方法。如果您只在一个线程中调用io_service.run(),则所有完成处理程序将在该线程的上下文中以串行方式执行。如果您在多个线程中调用io_service.run(),那么完成处理程序将在其中一个线程的上下文中执行。您可以将此视为一个线程池,其中池中的线程是在同一io_service.run()对象上调用io_service的线程。

如果您有多个线程调用io_service.run(),那么您可以通过将完成处理程序放入strand来强制完成处理程序。

要回答问题的最后部分,请致电boost::async_write()。这会将写入操作分配到已调用io_service.run()的线程上,并在写入完成时调用完成处理程序。如果您需要序列化此操作,那么它会更复杂一些,您应该阅读有关链here的文档。

答案 2 :(得分:5)

听起来这个问题归结为:

  

在来自两个不同线程的单个套接字上同时调用async_write_some()时会发生什么

我相信这正是非线程安全的操作。这些缓冲区将在线路上发出的顺序是未定义的,甚至可能是交错的。特别是如果您使用便捷函数async_write(),因为它实现为对下面的async_write_some()的一系列调用,直到整个缓冲区被发送。在这种情况下,从两个线程发送的每个片段可以随机交织。

保护您免受此情况影响的唯一方法是构建程序以避免出现这种情况。

这样做的一种方法是编写一个应用程序层发送缓冲区,单个线程负责将其推送到套接字上。这样你就可以只保护发送缓冲区本身。请记住,虽然简单的std::vector不起作用,但是在最后添加字节可能最终会重新分配它,可能是在有一个未完成的async_write_some()引用它时。相反,使用缓冲区的链接列表并使用asio的分散/聚集功能可能是个好主意。

答案 3 :(得分:5)

在重新编写async_write的代码之后,我现在确信当且仅当数据包大小小于

时,任何写操作都是线程安全的。
default_max_transfer_size = 65536;

一旦调用了async_write,就会在同一个线程中调用async_write_some。调用某种形式的io_service :: run的池中的任何线程将继续为该写操作调用async_write_some,直到它完成。

如果必须多次调用(数据包大于65536),这些async_write_some调用可以并将进行交错。

ASIO不像你期望的那样将写入队列排队到一个套接字,一个接着完成。为了确保线程交错安全写入,请考虑以下代码:

    void my_connection::async_serialized_write(
            boost::shared_ptr<transmission> outpacket) {
        m_tx_mutex.lock();
        bool in_progress = !m_pending_transmissions.empty();
        m_pending_transmissions.push(outpacket);
        if (!in_progress) {
            if (m_pending_transmissions.front()->scatter_buffers.size() > 0) {
                boost::asio::async_write(m_socket,
                    m_pending_transmissions.front()->scatter_buffers,
                        boost::asio::transfer_all(),
            boost::bind(&my_connection::handle_async_serialized_write,
                        shared_from_this(),
                        boost::asio::placeholders::error,
                                       boost::asio::placeholders::bytes_transferred));
            } else { // Send single buffer
                boost::asio::async_write(m_socket,
                                    boost::asio::buffer(
                                           m_pending_transmissions.front()->buffer_references.front(),                          m_pending_transmissions.front()->num_bytes_left),
                boost::asio::transfer_all(),
                boost::bind(
                        &my_connection::handle_async_serialized_write,
                        shared_from_this(),
                        boost::asio::placeholders::error,
                        boost::asio::placeholders::bytes_transferred));
            }
        }
        m_tx_mutex.unlock();
    }

    void my_connection::handle_async_serialized_write(
    const boost::system::error_code& e, size_t bytes_transferred) {
        if (!e) {
            boost::shared_ptr<transmission> transmission;
            m_tx_mutex.lock();
            transmission = m_pending_transmissions.front();
            m_pending_transmissions.pop();
            if (!m_pending_transmissions.empty()) {
                if (m_pending_transmissions.front()->scatter_buffers.size() > 0) {
            boost::asio::async_write(m_socket,
                    m_pending_transmissions.front()->scatter_buffers,
                    boost::asio::transfer_exactly(
                            m_pending_transmissions.front()->num_bytes_left),
                    boost::bind(
                            &chreosis_connection::handle_async_serialized_write,
                            shared_from_this(),
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
                } else { // Send single buffer
                    boost::asio::async_write(m_socket,
                    boost::asio::buffer(
                            m_pending_transmissions.front()->buffer_references.front(),
                            m_pending_transmissions.front()->num_bytes_left),
                    boost::asio::transfer_all(),
                    boost::bind(
                            &my_connection::handle_async_serialized_write,
                            shared_from_this(),
                            boost::asio::placeholders::error,
                            boost::asio::placeholders::bytes_transferred));
                }
            }
            m_tx_mutex.unlock();
            transmission->handler(e, bytes_transferred, transmission);
        } else {
            MYLOG_ERROR(
            m_connection_oid.toString() << " " << "handle_async_serialized_write: " << e.message());
            stop(connection_stop_reasons::stop_async_handler_error);
        }
    }

这基本上构成了一次发送一个数据包的队列。 async_write仅在第一次写入成功后调用,然后调用第一次写入的原始处理程序。

如果asio每个套接字/流自动写入队列,那会更容易。

答案 4 :(得分:2)

首先考虑套接字是一个流,并且内部没有防止并发读取和/或写入。有三个不同的考虑因素。

  1. 同时执行访问同一套接字的函数。
  2. 并发执行包含相同套接字的委托。
  3. 写入同一套接字的委托的交叉执行。
  4. chat example是异步但不是并发的。来自单个线程的io_service 运行,使所有聊天客户端操作都不是并发的。换句话说,它避免所有这些问题。即使async_write必须在内部完成发送消息的所有部分,然后才能继续进行任何其他工作,从而避免交错问题。

      

    处理程序仅由当前正在为io_service调用run(),run_one(),poll()或poll_one()的任何重载的线程调用。

    通过将工作发布到单个线程io_service,其他线程可以通过在io_service中排队工作来安全地避免并发和阻塞。但是,如果您的方案阻止您缓冲给定套接字的所有工作,则事情会变得更复杂。您可能需要阻止套接字通信(但不是线程),而不是无限地排队工作。此外,工作队列可能非常难以管理,因为它完全不透明。

    如果您的io_service运行多个线程,您仍然可以轻松避免上述问题,但您只能从其他读取或写入的处理程序(以及启动时)调用读取或写入。这将对所有访问套接字进行排序,同时保持非阻塞状态。安全性源于该模式在任何给定时间仅使用一个线程的事实。但是,从一个独立的线程发布工作是有问题的 - 即使你不介意缓冲它。

    strand是一个asio类,它以确保非并发调用的方式将工作发布到io_service。但是,使用strand来调用async_read和/或async_write只能解决三个问题中的第一个问题。这些函数在内部将工作发布到套接字的io_service。如果该服务正在运行多个线程,则可以同时执行该工作。

    那么,对于给定的套接字,您如何安全地同时安全地调用async_read和/或async_write?

    1. 对于并发呼叫者,第一个问题可以使用互斥锁或链来解决,如果你不想缓冲工作则使用前者,如果你想要缓冲工作则使用后者。这在函数调用期间保护套接字,但对其他问题没有任何作用。

    2. 第二个问题似乎最难,因为很难看到代码内部发生的异常从两个函数执行异步。异步函数都将工作发布到套接字的io_service。

    3. 来自boost套接字源:

      /**
       * This constructor creates a stream socket without opening it. The socket
       * needs to be opened and then connected or accepted before data can be sent
       * or received on it.
       *
       * @param io_service The io_service object that the stream socket will use to
       * dispatch handlers for any asynchronous operations performed on the socket.
       */
      explicit basic_stream_socket(boost::asio::io_service& io_service)
      : basic_socket<Protocol, StreamSocketService>(io_service)
      {
      }
      

      来自io_service :: run()

      /**
       * The run() function blocks until all work has finished and there are no
       * more handlers to be dispatched, or until the io_service has been stopped.
       *
       * Multiple threads may call the run() function to set up a pool of threads
       * from which the io_service may execute handlers. All threads that are
       * waiting in the pool are equivalent and the io_service may choose any one
       * of them to invoke a handler.
       *
       * ...
       */
      BOOST_ASIO_DECL std::size_t run();
      

      因此,如果你为一个套接字提供多个线程,它就别无选择,只能利用多个线程 - 尽管不是线程安全的。避免此问题的唯一方法(除了替换套接字实现)是给套接字只有一个线程可以使用。对于单个插座而言,无论如何这都是你想要的(所以不要为了替换而烦恼)。

      1. 第三个问题可以通过使用在async_write之前锁定的(不同的)互斥锁来解决,该互斥锁被传递到完成处理程序并在该点解锁。这将阻止任何调用者开始写入,直到前一次写入的所有部分都完成。
      2. 请注意,async_write帖子可以工作到队列 - 它几乎可以立即返回。如果你投入太多的工作,你可能不得不处理一些后果。尽管为套接字使用了单个io_service线程,但您可以通过对async_write的并发或非并发调用来发布任意数量的线程。

        另一方面,async_read很简单。没有交错问题,您只需从上一次调用的处理程序循环回来。您可能希望也可能不希望将生成的工作分派给另一个线程或队列,但如果您在完成处理程序线程上执行它,则只是阻止对单线程套接字的所有读取和写入。

        <强>更新

        我进一步深入研究了套接字流的底层实现(对于一个平台)的实现。看来套接字一直在调用线程上执行平台套接字调用,而不是发布到io_service的委托。换句话说,尽管async_read和async_write似乎立即返回,但实际上它们确实在返回之前执行所有套接字操作。只有处理程序发布到io_service。我没有记录,也没有通过我审查的exaple代码公开,但假设它是有保证的行为,它会显着影响上面的第二个问题。

        假设发布到io_service的工作不包含套接字操作,则无需将io_service限制为单个线程。然而,它确实强调了防止同步执行异步功能的重要性。因此,例如,如果一个人跟随聊天示例,而是将另一个线程添加到io_service,则会出现问题。通过在函数处理程序中执行异步函数调用,您可以执行并发函数。这将需要重新发布互斥锁或所有异步函数调用以在链上执行。

        更新2

        关于第三个问题(交错),如果数据大小超过65536字节,则工作在async_write内部分解并分批发送。但重要的是要理解,如果io_service中有多个线程,那么除了第一个线程之外的其他工作块将被发布到不同的线程。这一切都发生在调用完成处理程序之前的async_write函数内部。该实现创建了自己的中间完成处理程序,并使用它们来执行除第一个套接字操作之外的所有操作。

        这意味着如果有多个io_service线程要发布超过64kb的数据,那么围绕async_write调用(互斥或链)的任何防护都将保护套接字(默认情况下,这可能会有所不同)。因此,在这种情况下,交错防护装置不仅需要交错安全,还需要插座的线程安全。我在调试器中验证了所有这些。

        MUTEX选项

        async_read和async_write函数在内部使用io_service来获取要在其上发布完成处理程序的线程,阻塞直到线程可用。这使得使用互斥锁保护它们是危险的。当使用互斥锁来保护这些函数时,当线程备份到锁定时会发生死锁,从而使io_service挨饿。鉴于在发送&gt;时没有其他方法可以保护async_write。 64k使用多线程io_service,它有效地将我们锁定在该场景中的单个线程中 - 这当然解决了并发问题。

答案 5 :(得分:1)

根据2008年11月提升1.37 asio更新,某些同步操作(包括写入“现在是线程安全的”)允许“在单个套接字上进行并发同步操作,如果操作系统支持”boost 1.37.0 history。这似乎支持你所看到的,但过度简化的“共享对象:不安全”子句保留在ip :: tcp :: socket的boost文档中。

答案 6 :(得分:1)

对旧帖子的另一条评论...

我认为asio::async_write()重载的as​​io文档中的关键句子是the following

此操作是通过对流的async_write_some函数的零次或更多次调用来实现的,被称为组合操作。程序必须确保该流完成之前,该流不执行其他任何写操作(例如async_write,该流的async_write_some函数或任何其他执行写操作的组合操作)。

据我了解,本文记录了以上许多答案的假设: 如果多个线程执行asio::async_write ,则对io_context.run()的调用中的数据可能会交错。

也许这对某人有帮助;-)

答案 7 :(得分:0)

这取决于您是否从多个线程访问相同的套接字对象。假设你有两个线程运行相同的io_service::run()函数。

例如,如果您同时进行读写操作,或者可能执行取消操作 从其他线程。然后它不安全。

但是,如果您的协议一次只进行一次操作。

  1. 如果只有一个线程运行io_service运行,则没有问题。如果你想从其他线程执行套接字,你可以调用io_service :: post() 在socket上执行此操作的处理程序,因此它将在同一个线程中执行。
  2. 如果您有多个线程正在执行io_service::run并且您尝试同时执行操作 - 让我们说取消和读取操作,那么您应该使用线程。 Boost.Asio文档中有一个教程。

答案 8 :(得分:-2)

我一直在进行大量的测试,并且无法打破asio。即使没有锁定任何互斥锁。

但我会建议您使用async_readasync_write在每个调用周围使用互斥锁。

我认为唯一的缺点是如果你有多个线程调用io_service::run,你的完成处理程序可以同时调用。

就我而言,这不是一个问题。这是我的测试代码:

#include <boost/thread.hpp>
#include <boost/date_time.hpp>
#include <boost/asio.hpp>
#include <vector>

using namespace std;
char databuffer[256];
vector<boost::asio::const_buffer> scatter_buffer;
boost::mutex my_test_mutex;
void my_test_func(boost::asio::ip::tcp::socket* socket, boost::asio::io_service *io) {
while(1) {
    boost::this_thread::sleep(boost::posix_time::microsec(rand()%1000));

    //my_test_mutex.lock(); // It would be safer 
    socket->async_send(scatter_buffer, boost::bind(&mycallback));
    //my_test_mutex.unlock(); // It would be safer
}
}
int main(int argc, char **argv) {

for(int i = 0; i < 256; ++i)
    databuffer[i] = i;

for(int i = 0; i < 4*90; ++i)
    scatter_buffer.push_back(boost::asio::buffer(databuffer));
boost::asio::io_service my_test_ioservice;
boost::asio::ip::tcp::socket my_test_socket(my_test_ioservice);
boost::asio::ip::tcp::resolver my_test_tcp_resolver(my_test_ioservice);
boost::asio::ip::tcp::resolver::query  my_test_tcp_query("192.168.1.10", "40000");
boost::asio::ip::tcp::resolver::iterator my_test_tcp_iterator = my_test_tcp_resolver.resolve(my_test_tcp_query);
boost::asio::connect(my_test_socket, my_test_tcp_iterator);
for (size_t i = 0; i < 8; ++i) {
    boost::shared_ptr<boost::thread> thread(
            new boost::thread(my_test_func, &my_test_socket, &my_test_ioservice));
}

while(1) {
    my_test_ioservice.run_one();
    boost::this_thread::sleep(boost::posix_time::microsec(rand()%1000));
}
return 0;

}

这是我在python中的临时服务器:

import socket
def main():
    mysocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    mysocket.bind((socket.gethostname(), 40000))
    mysocket.listen(1)

    while 1:
        (clientsocket, address) = mysocket.accept()
        print("Connection from: " + str(address))
        i = 0
        count = 0
        while i == ord(clientsocket.recv(1)):
            i += 1
            i %= 256

            count+=1
            if count % 1000 == 0:
                print(count/1000)
        print("Error!")
return 0

if __name__ == '__main__':
    main()

请注意,运行此代码可能会导致计算机崩溃。