(これは私の元の質問の簡略版です)
ブーストasioソケットに書き込むスレッドがいくつかあります。これは問題なく非常にうまく機能しているようです。
ドキュメントには、共有ソケットはスレッドセーフではないと書かれているので( ここ 、一番下にあります)、ソケットをミューテックスなどで保護する必要があるかどうか疑問に思っています。
これ 質問 は保護が必要であると主張しますが、そうする方法についてのアドバイスはしません。
私の元の質問に対するすべての回答は、私がしていることは危険であると主張し、ほとんどの場合、書き込みをasync_writesまたはさらに複雑なものに置き換えるように促しました。しかし、これを行うのは気が進まない。なぜなら、すでに機能しているコードが複雑になり、回答者の誰も、自分たちが何について話しているのかを知っていると私に確信させなかったからだ。彼らは私と同じドキュメントを読んだようで、私と同じように推測していた。だった。
そこで、2つのスレッドから共有ソケットへのテスト書き込みを強調する簡単なプログラムを作成しました。
これがサーバーで、クライアントから受け取ったものをすべて書き出すだけです。
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;
}
これがクライアントです。クライアントは、共有ソケットにできるだけ速く書き込む2つのスレッドを作成します。
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);
}
これはうまくいきます!私の知る限り、完璧に。クライアントはクラッシュしません。メッセージは文字化けせずにサーバーに到着します。それらは通常、各スレッドから1つずつ、交互に到着します。 1つのスレッドが他のスレッドの前に2つまたは3つのメッセージを受け取ることがありますが、文字化けがなく、すべてのメッセージが到着する限り、これは問題ではないと思います。
私の結論:ソケットは理論的な意味でスレッドセーフではないかもしれませんが、失敗させるのは非常に難しいので、心配する必要はありません。
Async_writeのコードを再検討した後、パケットサイズがより小さい場合に限り、すべての書き込み操作がスレッドセーフであると確信しました。
default_max_transfer_size = 65536;
何が起こるかというと、async_writeが呼び出されるとすぐに、同じスレッドでasync_write_someが呼び出されます。何らかの形式のio_service :: runを呼び出すプール内のスレッドは、完了するまで、その書き込み操作に対してasync_write_someを呼び出し続けます。
これらのasync_write_some呼び出しは、複数回呼び出す必要がある場合(パケットが65536より大きい場合)にインターリーブできます。
ASIOは、期待どおりにソケットへの書き込みをキューに入れず、次々に終了します。 threadとinterleaveの両方の安全な書き込みを保証するために、次のコードを検討してください。
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);
}
}
これは基本的に、一度に1つのパケットを送信するためのキューを作成します。 async_writeは、最初の書き込みが成功した後にのみ呼び出され、最初の書き込みの元のハンドラーが呼び出されます。
Asioがソケット/ストリームごとに書き込みキューを自動化した方が簡単だったでしょう。
使う - boost::asio::io_service::strand
スレッドセーフではない非同期ハンドラーの場合。
ストランドは、イベントハンドラーの厳密な順次呼び出しとして定義されます(つまり、同時呼び出しはありません)。ストランドを使用すると、明示的なロックを必要とせずに(ミューテックスを使用するなど)、マルチスレッドプログラムでコードを実行できます。
タイマーチュートリアル は、おそらく頭をストランドに巻き付ける最も簡単な方法です。
この質問は次のように要約されます。
async_write_some()
が2つの異なるスレッドから1つのソケットで同時に呼び出された場合はどうなりますか
これはまさにスレッドセーフではない操作だと思います。これらのバッファがネットワーク上で出力される順序は定義されておらず、インターリーブされることもあります。特に便利な関数async_write()
を使用する場合は、バッファ全体が送信されるまで、その下にあるasync_write_some()
への一連の呼び出しとして実装されるためです。この場合、2つのスレッドから送信される各フラグメントは、ランダムにインターリーブされる可能性があります。
このケースにぶつからないように保護する唯一の方法は、このような状況を回避するようにプログラムをビルドすることです。
これを行う1つの方法は、単一のスレッドがソケットへのプッシュを担当するアプリケーション層の送信バッファーを作成することです。そうすれば、送信バッファ自体のみを保護できます。ただし、単純な_std::vector
_は機能しません。バイトを最後に追加すると、それを参照する未処理のasync_write_some()
が存在する場合でも、バイトが再割り当てされる可能性があるためです。代わりに、バッファーのリンクリストを使用し、asioのスキャッター/ギャザー機能を利用することをお勧めします。
ASIOを理解するための鍵は、どのスレッドが非同期メソッドを呼び出したかに関係なく、完了ハンドラーがio_service.run()
を呼び出したスレッドのコンテキストでのみ実行されることを理解することです。 1つのスレッドでio_service.run()
のみを呼び出した場合、すべての完了ハンドラーはそのスレッドのコンテキストでシリアルに実行されます。複数のスレッドでio_service.run()
を呼び出した場合、完了ハンドラーはそれらのスレッドの1つのコンテキストで実行されます。これは、プール内のスレッドが同じ_io_service
_オブジェクトでio_service.run()
を呼び出したスレッドであるスレッドプールと考えることができます。
複数のスレッドがio_service.run()
を呼び出す場合は、それらをstrand
に配置することで、完了ハンドラーを強制的にシリアル化できます。
質問の最後の部分に答えるには、boost::async_write()
を呼び出す必要があります。これにより、io_service.run()
を呼び出したスレッドに書き込み操作がディスパッチされ、書き込みが完了すると完了ハンドラーが呼び出されます。この操作をシリアル化する必要がある場合は、もう少し複雑なので、ストランドに関するドキュメントを読む必要があります ここ 。
最初に、ソケットがストリームであり、同時読み取りおよび/または書き込みに対して内部的に保護されていないことを考慮してください。 3つの明確な考慮事項があります。
チャットの例 は非同期ですが、同時ではありません。 io_service は単一のスレッドからのrunであり、すべてのチャットクライアント操作を非並行にします。言い換えれば、これらの問題をすべて回避します。 async_writeでさえ、他の作業を進める前に、メッセージのすべての部分の送信を内部的に完了して、インターリーブの問題を回避する必要があります。
ハンドラーは、io_serviceのrun()、run_one()、poll()、またはpoll_one()のオーバーロードを現在呼び出しているスレッドによってのみ呼び出されます。
作業を単一スレッドio_serviceに投稿することにより、他のスレッドはio_serviceで作業をキューに入れることにより、同時実行とブロックの両方を安全に回避できます。ただし、シナリオによって特定のソケットのすべての作業をバッファリングできない場合は、状況はさらに複雑になります。作業を無期限にキューに入れるのではなく、ソケット通信(スレッドではない)をブロックする必要がある場合があります。また、ワークキューは完全に不透明であるため、管理が非常に難しい場合があります。
Io_serviceが複数のスレッドを実行している場合でも、上記の問題を簡単に回避できますが、他の読み取りまたは書き込みのハンドラーから(および起動時に)読み取りまたは書き込みを呼び出すことしかできません。これにより、ブロックされないまま、ソケットへのすべてのアクセスがシーケンスされます。安全性は、パターンが常に1つのスレッドのみを使用しているという事実から生じます。しかし、独立したスレッドからの作業の投稿には問題があります-たとえそれをバッファリングしてもかまわないとしても。
strand は、非同時呼び出しを保証する方法で作業をio_serviceに投稿するasioクラスです。ただし、ストランドを使用してasync_readやasync_writeを呼び出すと、3つの問題のうち最初の問題のみが解決されます。これらの関数は、内部的にソケットのio_serviceに作業をポストします。そのサービスが複数のスレッドを実行している場合、作業は同時に実行できます。
では、特定のソケットに対して、async_readやasync_writeを同時に安全に呼び出すにはどうすればよいでしょうか。
同時呼び出し元では、最初の問題はミューテックスまたはストランドで解決できます。作業をバッファリングしたくない場合は前者を使用し、バッファリングしたい場合は後者を使用します。これにより、関数の呼び出し中にソケットが保護されますが、他の問題については何も行われません。
2番目の問題は、2つの関数から非同期に実行されるコード内で何が起こっているかを確認するのが難しいため、最も難しいように思われます。非同期関数は両方とも、ソケットのio_serviceに作業をポストします。
ブーストソケットソースから:
/**
* 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の投稿はキューに対して機能することに注意してください。これにより、ほぼ即座に戻ることができます。あなたがそれにあまりにも多くの仕事を投げるならば、あなたはいくつかの結果に対処しなければならないかもしれません。ソケットに単一のio_serviceスレッドを使用しているにもかかわらず、async_writeへの同時または非同時呼び出しを介して作業を投稿するスレッドがいくつあってもかまいません。
一方、async_readは単純です。インターリーブの問題はなく、前の呼び出しのハンドラーからループバックするだけです。結果の作業を別のスレッドまたはキューにディスパッチする場合としない場合がありますが、完了ハンドラスレッドで実行すると、シングルスレッドソケットでのすべての読み取りと書き込みがブロックされます。
[〜#〜]更新[〜#〜]
ソケットストリームの基礎となる実装の実装をさらに掘り下げました(1つのプラットフォーム用)。ソケットは、io_serviceにポストされたデリゲートではなく、呼び出し元のスレッドでプラットフォームソケット呼び出しを一貫して実行しているようです。言い換えると、async_readとasync_writeはすぐに戻るように見えますが、実際には、戻る前にすべてのソケット操作を実行します。ハンドラーのみがio_serviceに投稿されます。これは、私がレビューした例のコードによって文書化も公開もされていませんが、動作が保証されていると仮定すると、上記の2番目の問題に大きな影響を与えます。
Io_serviceに投稿された作業にソケット操作が組み込まれていないと仮定すると、io_serviceを単一のスレッドに制限する必要はありません。ただし、非同期関数の同時実行を防ぐことの重要性を強調しています。したがって、たとえば、チャットの例に従い、代わりにio_serviceに別のスレッドを追加すると、問題が発生します。関数ハンドラー内で実行される非同期関数呼び出しを使用すると、関数を同時に実行できます。これには、ストランドで実行するために、ミューテックスまたはすべての非同期関数呼び出しを再投稿する必要があります。
更新2
3番目の問題(インターリーブ)に関しては、データサイズが65536バイトを超えると、作業はasync_writeの内部で分割され、部分的に送信されます。ただし、io_serviceに複数のスレッドがある場合、最初のスレッド以外の作業のチャンクが異なるスレッドに投稿されることを理解することが重要です。これはすべて、完了ハンドラーが呼び出される前にasync_write関数の内部で発生します。実装は、独自の中間完了ハンドラーを作成し、それらを使用して最初のソケット操作を除くすべてを実行します。
これは、async_write呼び出し(ミューテックスまたはストランド)の周りのガードが、複数のio_serviceスレッドがある場合にnotソケットを保護することを意味しますand投稿するデータが64kbを超えます(デフォルトでは、これはおそらく異なる場合があります)。したがって、この場合、インターリーブガードは、インターリーブの安全性だけでなく、ソケットのねじ山の安全性にも必要です。これらすべてをデバッガーで検証しました。
ミューテックスオプション
Async_read関数とasync_write関数は、内部でio_serviceを使用して、完了ハンドラーをポストするスレッドを取得し、スレッドが使用可能になるまでブロックします。これにより、ミューテックスロックで保護するのは危険です。ミューテックスを使用してこれらの関数を保護すると、スレッドがロックに戻るときにデッドロックが発生し、io_serviceが不足します。マルチスレッドio_serviceで> 64kを送信するときにasync_writeを保護する他の方法がないことを考えると、そのシナリオでは効果的に単一のスレッドにロックされます-もちろん、並行性の問題は解決されます。
2008年11月のブースト1.37asioアップデートによると、書き込みを含む特定の同期操作は「スレッドセーフになりました」。これにより、「OSでサポートされている場合、個々のソケットでの同時同期操作」が可能になります ブースト1.37.0履歴 。これはあなたが見ているものをサポートしているように見えますが、過度に単純化された「共有オブジェクト:安全ではない」句はip :: tcp :: socketのブーストドキュメントに残っています。
複数のスレッドから同じソケットオブジェクトにアクセスするかどうかによって異なります。同じio_service::run()
関数を実行している2つのスレッドがあるとします。
たとえば、読み取りと書き込みを同時に行ったり、他のスレッドからキャンセル操作を実行したりする場合。それなら安全ではありません。
ただし、プロトコルが一度に1つの操作しか実行しない場合。
io_service::run
を実行しているスレッドが複数あり、操作を同時に実行しようとする場合、たとえば、キャンセルして操作を読み取る場合は、ストランドを使用する必要があります。 Boost.Asioのドキュメントにこのためのチュートリアルがあります。