2016年7月9日土曜日

Visual C++ CoroutineとBoost Coroutine

以前にBoost.Asioを使ってみたんですが、その時は非同期機能を使っていませんでした。非同期機能の場合、完了のコールバックを処理する必要がありますが、そのコーディングはどうしても煩雑になってしまいます。
この点に関してC#言語ではVS2013でasync機能によるサポートが行われるようになりました。この機能は非同期呼び出し時のコンテキストをコンパイラーが保持しておき、完了コールバック時にコンテキストを復元することで一続きの関数のように処理するものです。この機能は一般的にはCoroutine; コルーチンと呼ばれるようです。

実はC++言語でもコルーチンを標準に導入するべく検討がされているらしく、VS2015からサポートされています。VS2015 Update1からはプラットフォームを選ばず機能提供されています。それとは別にBoostライブラリでもBoost.Coroutineによるコルーチンが提供されています。こちらはコンパイラー側のサポートなしにアセンブラでスタックを強引に書き換えることで実現されているらしいです(詳しくはわかりません)。その影響でWindowsプラットフォームではBoost.Coroutineを使うためには/SAFESEH:NOオプションを付け安全な例外ハンドラーが存在しない旨を宣言する必要があります。

そこで、C++言語ネイティブなコルーチンが提供される環境ではそちらを、提供されない環境ではBoost.Coroutineに切り替え、ソースコードを共通化できるライブラリを用意してみました。

#pragma once
#include <future>
#ifdef USE_AWAIT
#include <experimental/resumable>
#include <boost/asio/use_future.hpp>
#else
#include <exception>
#include <memory>
#include <type_traits>
#include <utility>
#include <boost/asio/spawn.hpp>
#define co_await
#endif
namespace coroutine {
namespace detail {
#ifdef USE_AWAIT
using handler = boost::asio::use_future_t<>;
template<class Result> using result = std::future<Result>;
template<class Context, class... Args>
inline auto spawn(Context&&, Args&&... args) -> decltype(std::invoke(std::forward<Args>(args)..., boost::asio::use_future)) {
return std::invoke(std::forward<Args>(args)..., boost::asio::use_future);
}
#else
using handler = boost::asio::yield_context;
template<class Result> using result = Result;
template<class Tuple, std::size_t... Index>
inline void invoke_and_set(Tuple&& tuple, boost::asio::yield_context& handler, std::promise<void>& promise, std::index_sequence<Index...>) {
std::invoke(std::get<Index>(std::forward<Tuple>(tuple))..., handler);
promise.set_value();
}
template<class Tuple, class Result, std::size_t... Index>
inline void invoke_and_set(Tuple&& tuple, boost::asio::yield_context& handler, std::promise<Result>& promise, std::index_sequence<Index...>) {
auto result = std::invoke(std::get<Index>(std::forward<Tuple>(tuple))..., handler);
promise.set_value(result);
}
template<class Context, class... Args>
inline auto spawn(Context&& context, Args&&... args) {
auto promise = std::make_unique<std::promise<decltype(std::invoke(args..., std::declval<boost::asio::yield_context>()))>>();
auto future = promise->get_future();
boost::asio::spawn(std::forward<Context>(context), [tuple = std::forward_as_tuple(args...), promise = std::move(promise)](auto handler) {
try {
detail::invoke_and_set(std::forward<std::tuple<Args...>>(tuple), handler, *promise, std::index_sequence_for<Args...>{});
}
catch (...) {
promise->set_exception(std::current_exception());
}
});
return future;
}
#endif
}
// coroutine::invoke()が呼び出せるコルーチンの引数型
using handler = detail::handler;
// coroutine::invoke()が呼び出せるコルーチンの戻り値型
template<class Result> using result = detail::result<Result>;
/// <summary>コルーチンを呼び出します。</summary>
/// <param name="context">boost::asio::spawn()の第1引数となるため、boost::asio::io_service&amp; / boost::asio::io_service::strandなど</param>
/// <param name="callable">呼び出すコルーチン、引数はargs...の後にcoroutine::handler、戻り値はcoroutine::result&lt;Result&gt;</param>
/// <param name="args">呼び出すコルーチンの引数</param>
/// <returns>std::future&lt;Result&gt;になります。</returns>
template<class Context, class Callable, class... Args>
inline decltype(auto) spawn(Context&& context, Callable&& callable, Args&&... args) {
return detail::spawn(std::forward<Context>(context), std::forward<Callable>(callable), std::forward<Args>(args)...);
}
}
view raw coroutine.h hosted with ❤ by GitHub

これを使うとcoroutine::result<Result> func(..., coroutine::handler handler)のシグネチャを持つ関数を呼び出すことができます。サンプルはこちら。

#define _WIN32_WINDOWS _WIN32_WINDOWS_WIN7
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#define BOOST_DATE_TIME_NO_LIB
#define BOOST_ERROR_CODE_HEADER_ONLY
#define BOOST_REGEX_NO_LIB
#define BOOST_SYSTEM_NO_LIB
#include <iostream>
#include <thread>
#include <boost/asio/io_service.hpp>
#include <boost/asio/ip/tcp.hpp>
using boost::asio::io_service;
using boost::asio::ip::tcp;
#include "coroutine.h"
coroutine::result<int> run(io_service& ios, coroutine::handler handler) {
tcp::resolver resolver{ ios };
auto itor = co_await resolver.async_resolve(tcp::resolver::query{ "www.google.com", "http" }, handler);
int count = 0;
for (tcp::resolver::iterator end; itor != end; ++itor, ++count)
std::cout << itor->endpoint() << std::endl;
return count;
}
int main() {
io_service ios;
io_service::work work{ ios };
std::thread thread{ [&] { ios.run(); } };
auto result = coroutine::spawn(ios, run, ios);
std::cout << "spawned." << std::endl;
auto count = result.get();
std::cout << "completed, result count =" << count << std::endl;
ios.stop();
thread.join();
return 0;
}
view raw sample.cpp hosted with ❤ by GitHub