(这是我原始问题的简化版)
我有几个线程写入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);
}
这个有效!完全可以,据我所知。客户端不会崩溃。消息到达服务器时没有乱码。它们通常交替到达,每个线程一个。有时一个线程会在另一个线程之前收到两到三条消息,但只要没有乱码并且所有消息都到达,我认为这不是问题。
我的结论:在一些理论意义上,套接字可能不是线程安全的,但它很难让它失败,我不会担心它。
答案 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)
首先考虑套接字是一个流,并且内部没有防止并发读取和/或写入。有三个不同的考虑因素。
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?
对于并发呼叫者,第一个问题可以使用互斥锁或链来解决,如果你不想缓冲工作则使用前者,如果你想要缓冲工作则使用后者。这在函数调用期间保护套接字,但对其他问题没有任何作用。
第二个问题似乎最难,因为很难看到代码内部发生的异常从两个函数执行异步。异步函数都将工作发布到套接字的io_service。
来自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();
因此,如果你为一个套接字提供多个线程,它就别无选择,只能利用多个线程 - 尽管不是线程安全的。避免此问题的唯一方法(除了替换套接字实现)是给套接字只有一个线程可以使用。对于单个插座而言,无论如何这都是你想要的(所以不要为了替换而烦恼)。
请注意,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()
重载的asio文档中的关键句子是the following:
此操作是通过对流的async_write_some函数的零次或更多次调用来实现的,被称为组合操作。程序必须确保该流完成之前,该流不执行其他任何写操作(例如async_write,该流的async_write_some函数或任何其他执行写操作的组合操作)。
据我了解,本文记录了以上许多答案的假设:
如果多个线程执行asio::async_write
,则对io_context.run()
的调用中的数据可能会交错。
也许这对某人有帮助;-)
答案 7 :(得分:0)
这取决于您是否从多个线程访问相同的套接字对象。假设你有两个线程运行相同的io_service::run()
函数。
例如,如果您同时进行读写操作,或者可能执行取消操作 从其他线程。然后它不安全。
但是,如果您的协议一次只进行一次操作。
io_service::run
并且您尝试同时执行操作 - 让我们说取消和读取操作,那么您应该使用线程。 Boost.Asio文档中有一个教程。答案 8 :(得分:-2)
我一直在进行大量的测试,并且无法打破asio。即使没有锁定任何互斥锁。
但我会建议您使用async_read
和async_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()
请注意,运行此代码可能会导致计算机崩溃。