ROSのC#クライアントと非同期プログラミング

Reactive Extensionsを使って、ROSのC#クライアントを作りました。

ROS

ROSとは

ROS(Robot Operating System)とは、Willow Garage社が開発しているロボットアプリケーションのためのソフトウェア開発基盤です。

ROSについてざっくりと知るには、以下のページが分かりやすいです。

ROSでは、ノードと呼ばれるロボットのソフトウェアモジュールをネットワークを通じて連携させてシステムを構築するわけですが、そのためのフレームワークやビルド・パッケージングの仕組みを提供しています。

ロボット用とは言いますが、分散システムを構築するフレームワークなので、ロボット以外のアプリケーションにもいろいろと活用できそうです。

ROSのクライアントライブラリ

ROSのノード間で連携する機能としては、3種類のモデルの通信機能が用意されています。

  • Publisher/Subscriber型のメッセージ通信(Topic)
  • RPC型のメソッド呼び出し(Service)
  • データ共有機能(Parameter)

これらの通信を行うクライアントライブラリの実装指針が下記のページで示されています。

現在のところクライアントライブラリの実装としては、C++PythonLISPJavaLuaRubyなどがありますが、C#の実装はまだないようです。

と思っていたら、先日 roscs なるものがリリースされていました。これはC++のコードをC#でラップしていて、Linux+mono環境でのみ動作するものみたいです。

一方、RosSharpはすべてC#で実装していて、Windowsでしか動きません。(mono-reactive を使えば、Linux+monoで動くかもしれませんが未検証です)

ROSはLinux文化なので、Windowsでしか動かないRosSharpにどれほどの需要があるのか分かりませんが、WindowsのGUIからROSのノードを使いたいときや、Kinect SDKと組み合わせたいときには有用なんじゃないでしょうか。

非同期プログラミング

時代は非同期!

スマートフォンやタブレットなどの端末が普及し、ユーザにストレスを与えないレスポンスのよいアプリケーションを開発することが重視されてきています。
そういったアプリケーションを開発するためには、非同期処理が重要となるわけですがなかなか実装が難しい。

そのため、次期C#C# 5.0、Visual Studio 2012)ではasync/await構文がサポートされ、非同期処理が簡潔に書けるようになります。

また、Windows 8の新しいアプリケーション実行基盤であるWinRTでは、50msec以上時間がかかる可能性のある処理は、非同期APIしか用意されていないそうです。

というわけで、RosSharpでは通信周りなど時間のかかる処理については、すべて非同期呼び出しとなるように実装しました。

C#で非同期処理を行うための手段としては、Task Parallel Library(TPL)や、Reactive Extensions(Rx)などがありますが、1回きりの非同期メソッド呼び出しはTask、データが非同期に連続的に流れてくるようなところはRx、と使い分けるのが良いようです。RosSharpでもその方針に従って実装しています。

RosSharpのサンプルコード

では、RosSharpのコードを見てみましょう。
同期呼び出しによるサンプルは以下のようになります。

try
{
    // Nodeの生成。Nodeが生成されるまで待つ。
    var node = Ros.InitNodeAsync("/Listener").Result;
    
    // Subscriberの生成。Subscriberが生成されるまで待つ。
    var subscriber = node.SubscriberAsync<RosSharp.std_msgs.String>("/chatter").Result;
    
    // メッセージを購読
    subscriber.Subscribe(x => Console.WriteLine(x.data));
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

次に、TAP(Task-based Asynchronous Pattern)によるサンプルは以下のようになります。

従来のAPM(Asynchronous Programming Model)やEAP(Event-based Asynchronous Pattern)とかに比べると、ネストが深くならずにすむのでシンプルに書けますが、慣れてないと読み書きしにくいのではないでしょうか。

// Nodeの生成
Ros.InitNodeAsync("/Listener")
    .ContinueWith(node => // Nodeが生成されたら呼び出される。
    {
        // Subscriberの生成
        return node.Result.SubscriberAsync<RosSharp.std_msgs.String>("/chatter");
    })
    .Unwrap()
    .ContinueWith(subscriber => // Subscriberが生成されたら呼び出される。
    {
        // メッセージの購読
        subscriber.Result.Subscribe(x => Console.WriteLine(x.data));
    })
    .ContinueWith(res => // エラーが発生したら呼び出される
    {
        Console.WriteLine(res.Exception.Message);
    }, TaskContinuationOptions.OnlyOnFaulted);

最後に、async/awaitを使ったサンプルは以下のようになります。*1
同期型とほとんど同じ書き方になってシンプルですね。
なお、以下のプログラムはVisual Studio 2012 RCで動作確認していますが、まだリリース前で今後仕様が変わる可能性もあるのでご注意ください。

try
{
    // 非同期でノードを生成する
    var node = await Ros.InitNodeAsync("/Listener");
    // ノード生成が完了するまでは呼び出し元に処理を返す。

    // 非同期でSubscriberを生成する。
    var subscriber = await node.SubscriberAsync<RosSharp.std_msgs.String>("/chatter");
    // Subscriber生成が完了するまでは呼び出し元に処理を返す。

    // メッセージを購読する。
    subscriber.Subscribe(x => Console.WriteLine(x.data));
}
catch (Exception ex)
{
    Console.WriteLine(ex.Message);
}

ちなみに、全部Rxで書いてみると以下のようになります。*2

Ros.InitNodeAsync("/Listener")
    .ToObservable()
    .SelectMany(x => x.SubscriberAsync<RosSharp.std_msgs.String>("/chatter").ToObservable())
    .Switch()
    .Subscribe(
        x => Console.WriteLine(x.data),
        ex => Console.WriteLine(ex.Message)
    );
非同期処理で気をつけるべきところ

さて、TaskやRxを使ってコードを書いているといろいろとハマったので注意すべき点を挙げてみます。(当たり前のことばかりかもしれませんが・・・)

  • TaskにしろRxにしろ、例外をどのように伝搬させるかをしっかりと設計すること。正常系だけ先に書いて、あとで例外処理を追加していくと可読性の低いコードになってしまいました。また、ハンドルしてない例外があると、GCが動いたときにファイナライザで例外が発生するので、どこに問題があるのか追いかけづらいです。
  • Rxではメッセージの購読の必要がなくなったらきちんと破棄すること。今回メモリリークを多数発生させてしまいました。
  • ロックのタイミングに注意すること。ロックの範囲が広すぎると非同期処理のメリットも減ってしまうので、できるだけロック範囲が狭くなるように考える必要があります。
  • RosSharpではあまり考慮できていないのですが、処理のキャンセルや進捗状況についても考える必要があります。

NuGet対応

ROSはOSと言うだけあって、ビルドやパッケージングするための基盤が用意されています。しかし、RosSharpはそのあたりにまったく依存していません。

その代わりと言っては何ですが、RosSharpはNuGet Galleryに登録してあるので、簡単に使い始めることができます。

IDLパーサ

ROSでは、トピックやサービスで利用したい型を、独自フォーマットのIDL(Interface Definition Language)ファイルで定義することができます。

今回は、そのためのパーサをF#+FParsecで作りました。

オフサイドルールのパーサに関しては、以下の記事のものを使わせていただきました。感謝。

リアライザのパフォーマンス

RosSharpはノード間でメッセージ通信を行う際に、当然メッセージのシリアライズ・デシリアライズを行います。
下記の記事を見て、RosSharpのシリアライザのパフォーマンスはどんなものかなと思ったので、試しに計測してみました。(ついでにCORBA実装のIIOP.NETも)

Serialize Formatter - Protocol Buffers 2.0.0.480
00:00:00.4463423
13MB
Serialize JsonSerializer - JSON.NET 4.5.7
00:00:07.2403286
38MB
Serialize JsonSerializer - JSON.NET 4.5.7 BSON
00:00:10.0504359
44MB
Serialize AutoMessagePackSerializer`1 - MessagePack for CLI 0.2-beta1
00:00:00.5075449
11MB
Serialize Object RosSharp 0.1.0
00:00:00.4581154
16MB
Serialize CdrSerializer`1 - IIOP.NET 1.9.3
00:00:01.2561850
22MB

Deserialize Formatter - Protocol Buffers 2.0.0.480
00:00:00.8277988
Check => OK
Deserialize JsonSerializer - JSON.NET 4.5.7
00:00:09.1964346
Check => OK
Deserialize JsonSerializer - JSON.NET 4.5.7 BSON
00:00:09.9433615
Check => OK
Deserialize AutoMessagePackSerializer`1 - MessagePack for CLI 0.2-beta1
00:00:05.5326335
Check => NG
Deserialize Object RosSharp 0.1.0
00:00:00.9619598
Check => OK
Deserialize CdrSerializer`1 - IIOP.NET 1.9.3
00:00:03.2890727
Check => NG

RosSharpはProtocol Bufferより少し遅いくらいですね。ただし、これはRosSharpがすごいということではありません。

RosSharp以外のシリアライザは、リフレクションを使ってクラス構造を解析しつつ、シリアライズを行っています。一方のRosSharpは、IDL(Interface Definition Language)ファイルを書いて、そこから特定の型専用のシリアライズ・デシリアライズ処理のソースコードを生成しています。

というわけでフェアじゃないです。むしろ、リフレクションしてるはずのProtocol Bufferがこの速度だということに驚きですね。

今後

Visual Studio 2012がリリースされたら、async/awaitで全面的に書き直して、Rx2.0にも対応させたいですね。
また、テストやドキュメントが不足しているので追加していきたいと思います。

あとは、プロトタイプだけ作って放置してあるReactiveRTMもちゃんと実装しようかな。

*1:この処理を呼び出すメソッドにはsyncキーワードを付与する必要があります。

*2:Rx2.0ではTaskとの連携が強化されるのでもう少し簡潔に書けるようになります。