当服务器线程尝试解锁不属于它的read_lock 时,程序正在调用未定义的行为。
int main()
{
...
std::mutex read_mutex;
std::unique_lock<std::mutex> read_lock(read_mutex); // Acquired by main.
std::condition_variable read_done;
std::thread server([&]() { // Capture lock reference.
std::unique_lock<std::mutex> lock(mutex);
...
// The next line invokes undefined behavior as this thread does did
// not acquire read_lock.mutex().
if (read_done.wait_for(read_lock, ...)
// ^^^^^^^^^ caller does not own.
{
...
}
});
signal.wait(lock);
...
}
特别是在调用condition_variable::wait_for(lock) 时,标准要求lock.owns_lock() 为真并且lock.mutex() 被调用线程锁定。
混合同步和异步流通常会增加复杂性。在这种特殊情况下,同步调用在每一层中交织在一起,使用较低级别的结构进行事件/信号通知而没有持久状态,我认为它增加了不必要的复杂性并使流程过于复杂。此外,广泛的变量范围会增加复杂性。如果 read_lock 从未被 lambda 捕获,则会发生编译器错误。
在尝试观察两个事件时考虑空间分离:
// I will eventually be interested when the server starts
// accepting connections, so start setting up now.
std::mutex server_mutex;
std::unique_lock<std::mutex> server_lock(server_mutex);
std::condition_variable server_started;
std::thread server([&]()
{
// I will eventually be interested when the server reads
// data, so start setting up now.
std::mutex read_mutex;
std::unique_lock<std::mutex> read_lock(read_mutex);
std::condition_variable read_done;
listener.async_accept(...,
[&](...)
{
// Got connection.
async_read_until(...,
[&](...)
{
// Someone may be interested that data has been read,
// so use the correct mutex and condition_variable
// pair.
std::unique_lock<std::mutex> read_lock(read_mutex);
read_lock.unlock();
read_done.notify_one();
});
}); // async_accept
// Someone may be interested that I am accepting connections,
// so use the correct mutex and condition_variable pair.
std::unique_lock<std::mutex> server_lock(server_mutex);
server_lock.unlock();
server_done.notify_one();
// I am now interested in if data has been read.
read_done.wait_for(read_lock);
}); // server thread
// I am now interested in if the server has started.
server_started.wait(server_lock);
调用者必须准备处理一个事件,开始一个操作,然后等待事件,并且操作必须知道调用者感兴趣的事件。为了使情况更糟,现在必须考虑锁排序以防止死锁.请注意在上面的示例中,服务器线程如何获取read_mutex,然后获取server_mutex。另一个线程不能以不同的顺序获取互斥锁,而不会引入死锁的机会。就复杂性而言,这种方法在事件数量上的扩展性很差。
可能值得考虑重新检查程序的流程和控制结构。如果它可以写成主要是异步的,那么回调链、延续或信号槽系统 (Boost.Signals) 可能会使解决方案变得简单。如果一个人更喜欢像同步读取异步代码一样,那么 Boost.Asio 对coroutines 的支持可以提供一个干净的解决方案。最后,如果需要同步等待异步操作的结果或超时,请考虑使用 Boost.Asio 的support for std::future 或直接使用它们。
// Use an asynchronous operation so that it can be cancelled on timeout.
std::future<std::size_t> on_read = boost::asio::async_read_until(
socket, buffer, '\n',boost::asio::use_future);
// If timeout occurs, then cancel the operation.
if (on_read.wait_for(std::chrono::seconds(1)) == std::future_status::timeout)
{
socket.cancel();
}
// Otherwise, the operation completed (with success or error).
else
{
// If the operation failed, then on_read.get() will throw a
// boost::system::system_error.
auto bytes_transferred = on_read.get();
}
虽然我强烈主张重新检查整体控制结构并减少变量范围,但以下示例与上述示例大致相同,但使用std::future 可能更易于维护:
// I will eventually be interested when the server starts
// accepting connections, so start setting up now.
std::promise<void> server_started_promise;
auto server_started = server_started_promise.get_future();
std::thread server([&]()
{
// I will eventually be interested when the server reads
// data, so start setting up now.
std::promise<void> read_done_promise;
auto read_done = read_done_promise.get_future();
listener.async_accept(...,
[&](...)
{
// Got connection.
async_read_until(...,
[&](...)
{
// Someone may be interested that data has been read.
read_done_promise.set_value();
});
}); // async_accept
// Someone may be interested that I am accepting connections.
server_started_promise.set_value();
// I am now interested in if data has been read.
read_done.wait_for(...);
}); // server thread
// I am now interested in if the server has started.
server_started.wait();
下面是基于demonstrates使用std::future以同步方式控制流和超时异步操作的原始代码的完整示例:
#include <future>
#include <iostream>
#include <thread>
#include <boost/asio.hpp>
#include <boost/asio/use_future.hpp>
#include <boost/optional.hpp>
#include <boost/utility/in_place_factory.hpp>
int main()
{
using boost::asio::ip::tcp;
// Setup server thread.
boost::asio::io_service server_io_service;
std::promise<tcp::endpoint> server_promise;
auto server_future = server_promise.get_future();
// Start server thread.
std::thread server_thread(
[&server_io_service, &server_promise]
{
tcp::acceptor acceptor(server_io_service);
acceptor.open(tcp::v4());
acceptor.set_option(
boost::asio::socket_base::enable_connection_aborted(true));
acceptor.bind(tcp::endpoint(tcp::v4(), 0));
acceptor.listen();
// Handlers will not chain work, so control the io_service with a work
// object.
boost::optional<boost::asio::io_service::work> work(
boost::in_place(std::ref(server_io_service)));
// Accept a connection.
tcp::socket server_socket(server_io_service);
auto on_accept = acceptor.async_accept(server_socket,
boost::asio::use_future);
// Server has started, so notify caller.
server_promise.set_value(acceptor.local_endpoint());
// Wait for connection or error.
boost::system::system_error error =
make_error_code(boost::system::errc::success);
try
{
on_accept.get();
}
catch (const boost::system::system_error& e)
{
error = e;
}
std::cout << "Server got a connection " << error.code() << std::endl;
// Read from connection.
boost::asio::streambuf buffer;
auto on_read = boost::asio::async_read_until(
server_socket, buffer, '\n', boost::asio::use_future);
// The async_read operation is work, so destroy the work object allowing
// run() to exit.
work = boost::none;
// Timeout the async read operation.
if (on_read.wait_for(std::chrono::seconds(1)) ==
std::future_status::timeout)
{
std::cout << "Server read timed out -- cancelling socket jobs"
<< std::endl;
server_socket.close();
}
else
{
error = make_error_code(boost::system::errc::success);
std::size_t bytes_transferred = 0;
try
{
bytes_transferred = on_read.get();
}
catch (const boost::system::system_error& e)
{
error = e;
}
std::cout << "Got " << bytes_transferred << ", "
<< error.code() << std::endl;
}
std::cout << "Exiting server thread" << std::endl;
});
// Wait for server to start accepting connections.
auto server_endpoint = server_future.get();
std::cout << "Server set up" << std::endl;
// Client thread.
std::promise<void> promise;
auto future = promise.get_future();
std::thread client_thread(
[&server_endpoint, &promise]
{
boost::asio::io_service io_service;
tcp::socket client_socket(io_service);
boost::system::error_code error;
client_socket.connect(server_endpoint, error);
std::cout << "Connected " << error << std::endl;
promise.set_value();
// Keep client socket alive, allowing server to timeout.
std::this_thread::sleep_for(std::chrono::seconds(2));
std::cout << "Exiting client thread" << std::endl;
});
// Wait for client to connect.
future.get();
std::cout << "Client set up" << std::endl;
// Reset generic promise and future.
promise = std::promise<void>();
future = promise.get_future();
// Run server's io_service.
std::thread server_io_thread(
[&server_io_service, &promise]
{
std::cout << "About to service server IO requests" << std::endl;
try
{
server_io_service.run();
}
catch (const std::exception& e)
{
std::cout << "Exception caught: " << e.what() << std::endl;
}
std::cout << "Service jobs all run" << std::endl;
promise.set_value();
});
if (future.wait_for(std::chrono::seconds(3)) ==
std::future_status::timeout)
{
std::cout << "IO thread timed out servicing requests -- stopping it"
<< std::endl;
server_io_service.stop();
}
// Join all threads.
server_io_thread.join();
server_thread.join();
client_thread.join();
}