Сокеты и многопоточность
От: b0r3d0m  
Дата: 04.08.16 06:35
Оценка:
Привет всем.

Появилась необходимость использовать асинхронные функции boost::asio для написания клиент-серверного приложения, отправляющего и принимающего некоторые данные по сети.

Меня интересует следующее -- какие ограничения накладывает boost::asio на одновременное использование одного и того же объекта ip::tcp::socket из нескольких потоков?

Первым делом я, разумеется, направился в документацию, в которой, казалось бы, чётко сказано следующее:

Thread Safety

Distinct objects: Safe.

Shared objects: Unsafe.


Раз это написано после перечисления всех имеющихся в интерфейсе socket'а функций, я делаю логичный вывод о том, что даже одновременный вызов async_read и async_write (несмотря на фулл-дуплексность TCP) ведёт к UB:

// thread 1
sock.async_read(...); // вызывается одновременно с async_write из потока 2

// thread 2
sock.async_write(...); // вызывается одновременно с async_read из потока 1

// thread 3 (единственный поток, вызывающий run у соответствующего объекта io_service)
while (true)
{
  io_service.run();
}


Смотрел примеры в документации boost'а (например, этот) и заметил то, что они оборачивают в strand только handler'ы, а не сами вызовы функций async_write и async_read. С другой стороны, конкретно в приведённом мной примере (и, собственно, это единственный пример, содержащий одновременно сокеты и strand) у них логически не может произойти ситуации с одновременным вызовом функций одного и того же сокета. Всё начинается со start'а и лишь по результатам его деятельности вызывается следующая функция:

void connection::start()
{
  socket_.async_read_some(boost::asio::buffer(buffer_),
      strand_.wrap(
        boost::bind(&connection::handle_read, shared_from_this(),
          boost::asio::placeholders::error,
          boost::asio::placeholders::bytes_transferred)));
}

void connection::handle_read(const boost::system::error_code& e,
    std::size_t bytes_transferred)
{
  // ...

      socket_.async_read_some(boost::asio::buffer(buffer_),
          strand_.wrap(
            boost::bind(&connection::handle_read, shared_from_this(),
              boost::asio::placeholders::error,
              boost::asio::placeholders::bytes_transferred)));

  // ...
}


Да и вообще, даже если мы завернём handler'ы таких функций, как async_read и async_write, означает ли это то, что сами операции I/O (в данном случае отправки данных на сокет и получения их из буфера) не будут выполнены одновременно? Ведь в доках говорится именно про handler'ы, а не сами операции чтения-записи:

A strand is defined as a strictly sequential invocation of event handlers (i.e. no concurrent invocation)


Может быть, мне логически без разницы, могут ли несколько handler'ов вызваться одновременно друг с другом -- разделяемых ресурсов там может и не быть или быть, но под защитой какого-то собственного примитива синхронизации (например, mutex).

Вопрос гуглится, но после прочтения кучи ответов на SO (которые либо не подтверждены словами из документации, либо основаны на выяснении деталей реализации boost::asio конкретной версии) я запутался ещё больше.

Может ли кто-нибудь подсказать, как всё есть на самом деле?

Заранее благодарю.
Re: Сокеты и многопоточность
От: b0r3d0m  
Дата: 04.08.16 11:45
Оценка:
Вот из этого ответа на SO у меня сложилось впечатление, что:
— Запрещены именно одновременные вызовы всех функций сокета наподобие async_write и async_receive из нескольких потоков. Тут надо синхронизировать обращение к данным функциям либо при помощи strand'а, либо при помощи своих собственных примитивов синхронизации.
— В то время как сами хэндлеры могут быть и не обёрнуты в strand'ы.

Я прав?
Re[2]: Сокеты и многопоточность
От: smeeld  
Дата: 04.08.16 12:12
Оценка:
Здравствуйте, b0r3d0m, Вы писали:


Когда делаете async_write, то делается обычная неблокирующая запись в сокете в потоке, который делает этот async_write, и в специальную очередь помещается структура с хендлером. Эта очередь обслуживается в потоке, которые исполняет io_service::run, хендлер будет исполняться в этом потоке когда ОС вернёт в юзерспайс сигнал об отправке или приёме пакета. Если в хендлере будет ещё один async_write, то запись в сокет будет происходить уже в потоке, исполняющем хендлер, то есть в том, который прокручивает io_service::run. То же самое в остальных async* фукциях из boost::asio. Упомянутая strand-это разновидность реализации, strand_service, там имеется ещё task_io_service и win_iocp_io_service. И вообще, если чего не понятно из доков, то лучше смотреть в исходники, там всё поятно.
Отредактировано 04.08.2016 12:27 smeeld . Предыдущая версия .
Re[3]: Сокеты и многопоточность
От: b0r3d0m  
Дата: 04.08.16 18:44
Оценка:
Спасибо за объяснение, но что мне эти детали дают как пользователю класса ip::tcp::socket? Ведь то, что

Когда делаете async_write, то делается обычная неблокирующая запись в сокете в потоке, который делает этот async_write, и в специальную очередь помещается структура с хендлером


никак не говорит о том, как реализован конкретно boost::asio::ip::tcp::socket, и какие требования накладывает на своё использование в результате этого. Он может как требовать синхронизации, так и не требовать -- всё зависит от того, как автор(ы) библиотеки подошли к данному вопросу.
Re[4]: Сокеты и многопоточность
От: smeeld  
Дата: 04.08.16 20:15
Оценка:
Здравствуйте, b0r3d0m, Вы писали:


B>никак не говорит о том, как реализован конкретно boost::asio::ip::tcp::socket, и какие требования накладывает на своё использование в результате этого. Он может как требовать синхронизации, так и не требовать -- всё зависит от того, как автор(ы) библиотеки подошли к данному вопросу.


Никак он не реализован, это просто ООП-врайпер над сокетами libc. Упомянутый async_write-это обыкновенный write в неблокирующий дескриптор сокета, тот самый что выдаётся POSIX функцией socket (или что там в винде вместо этого), и последующий мониторинг событий на этом сокете в потоке io_service::run разными реакторами в зависимости от ОС. Это лиуксовый реактор, обратите внимание на метод start_op, через этот метод реализованы все функции работы с сокетами различных типов в asio. Все правила работы с POSIX сокетами и из свойства естественным образом переносятся на сокеты в boost::asio.
Отредактировано 04.08.2016 20:23 smeeld . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.