あめだまふぁくとりー

Boost.Graphとかできますん

iTerm2 上の Neovim だと Ctrl + backslash が効かない

iTerm2 で Neovim を再起動するとなぜか Ctrl + backslash が効かなくなるため,これまで Mac 標準のターミナルを今まで使用していたのだが,iTerm2 の設定でうまく回避することができた.

iTerm2 の Key Bindings の設定で,「Send Hex Codes」Action を「^¥」に割り当て上げるだけで OK. 指定するコードは 0x1c となる. ちなみに Ctrl + backslash のコードは,insert モードで <C-v><C-\> を入力して,入力した文字の上で g8 すればわかる.

ついでに Alt + backslash は,「Send Escape Sequence」Action で \ を送信してあげれば良い.

Mac 標準のターミナルだと im-select で IME を OFF にすると,次回 ON にした際の初入力で再変換が誤発して非常にストレスフルだったので,これで安心して日本語入力ができるようになった.

vfiler.vimの乗り換えました

Neovimの設定をLuaで一新したので,使用していたプラグインも見直しを行いました. ファイラーはこれまでDefxを使用していましたが,以下の理由でvfiler.vimを採用しました.

  • Luaで設定が書けること(Luaの設定ファイル内でVimScriptを書く方法がよくわかっていないので).
  • 依存関係がNeovim内で閉じていること(Defxはpynvimが必要だったりして,新しく環境を構築するときにいろいろと面倒だったため).
  • 2画面ファイラーとして使えること(VimFilerの使いやすさが忘れられないため).

機能的に足りないところもあるのですが,view:get_item()を使うと現在のカーソルのエンティティの情報が取れるので,これでいろいろマッピングを追加しました(ドキュメントには書いていないのでもしかしらた非推奨の使い方かもしれません).

ファイル名の表示

サイドバーとして表示している場合,横幅の関係上ファイル名が省略されて表示されてしまいます.なので,Ctrl+gで現在のカーソルのエンティティの名前を表示するようにしました.

            ["<C-g>"] = function(vfiler, context, view)
                local item = view:get_item()
                print(item.name)
            end,

ファイルかディレクトリかでの処理の分岐

デフォルトだと,ディレクトリにしか効果のないアクションもあるので,カーソルのエンティティがファイルかディレクトリかを判断してアクションを変えるようにしました.

            ["o"] = function(vfiler, context, view)
                local vfiler_action = require("vfiler/action")
                local item = view:get_item()
                if item.type == "directory" then
                    if item.opened == true then
                        vfiler_action.close_tree_or_cd(vfiler, context, view)
                    else
                        vfiler_action.open_tree(vfiler, context, view)
                        -- 開いたツリーの中にカーソルが移動するので元に戻す
                        vfiler_action.move_cursor_up(vfiler, context, view)
                    end
                else
                    vfiler_action.open_by_vsplit(vfiler, context, view)
                end
            end,

ターミナルとの連携

vfiler.viimからターミナルを開いて,選択中のエンティティを入力済みにするようにしました. これにはview:get_item()の代わりにview:selected_items()を使用しています.

local open_vfiler_terminal = function(dirpath, args)
    local termname = "term://" .. vim.fn.getbufinfo("%")[1].name
    local termbufs = vim.fn.getbufinfo(termname)
    local job_id = nil
    if next(termbufs) == nil then
        vim.cmd [[
            botright new
            resize 15
        ]]
        job_id = vim.fn.termopen({vim.opt.shell:get()}, { cwd = dirpath })
        vim.cmd("keepalt file " .. termname)
    else
        local termbufinfo = termbufs[1]
        if termbufinfo.hidden == 0 then
            -- 表示済みの場合は表示中のWindowにフォーカスする
            local wids = vim.fn.win_findbuf(termbufinfo.bufnr)
            if next(wids) ~= nil then
                vim.fn.win_gotoid(wids[1])
            end
        else
            -- 非表示の場合は画面分割で開く
            vim.cmd("botright sbuffer " .. termbufinfo.bufnr)
            vim.cmd [[resize 15]]
        end
        job_id = termbufinfo.variables.terminal_job_id
        -- VFilerで開いているディレクトリに移動する
        vim.fn.chansend(
            job_id,
            vim.api.nvim_replace_termcodes("<C-U>", true, true, true)
            .. " cd " .. vim.fn.shellescape(dirpath)
            .. vim.api.nvim_replace_termcodes("<CR>", true, true, true))
    end
    if args ~= "" then
        vim.fn.chansend(
            job_id,
            args .. vim.api.nvim_replace_termcodes("<C-A>", true, true, true))
    end
    vim.cmd [[startinsert]]
end
            ["H"] = function(vfiler, context, view)
                local selected_items = view:selected_items()
                local args = ''
                for _, item in pairs(selected_items) do
                    if item.selected then
                        args = args .. ' ' .. vim.fn.shellescape(item.path)
                    end
                end
                vfiler_action.clear_selected_all(vfiler, context, view)
                open_vfiler_terminal(selected_items[1].parent.path, args)
            end,

Sphinx における日本語文書の不自然な空白を除去する拡張を公開した

Sphinx で日本語文章を書いたドキュメントを HTML として表示すると, 改行箇所やインラインマークアップの前後で空白が表示されてしまいます. 今までは japanesesupport.py を使用していたのですが, pip でインストールできないのが面倒くさかったので同機能の拡張を自作してパッケージ化しました.

pypi.org

japanesesupport.py には色々問題があったため, 一から自作しています.

半角文字間の空白の維持

japanesesupport.py では英単語間で改行を挟むとその英単語間の空白まで除去されてしまいます.

sphinxcontrib-trimblank では除去対象の空白の前後が半角かどうかチェックするようにしています. ASCII コードの範囲だけでチェックすると必要以上に空白が消えてしまうので, 全角となる Unicode の文字範囲を適当に調べて実装しています (この点は改善の余地があると思います).

パラグラフ以外での空白の除去

japanesesupport.py は Docutils の paragraph ノード 直下のテキストしか空白除去の対象にしていません. このため, インラインマークアップ中の改行等が除去されません.

sphinxcontrib-trimblank はテキストを包含できる他のノードも対象にいれています (もちろん, literal_block 等の改行を維持するノードは対象外にしています).

改行とインラインマークアップ前後以外の空白の除去

私は Sphinx で文章を書く場合は日本語と半角英数字との間に空白を入れるようにしているのですが, 以下のような文章を japanesesupport.py で処理すると二つ目の japanesesupport.py の直前の空白は残りますが直後の空白は消えてしまいます.

japanesesupport.py でこの文章を処理すると, 二つ目の japanesesupport.py
の直前には空白があるのに対し, 直後には空白がない文章が生成されてしまう.

f:id:amedama41:20190811173058p:plain
japanesesupport.py での処理結果

sphinxcontrib-trimblankでは一貫性のある文章を出力するため,改行とインラインマークアップ前後以外の空白も除去します.

f:id:amedama41:20190811173116p:plain
sphinxcontrib-trimblank での処理結果

ただし, HTML など特定種別のドキュメントでは日本語と半角英数字との間の空白だけは除去したくない人もいるかと思います. その場合, conf.py に以下のオプションを追加することで日本語と半角英数字間の空白を維持できます.

# html, singlehtml でビルドする場合は半角英数字との間の空白は維持する
trimblank_keep_alnum_blank = ['html', 'singlehtml']

f:id:amedama41:20190811173125p:plain
日本語と半角英数字間の空白を維持した場合の処理結果

ドキュメントの種別に関わらず, 維持したい場合はオプションの値をリストの代わりに True を指定できます.

CentOS7でPlantUMLのレイアウトが上手く制御できない

CentOS7でPlantUMLサーバを立ち上げて見たのですが,ユースケース図やクラス図の配置が上手く制御できませんでした. 結論としてGraphvizのバージョンを上げることで解決できました.

制御が正しく機能しない例

PlantUMLでは,要素間の関係にleft, right, up, downを指定することで要素間の位置関係をコントロールできます. 例えば,以下のコードをPlantUML Web Serverコンパイルすると次の図が生成されます.

@startuml
A -up- A1
A -right- A2
A -down- A3
A -left- A4
@enduml

f:id:amedama41:20190720095046p:plain
出力例(正しいレイアウト)

しかし,CentOS7で立ち上げたPlantUMLサーバでは同じコードでも以下の図が生成されてしまいました.

f:id:amedama41:20190720095606p:plain
出力例(誤ったレイアウト)

どうやら,leftrightが正しく動作していないようです.A - A2のように関係に一つのハイフンを使った場合も同様です(ハイフン一つだと-right-と同じ意味になります).

環境の確認

公式のサーバと自前のCentOS7環境で出力結果が異なる原因を探るため,動作環境の確認をしました. 以下のコードでPlantUMLとGraphvizのバージョンを確認できます.

@startuml
version
@enduml

以下の表に確認結果を載せます.

自前環境 公式サーバ
PlantUML 1.2019.06 1.2019.08
Graphviz 2.30.1 2.38.0
JRE 1.8.0 1.7.0

自前環境ではPlantUMLとGraphvizのバージョンが古いことがわかりました. PlantUMLはMavenでインストールしたものを,GraphvizJREはCentOS7のデフォルトのリポジトリからインストールしたものを使っていました.

まず,PlantUMLのバージョンを1.2019.08に更新してみましたが結果は変わりませんでした. なので,次にGraphvizを更新してみました.

Graphvizの更新

CentOS7のデフォルトのリポジトリには2.30.1しかありません. Googleで検索すると,過去にGraphvizの公式がRHEL用のGraphvizリポジトリを提供していたようですが,現在はリポジトリの定義ファイルはリンク切れになっていました. なので,ソースコードから自前でビルドすることにしました.Graphvizの公式ダウンロードページからは2.40.1のソースしかダウンロードできず,PlantUMLのFAQには2.40では問題が出る可能性があると書いてあるので,GtiLabからソースを持ってきました.

# ソースコードをクローン
git clone https://gitlab.com/graphviz/graphviz.git
# tagがないのでログで2.38のコミットハッシュを検索してcheckout
cd graphviz
git checkout -b v2.38 f54ac2c
# PlantUMLの連携にlibexpatが必要 (FAQ参照)
sudo yum install expat-devel
# Graphviz公式ページ記載の手順でビルド.今回は/opt/graphvizにインストールする
./autogen
./configure --prefix=/opt/graphviz
make
sudo make install

あとは,PlantUMLサーバがアップデートしたGraphvizを使うようにすれば完了です.

mvn jetty:run -DGRAPHVIZ_DOT=/opt/graphviz/bin/dot

この状態で,問題となったコードをコンパイルすると公式と同じ結果を出力できるようになりました.

まとめ

CentOS7のGraphvizは古いのでPlantUMLが期待通りの結果を出力してくれません. Graphvizを自前でビルドしてバージョンアップするか,CentOS以外のOSを使ってやるとよいです.

sphinx-docxbuilderを復活させた

f:id:amedama41:20190407155426p:plain

どうしても Office Word を使いたくなかったので, sphinx-docxbuilder を復活させまし た. ソースは GitHub にあげています.

最初はちょっと手を入れる程度だったのですが, テーブル周りに色々問題があったりで修正しすぎてソースコードの原型はほとんどなくなりました.

インストール

pip でインストールできます

pip install docxbuilder

使い方

conf.pyextensions 設定に "docxbuilder" を追加して, make docx を実行するだけです.

設定

ドキュメントのファイル名等は docx_documents 設定で行います.

# index.rst をドキュメントのルートとし, index.rst の TOC ツリー以外は除外する
# 出力ファイル名は docxbuilder.docx で title, creator, subject プロパティを設定する.
docx_documents = [
  ('index', 'docxbuilder.docx', { 'title': 'Title', 'creator': 'Author', 'subject': 'A manual of docxbuilder', }, True),
]

設定値はタプルのリストになっており, タプルの最初の値はルートとなるドキュメントファイル (通常は index), 二つ目は出力ファイル名, 三つ目はドキュメントのプロパティ, 四つ目はルートドキュメントファイルの TOC ツリー以外の要素を除外するかどうかを示します.

デフォルトスタイルファイルの表紙は title, creator, subject プロパティを参照しているので, この三つのプロパティは指定しておくとよいです.


スタイルファイルは docx_style 設定で指定できます.

docx_style = 'path/to/custom_style.docx' # カスタムスタイルファイルを指定する.

Word のスタイルはパラグラフを分割させないような指定もできるのですが, スタイルファイルで指定するのは面倒なので, パラグラフや図表の配置は docxbuilder の中でできるだけいい感じになるように頑張っています. 表については docx_table_options 設定や特定のクラスを指定することで, 表が複数のページに分割されないようにしたり, 表を横長のページに配置することも可能です.


docx_coverpage 設定で表紙を追加するかどうか指定します.

docx_coverpage = True # 表紙を挿入

表紙は自動生成するのではなく, スタイルファイルの表紙が挿入されます. また, 表紙の定義は "Cover Pages" タグが指定されたページなのですが, このタグを明示的に指定する方法は多分ないので, 自前の表紙を作る際は Word の表紙挿入機能でプリデザインの表紙を挿入して, それを編集する必要があります.


docx_pagebreak_before_section 設定で,セクションタイトルの前で改ページすることが可能です.

docx_pagebreak_before_section = 2  # セクションレベルが 2 以下 (章と節) のタイトルはページの先頭に配置

指定した数値以下のセクションレベルを持つタイトルはページの先頭に配置されるように改ページされます. 最小のレベルは 1 です. デフォルトの設定値は 0 なので一切改ページされません.


目次は最初のセクションの前にある場合もあり, 目次の後に改ページしたいかもしれません. その場合は, docx_pagebreak_after_table_of_contents 設定を使用します.

docx_pagebreak_after_table_of_contents = 0 # セクションレベル 0 以下のセクションに登場する目次の後で改ページ

表の配置については docx_table_options 設定を使用します.

docx_table_options = {
  'landscape_columns': 6,      # 列数が 6 以上の表は横長のページに配置
  'in_single_page': True,      # 可能な限り表が複数のページに分割されないようにする
  'row_splittable': True,      # セルが複数ページに跨ってもよい
  'header_in_all_page': True,  # 表が複数ページに分割される場合, すべてのページに表のヘッダーを表示する
}

この設定はすべての表に適用されます. 個別に設定したい場合は表にクラスを指定します (後述).

ドキュメントのカスタマイズ

ドキュメントのスタイル

ドキュメントの見た目はスタイルファイルを使って変更可能です. スタイルファイルは Word のファイルで, ドキュメントに適用するスタイルや目次, ページ設定 (余白やヘッダ等) を定義します.

docxbuilder で使用するスタイルには文字スタイル, パラグラフスタイル, 表スタイルの三種類のスタイルがあります.

主に使用するスタイルは こちら を参照してください. docxbuilder はスタイル名 (Emphasis, Body Text 等) とスタイル種別 (文字, パラグラフ, 表) をチェックしてスタイルを適用するので, この二つの定義は間違えないようにしてください.

また, List Bullet と List Style は箇条書きに適用されるスタイルで, このスタイルにはアウトラインの定義が必須です.

クラスによるカスタマイズ

図表が横長になる場合, 横長のページにそれらを配置したくなるケースがあります. その場合, docx-landscape クラスを指定するとよいです.

.. rst-class: docx-landscape
.. list-table::

   * - cell1-1
     - cell1-2
   * - cell2-1
     - cell2-1

図に使用できるクラスは docx-landscape クラスだけですが, 表には他にも docx-in-single-page, docx-row-splittable, docx-header-in-all-page が使用可能です (効果は docx_table_options のものと同じです).

今後の予定

現状数式は latex の数式そのままが出力されるので, Word の数式として出力したいです (これ を使って latex 数式を MathML に変換して, MathML を OMML に変換する予定).

Networking TS の Boost.Asio からの変更点 - その 4: Associated Executor

はじめに

Boost 1.66 で Boost.Asio が Networking TS に対応するようです. なのでその 3 からかなり間が空きましたが, その 4 の記事を書きます. 前回 Executor について説明したので, 今回は Associated Executor について説明します.

Boost.Asio でのハンドラ呼び出し

Boost.Asio では非同期処理完了時のハンドラ呼び出しに asio_handler_invoke という非メンバ関数を使用することになっています. この関数はデフォルトでは引数で受け取った関数オブジェクトを単に呼び出すだけです.

template <class Function>
void asio_handler_invoke(Function& function, ...) {
  function();
}

ただし, この関数も asio_handler_allocate 等と同様, 引数として別にハンドラを指すポインタを取るので, 関数の呼び出し方をカスタマイズすることができます.

Strand では実際に asio_handler_invoke をフックすることで, ハンドラが並列に実行されないことを担保しています.

template <class Function, class Handler>
void asio_handler_invoke(
      Function& function
    , wrapped_handler<strand, Handler>* handler) {
  // wrapped_handler は strand.wrap(handler) で得られるハンドラの型
  handler->strand.dispatch(function); // 実際はもう少しいろいろなことをやる
}

Networking TS でのハンドラ呼び出し

Networking TS では asio_handler_invoke は廃止し, 代わりに Associated executor を使用します.

Associated executor は その 1 で説明した Associated allocator と同様にハンドラごとに関連付けられた Executor です.

Associated executor はやはり Associated allocator と似たように, associated_executor を使用して取得できます.

my_handler h;
auto exec = associated_executor<my_handler>::get(h);

Networking TS では非同期処理完了時にこの Associated executor にハンドラを post / dispatch します. なので, 独自に Executor を使用するようにすることで, asio_handler_invoke と同様のことを実現できます. Associated executor が strand なら, ハンドラは呼び出される前に strand.post / dispatch されます.

Executor のハンドラへの関連付け

既存の Executor をハンドラに関連付けたい場合は, bind_executor を使用します.

bind_executor は前回の紹介では strand.wrap に相当するものと説明しましたが, 実際はハンドラに Executor を関連付けること以上のことは行いません.

これはどうゆうことかというと, bind_executor の結果の関数オブジェクトを関数呼び出し演算子で呼び出しても, 関連付けた Executor を通してハンドラを実行しないということです.

この差については少し注意したほうがよさそうですが, 実際はユーザが直接関数呼び出し演算子で呼び出すことはあまりないと思いますので, あまり気にすることもないかと思います.

auto wrapped_handler = strand.wrap(handler);
wrapped_handler(); // strand の中で実行

auto bind_handler = bind_executor(strand, handler);
bind_handler(); // その場で実行. strand の中では実行しない.

Associated executor への変更による影響

Networking TS では非同期処理完了時にハンドラは Associated executor に dispatch, または post されます. 前回説明したように Executor の dispatch / post はハンドラをそのまま実行します (Boost.Asio では asio_invoke_handler はさらに asio_handler_invoke を呼び出す場合があります).

これはどういうことかというと, ハンドラが入れ子になっている場合, 内部の Associated executor は無視されるということです.

下記のコードを例にして説明します.

auto bind_h
  = bind_executor(strand1, bind_executor(strand2, h));
socket.async_read(buffer, bind_h);

このコードでは strand1 と strand2 を二重にバインドして, ソケットからの非同期読み出しを行なっています. bind_h の外側の Associated executor は strand1 であり, 内側は strand2 です. つまり, bind_h の直接の Associated executor は strand1 になります.

そのため, 読み出しが完了すると, strand1.dispatch(func) が実行され, strand1 の中で func() が呼び出されます (func は std::bind(bind_h, read_size, error) に相当する関数オブジェクト). 上記で説明した通り, bind_executor の戻り値の関数オブジェクトは, バインドした Executor を無視してハンドラを呼び出します. よって, bind_executor(strand1, bind_executor(strand2, h))() は即座に bind_executor(strand2, h)() を実行し, それは即座に h() を実行することになります.

つまり, 上記のコードではハンドラは strand1 の中で実行されることになります.

Boost.Asio の場合は, asio_handler_invoke が連鎖する設計になっていたため, 以下のコードでは strand2 の中で実行されます.

auto wrapped_h = strand1.wrap(strand2.wrap(h));
socket.async_read_some(buffer, wrapped_h);

Networking TS と Boost.Asio では最終的に使用される Executor が異なるので注意する必要があります.

Networking TS の改善点

Networking TS では, executor::dispatch / post とbind_executor の設計を見直し, Executor のネストを無視するようにしました. これにより, Boost.Asio に比べて速度の面と安全性の面で改善された箇所があるので, 以降はそれらについて説明します.

Strand の効率化

Boost.Asio の場合

Boost.Asio で strand でラップしたハンドラが非同期処理完了時に呼び出される流れは以下のようになります (ここでは strand.wrap(handler) の型を wrapper とします).

  1. wrapper 用の asio_handler_invoke の中で strand にラップした関数オブジェクトを dispatch.
  2. strand のコンテキストで asio_handler_invoke 経由で wrapper::operator() を実行. strand に handler を dispatch する.
  3. strand のコンテキストで asio_handler_invoke 経由で handler を実行.

このように, 1 と 2 の処理で無駄に一回多く dispatch を行っています.

また, 2 の処理で呼び出す asio_handler_invoke を 1 で呼び出したものと違うものにするため, 実際には 1 の dispatch 時に関数オブジェクトをリラップして関数オブジェクトの型を変更しています. ラップしない場合, 2 で 1 と同じ asio_handler_invoke が呼び出され, 無限再帰になってしまいます.

Networking TS の場合

Networking TS ではこれらが改善され, Strand を通した呼び出しが少しだけ効率的になります.

  1. Associated executor である strand にメンバ関数 dispatch を使用して dispatch.
  2. strand のコンテキストで元のハンドラを実行.

Networking TS では executor_binder::operator() は単に元のハンドラを呼び出すだけなので, 不要な dispatch は発生しませんし, リラップの必要性もありません.

パフォーマンス測定

実際にどれくらい改善されたか測定します. Boost.Asio でも以下のように strand::dispatch をラムダ式の中で呼び出せば, Networking TS と同等の改善が得られます (注意:この方法は async_write 等の composed operation と一緒に使用できないので, プロダクトコードで使用するのはおすすめしません).

socket.async_read_some(buffer, [&] { strand.dispatch(handler); });

測定用のコードは Gist に置いてあります.

ここでは, あらかじめ io_service に 10,000,000 個のハンドラを post しておき, それらがすべて実行されるまでの時間を測りました (使用したコンパイラは clang でオプションは -std=c++11 -stdlib=libc++ -pedantic -Wall -O3 です).

f:id:amedama41:20171209101405p:plain

わずかに改善されているのがわかりますね.

安全性の改善

Strand の効率化の説明で見た通り, Boost.Asio の Strand 用の asio_handler_invoke は無限再帰呼び出しを回避するため注意深く設計されています. ユーザ自身が asio_handler_invoke を定義する場合も, このことに注意して設計, 実装する必要があります.

しかしながら, これは意外と容易ではありません. Boost.Asio では様々な関数が asio_handler_invoke を使い, 様々なクラスが自身の asio_handler_invoke をフックしています. それらの関係図全体が見えていないと, 安全な asio_handler_invoke を設計できたとは自信を持って言うことはできないでしょう.

Networking TS では, Associated executor を使用する関数 (非メンバ関数版の dispatch / post) と使用しない関数 (メンバ関数版の dispatch / post) を分離し, 後者を呼び出すことで再帰を発生しないようにしました.

ユーザは周りに気にせず安全に自身の Associated executor を実装できるようになります.

まとめ

Networking TS では Boost.Asio の asio_handler_invoke を Executor に置き換えました.

また, Executor のネストを無視するように設計変更したことで, Strand の効率や, ユーザによるカスタマイズの安全性を改善しました.

とりあえず, Networking TS の Boost.Asio からの変更点については, その 4 までで書きたいことは書けたと思いますので, 本シリーズは今回で終了です.

Networking TS の Boost.Asio からの変更点 - その 3: Executor

はじめに

前回までに説明した Associated allocator や async_result は少し地味目な変更点でした. 今回説明する Executor は Boost.Asio から Networking TS の中でも大きめの変更になっています.

一般化された io_service と strand

Executor は Networking TS で明示された概念であり, 簡単にいうと io_service と strand で共通するインタフェース部分のことです. つまり, ハンドラを実行する dispatch や post, それらを関数呼び出しの形で行うための wrap がそれに該当します.

Executor はハンドラをどのように呼び出すか定義するもので, 例えば strand の場合は複数のハンドラを直列に呼び出すように制御します.

io_service には Executor の機能の他にも, ハンドラを実際に実行する run などの関数があります. これらはハンドラをどこで実行するかを定義するものとみなすことができます. そのため, これを ExecutionContext と Networking TS では呼びます. ExecutionContext にはその ExecutionContext にハンドラを追加するための Executor が必ず対応付けられています.

io_service は Executor と ExecutionContext という概念に分割されたことで, io_context に名前が変わりました.

/* Boost.Asio */
io_service io_svc{};

/* Networking TS */
io_context io_ctx{};
io_context::executor_type executor = io_ctx.get_executor();

これらの一般化の裏には io_service 以外の実行コンテキストも使えるようにするといった方針があると思われます. 実際に Networking TS では io_context の他にも, 暗黙的に別スレッドでハンドラを実行する system_context と system_executor が定義されています.

また, ジェネリックプログラミングを容易にするという目的もあるかもしれません. これまでは io_service はコピーができない等 strand と異なる部分があったため, これらを取り替えるコードを書くには手間がありました. Networking TS では Executor はコピー可能と定義されているため, 任意の Executor を使用するコードは容易に書けます.

実際に Executor 自身もこの一般化によってメンバ関数の一部を非メンバ関数に移動することができています. 以下ではメンバ関数の変更点を見ていきます.

dispatch / post の非メンバ関数

Boost.Asio では io_service や strand のメンバ関数 dispatch と post は他の非同期関数と同様の振る舞いをしていました. 具体的には,

  • CompletionToken を引数に取り, async_result で戻り値を決めハンドラに変換する,
  • メモリを割り当てる場合は, asio_handler_allocate/deallocate を使用する,
  • ハンドラを呼び出す場合は, asio_handler_invoke を通して呼び出す,

ということをしていました.

Networking TS ではこれらの振る舞いはすべて変更され,

  • CompletionToken ではなくハンドラとしての関数を引数に取り, 戻りの型は常に void である,
  • メモリを割り当てる場合は, 第二引数で渡されたアロケータを使用する,
  • ハンドラの呼び出す場合は, h(args) の形でそのまま呼び出す,

となっています.

Boost.Asio 版と同じ振る舞いをさせたい場合は, 非メンバ関数版の dispatch, post を使用します.

/* Boost.Asio */
auto fut0 = io_service.post(use_future); // OK.

/* Networking TS */
auto fut1 = executor.post(use_future); // NG: CompletionToken は変換されず, 
                                       // 戻り値の型は void.
                                       // さらに第二引数も必要.
auto fut2 = post(executor, use_future); // OK.

メンバ関数版はハンドラを右辺値 (xvalue or prvalue) として受け取れればよいとなっているので, メンバ関数は他の非同期関数から間接的に使用されるものであり, 通常は非メンバ関数を使うと思っていいでしょう.

この変更がどう影響してくるのかは次回に説明します.

defer の追加

また, Networking TS では dispatch と post の他に defer という関数が追加されています. これは機能的には post と全く同じですが, defer は処理を遅延させたいことを明示させるために存在するようです. おそらく, Boost.Asio の asio_handler_is_continuation 辺りの機能が関連しているんではないかと思います.

wrap の非メンバ関数

strand::wrap は知っているかもしれませんが, 実は io_service::wrap も存在していました. Executor に一般化したことで wrap はメンバ関数である必要性がなくなり, bind_executor と呼ばれる非メンバ関数に変更されました. また, 戻り値の型も未指定から executor_binder クラステンプレートの実体になりました.

/* Boost.Asio */
auto f0 = io_service.wrap([]{ std::cout << "asio"; });
f0(); // io_service のコンテキストで呼び出される

/* Networking TS */
auto f1 = bind_executor([]{ std::cout << "Networking TS"; }, executor);
f1(); // executor のコンテキストで呼び出され...?

bind_executor で生成された関数オブジェクトは Boost.Asio の wrap で生成したものと振る舞いが違う点もあり, 同じと思って使用すると期待どおりの結果にならない場合もあります. この点の説明も次回に回します.

strand の一般化

io_service 等の概念を Executor に一般化したことで, strand も一般化されます. Boost.Asio では strand は io_service の実行コンテキストにおいて, ハンドラが直列に実行されることを保証しますが, Networking TS では任意の Executor (に関連する ExecutionContext) に対しての保証をします.

/* Boost.Asio */
io_service::strand s0{io_svc};

/* Networking TS */
strand<io_context::executor_type> s1{io_ctx.get_executor()};

io_service 以外にも strand を使用することができるようになりましたが, それによって strand の振る舞いの一部も変更されています.

strand::dispatch がハンドラを即座に呼ぶ条件

Boost.Asio での strand::dispatch は

  1. io_service::run 等が実行されているスレッドで呼び出された, かつ,
  2. この strand に追加されているハンドラが同時に実行されない状況にある,

という二つの条件を満たす場合, 引数で渡されたハンドラを dispatch の呼び出の中で実行します.

この二つ目の条件は, Boost.Asio のドキュメントには「strand に追加したハンドラの中から dispatch を呼び出した場合」と記載されていますが, 他にも strand にハンドラが一つも追加されていない場合でも満たされます.

Networking TS ではこの後者の場合はなくなります. strand が一般化されたことで, 一つ目の条件を満たされているか判断できなくなったためです (前者の場合が満たされている場合については, 自動的に一つ目の条件も満たされるので判断は不要です). Boost.Asio では strand は io_service が密に結合することで, 前者の条件判定を可能にしていました.

内部ハンドラのメモリ割り当て

strand は, strand に追加されたハンドラを実行するための内部ハンドラを一つだけ保持し, その内部ハンドラをベースとなる Executor に追加することで直列実行を実現しています. このため, ベース Executor がそのハンドラを保持しておくためのメモリ割り当てが必要となります.

Boost.Asio では strand と io_service が密に結合していたため, ハンドラ用のメモリ割り当ては strand オブジェクトを生成したときだけで十分でした (必要なメモリのサイズ, データ構造を strand が知っていた).

Networking TS では strand は任意の Executor に対応する必要があるので, 内部ハンドラ追加のたびにメモリ割り当てが必要です. このことから

  • 動的にメモリを割り当て / 解放するためのコスト,
  • メモリ割り当て失敗による例外に対する例外安全性,

といった課題が予想されます.

幸い, 現状の Networking TS の実装は一度割り当てたメモリをできるだけ再利用するので, 割り当てコストは小さく, 例外が投げられる可能性が発生する機会も多くないようです.

まとめ

Networking TS では io_service と strand の共通部分は Executor に, io_service の残りの部分は ExecutionContext に一般化されました. それに伴い, 各メンバ関数が非メンバ関数になったり, それらの機能の振る舞いにも変更が入りました.

また, io_service を前提としていた strand は Executor に対して一般化されました. 本記事では紹介しませんでしたが, 他にも io_service::work が一般化されていたりします.

Boost.Asio から Networking TS に移行する場合はこれらの変更点がソースコード上に明確に現れることでしょう.

次回は Associated Executor の説明をして, 本記事で説明した dispatch や bind_executor がどう影響するのかを見ていこうと思います.