.NET RemotingとWCFの性能比較

2017/06/27

.NET RemotingよりWCFのほうが遅いという噂もあるようなので、絶対値がどれぐらいなのかを調べるついでに、.NET RemotingとWCFとで比較してみた。

条件は以下の通り

  • 同一マシン内で、IPCにより通信を行う。
  • クライアントとサーバは別プロセスとする
  • クライアント側からサーバ側の手続きを呼び出す処理の速度を比べる
  • Releaseモードでコンパイルする。
  • F#4.1、.NET Framework 4.7で実行する

昨日とほぼ同じだが、.NET RemotingとWCFのコードを以下に示す。

■.NET Remoting版

-----------------------------------------------------------------

open System
open System.Runtime.Remoting
open System.Runtime.Remoting.Channels
open System.Runtime.Remoting.Channels.Ipc
open System.Threading

let LoopCount = 100000UL

// サーバからクライアントに対して公開されるオブジェクト
type CSharedObject() =
    inherit MarshalByRefObject()
    member this.foo arg =
        ( arg / 2UL )

// サーバ側の処理
let Server () =
    ChannelServices.RegisterChannel( new IpcServerChannel "test1", true )
    let rSharedObject = new CSharedObject()
    RemotingServices.Marshal(
        rSharedObject,
        "test2",
        typeof< CSharedObject >
    ) |> ignore

    System.Threading.Thread.Sleep 60000

// クライアント側の処理
let Client () =
    // サーバ側で公開されているオブジェクトを取得する
    ChannelServices.RegisterChannel( new IpcClientChannel(), true )
    let rSharedObject =
        Activator.GetObject(
            typeof< CSharedObject >,
            "ipc://test1/test2"
        ) :?> CSharedObject

    let rec foo cnt sum =
        if cnt < LoopCount then
            foo ( cnt + 1UL ) ( sum + ( rSharedObject.foo cnt ) )
        else
            sum

    for i = 1 to 5 do
        let StartTime = DateTime.Now
        printf "%u\n" ( foo 0UL 0UL )
        let ElapseTime = ( DateTime.Now - StartTime ).TotalMilliseconds
        printf
            "time=%f(ms), count/sec=%f\n"
            ElapseTime ( ( float LoopCount ) / ElapseTime * 1000.0 )

[<EntryPoint>]
let main argv = 
    if argv.[0] = "server" then
        Server ()
    else
        Client ()
    0

-----------------------------------------------------------------

■WCF版

-----------------------------------------------------------------

open System
open System.ServiceModel
open System.Threading

let g_Address = "net.pipe://localhost/test"
let LoopCount = 100000UL

// クライアントに公開されるオブジェクトのインタフェース
[<ServiceContract()>]
type public IRemote =
    [<OperationContract(Name="Hello")>]
    abstract member Hello : arg:uint64 -> uint64

// 遠隔手続きを実装するクラス
type public Remote() =
    interface IRemote with
        member this.Hello arg  =
            ( arg / 2UL )

// サーバ側の処理
let Server () =
    let host =
        new ServiceHost(
            typeof<Remote>,
            new Uri( "net.pipe://localhost" )
        )
    host.AddServiceEndpoint(
        typeof<IRemote>,
        new NetNamedPipeBinding( NetNamedPipeSecurityMode.None ),
        g_Address
    ) |> ignore
    host.Open();

    Threading.Thread.Sleep 60000

// クライアント側の処理
let Client () =
    let proxy =
        ChannelFactory<IRemote>.CreateChannel(
            new NetNamedPipeBinding( NetNamedPipeSecurityMode.None ),
            new EndpointAddress( g_Address )
         )
    let rec foo cnt sum =
        if cnt < LoopCount then
            foo ( cnt + 1UL ) ( sum + ( proxy.Hello cnt ) )
        else
            sum

    for i = 1 to 5 do
        let StartTime = DateTime.Now
        printf "%u\n" ( foo 0UL 0UL )
        let ElapseTime = ( DateTime.Now - StartTime ).TotalMilliseconds
        printf
            "time=%f(ms), count/sec=%f\n"
            ElapseTime ( ( float LoopCount ) / ElapseTime * 1000.0 )
    
[<EntryPoint>]
let main argv = 
    if argv.[0] = "server" then
        Server ()
    else
        Client ()
    0

-----------------------------------------------------------------



これを実行してみる。


.NET Remotingの実行結果

2499950000
time=7941.328300(ms), count/sec=12592.351836
2499950000
time=7799.342400(ms), count/sec=12821.593779
2499950000
time=7746.714100(ms), count/sec=12908.698928
2499950000
time=7709.778300(ms), count/sec=12970.541578
2499950000
time=7731.699700(ms), count/sec=12933.766685

"2499950000"の出力は最適化させないように、無理にでも処理結果を使うようにしているだけであって、意味はない。

とりあえず、1秒間に1万2千回程度呼び出しができるようだ。

次はWCFの実行結果

2499950000
time=4733.906600(ms), count/sec=21124.202155
2499950000
time=4538.663200(ms), count/sec=22032.919297
2499950000
time=4550.678500(ms), count/sec=21974.745085
2499950000
time=4541.667300(ms), count/sec=22018.345553
2499950000
time=4558.723600(ms), count/sec=21935.964707

WCFだと秒間で2万1千回程度実行できているように見える。

よくわからないが、.NET RemotingよりWCFのほうが早い。

何か設定や指定が悪いのか、あるいは.NETのバージョンが上がってWCFの最適化が進んでいるためか、原因は知らんが、まぁこの程度だということを前提に考えておこう。


F#でIPCをやってみる

2017/06/26

F#でプロセス間通信を行うことを考えてみる。

無論、丸裸のTCPやUDP、共有メモリやパイプ・メールスロットなんかでもいいわけだが、それだと正直に言って実装がしんどい。だから、何かいい感じの方法は無いものかと探っていたら、.NET Remotingなる方法があるらしいので、試してみた。

■.NET Remoting版

-------------------------------------------------------------------------------------------

open System
open System.Runtime.Remoting
open System.Runtime.Remoting.Channels
open System.Runtime.Remoting.Channels.Ipc
open System.Diagnostics
open System.Threading

// サーバからクライアントに対して公開されるオブジェクト
type CSharedObject() =
    inherit MarshalByRefObject()

    // publicかつ可変なメンバ変数
    [<DefaultValue>]
    val mutable public v2 : int
    let mutable value = 0

    // publicなメソッド
    member this.foo arg =
        let pid = Process.GetCurrentProcess().Id
        let thid = Thread.CurrentThread.ManagedThreadId
        printf "CSharedObject.foo, pid=%d, thid=%d, value=%d, arg=%d\n" pid thid value arg
        value <- arg
        arg + 1         // 意味はないが、+1した値を返してみる

// サーバ側の処理
let Server () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Server Process started. pid = %d, thid=%d\n" pid thid

    // CSharedObjectのオブジェクトを作って公開する
    ChannelServices.RegisterChannel( new IpcServerChannel "test1", true )
    let rSharedObject = new CSharedObject()
    RemotingServices.Marshal( rSharedObject, "test2", typeof< CSharedObject > ) |> ignore

    //************************************************************
    for i = 1 to 60 do
        // サーバ側でv2の値を定期的に更新してみる
        rSharedObject.v2 <- i
        printf "Server, pid=%d, thid=%d, v2=%d\n" pid thid rSharedObject.v2
        System.Threading.Thread.Sleep 1000
    //************************************************************

// クライアント側の処理
let Client () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Client Process started. pid = %d, thid=%d\n" pid thid

    // サーバ側で公開されているオブジェクトを取得する
    ChannelServices.RegisterChannel( new IpcClientChannel(), true )
    let rSharedObject = Activator.GetObject( typeof< CSharedObject >, "ipc://test1/test2" ) :?> CSharedObject

    //************************************************************
    for i = 1 to 10 do
        // 定期的に公開オブジェクトのメソッドを呼ぶ
        printf
            "Client, pid=%d, thid=%d, foo=%d, v2 = %d\n"
            pid thid ( rSharedObject.foo ( i * 100 ) ) rSharedObject.v2
        Threading.Thread.Sleep 500
    //************************************************************

[<EntryPoint>]
let main argv = 
    if argv.[0] = "server" then
        Server ()
    else
        Client ()
    0

-------------------------------------------------------------------------------------------

上記と合わせて、System.Runtime.Remotingを参照設定に追加して、コンパイルしてやったら動いた。

検索して出てきたC#のソースをほぼそのままF#に変えてやっただけではあるが、非常に素直に動作してくれる。

どうも、挙動としてはサーバ側で公開したオブジェクト(上記ではCSharedObjectクラスのインスタンス)のメソッドをクライアント側から呼び出すと、引数がクライアントからサーバに渡されて、サーバ側のプロセス空間で手続きが実行され、戻り値がクライアント側に返されるらしい。

併せて、CSharedObjectクラスのメンバ変数(上記だとCSharedObject.v2)をサーバないしクライアントで変更すると、それが相手側に反映されるらしい。正直この辺りはどのような実装になっているのか、あるいは、いつのタイミングで変更が反映されるのか、さっぱり理解ができない。しかしまぁ、publicで変更可能でメンバ変数など断じて使うことはないから、動作のメカニズムなど知る必要はないだろう。

非常に素晴らしいではないか! と言いたいところだが、よく調べてみると、.NET Remotingは.NET 3.0以降では非推奨になっているという。今ではWCFを使えと。

WCFは本屋でそういうタイトルの本が売っているのを見たことはあるが、今まで手を出したことがない。とりあえず、統一的に通信できるようにする仕掛けらしいのだが、いまいちよくわからない。かつ、深入りする気力がない。

どうせやりたいこととしては、「同一マシン内で」「お手軽に」プロセス間通信を行いたいだけなのだ。なので、またしてもネットにあったソースをパクってF#に書き直してやってみた。

■WCF版

-------------------------------------------------------------------------------------------

open System
open System.ServiceModel
open System.Diagnostics
open System.Threading

let g_Address = "net.pipe://localhost/test"

// クライアントに公開されるオブジェクトのインタフェース
[<ServiceContract()>]
type public IRemote =
    [<OperationContract(Name="Hello")>]
    abstract member Hello : arg:string -> string

// 遠隔手続きを実装するクラス
type public Remote() =
    interface IRemote with
        member this.Hello( arg ) =
            let pid = Process.GetCurrentProcess().Id
            let thid = Thread.CurrentThread.ManagedThreadId
            printf "Remote.Hello arg=%s, pid=%d, this=%d\n" arg pid thid
            sprintf "Hello %s" arg

// サーバ側の処理
let Server () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Server start pid=%d, this=%d\n" pid thid

    let host = new ServiceHost( typeof<Remote>, new Uri( "net.pipe://localhost" ) )
    host.AddServiceEndpoint(
        typeof<IRemote>,
        new NetNamedPipeBinding( NetNamedPipeSecurityMode.None ),
        g_Address
    ) |> ignore
    host.Open();

    Threading.Thread.Sleep 60000

// クライアント側の処理
let Client () =
    let pid = Process.GetCurrentProcess().Id
    let thid = Thread.CurrentThread.ManagedThreadId
    printf "Client start pid=%d, this=%d\n" pid thid

    let proxy =
        ChannelFactory.CreateChannel(
            new NetNamedPipeBinding( NetNamedPipeSecurityMode.None ),
            new EndpointAddress( g_Address )
         )
    
    for i = 1 to 10 do
        printf "client, i=%d, pid=%d, this=%d\n" i pid thid
        proxy.Hello ( "abc" + i.ToString() ) |> ignore
        Threading.Thread.Sleep 1000

[<EntryPoint>]
let main argv = 
    if argv.[0] = "server" then
        Server ()
    else
        Client ()
    0

-------------------------------------------------------------------------------------------

上記と合わせて、System.Runtime.SerializationとSystem.ServiceModelを参照設定に追加してやる。

こうしてやると、概ね.NET Remotingの場合と同じように、クライアント側からメソッドを呼んでやるとサーバ側の手続きが実行されるという挙動が実現できる。

しかし、ここで1つ嵌ったことがあったから記録として残しておく。

上記のコードの内インタフェースの宣言部分について、初めは以下のように書いていた。

[<ServiceContract()>]
type public IRemote =
    [<OperationContract(Name="Hello")>]
    abstract member Hello : string -> string

Helloメソッドの仮引数名を省略していた。

この状態でサーバ側のプロセスを起動すると、new ServiceHost(……)の処理で"ハンドルされていない例外: System.ArgumentNullException: サービス コントラクトを構成する操作内で使用するすべてのパラメーター名は NULL にできません。"なる例外が発生して処理が異常終了する。

どうにも、発生した事象からは原因が特定できず、まともに動作させられるようになるまで、ひどく苦労した。大体、普通、仮想メソッドの仮引数名など意味はないと思うだろう。F#でもC++でも省略できるし。

どうにも原因がわからなくて、とりあえず試しにC#で書いてみたらあっさり動くし、その時何となくIRemote.Helloの仮引数名(arg)が省略できないから、何となくF#側も明記してやったら、たまたま偶然動くようになった。わかるわけねぇだろう。

まぁ、マーシャリングに必要なんだと言われればそんな気もしなくもないが、しかし、エラーメッセージはもう少しどうにかならないものなのだろうか?

F#でWindows Serviceを実装する。

2017/06/23

.NET FrameworkでWindowsのサービスを実装するのは簡単だろうと高をくくっていたらいろいろとはまったので記録を残しておく。

大きくやらなければならないのは、

  • Serviceとして実行される処理の実装
  • インストーラの実装

の2つ。

そのうち、インストーラの実装の方が、大体「Visual StudioでWindowsサービス用のテンプレートを使って……云々」という記載だけで誤魔化しているものばかりで、ほとんどまともな説明がない。わかってないなら、偉そうに書くなと。

(1)Serviceの実装

Serviceの実装は、

  1. System.ServiceProcess.ServiceBaseクラスを継承したサブクラスを実装する。
  2. System.ServiceProcess.ServiceBase.Runメソッドで、サービスを起動してやる。

2つをやる必要がある。

なので、以下のようなプログラムを記述する。

module File1

open System.ServiceProcess
open System.IO

// Windowsサービスに関連するイベントを処理するクラス
type ServiceTest() =
    inherit ServiceBase()
    do
        base.ServiceName <- "ServiceTest"
        base.CanStop <- true
        base.CanPauseAndContinue <- true
        base.AutoLog <- true

    override this.OnContinue () =
        File.AppendAllText( @"d:\a.txt", "OnContinue\r\n" )

    override this.OnCustomCommand command =
        File.AppendAllText( @"d:\a.txt", "OnCustomCommand " + command.ToString () + "\r\n" )

    override this.OnPause () =
        File.AppendAllText( @"d:\a.txt", "OnPause\r\n" )

    override this.OnPowerEvent powerStatus =
        File.AppendAllText( @"d:\a.txt", "OnPowerEvent " + powerStatus.ToString () + "\r\n" )
        true

    override this.OnSessionChange changeDescription =
        File.AppendAllText( @"d:\a.txt", "OnSessionChange " + changeDescription.ToString () + "\r\n" )

    override this.OnShutdown () =
        File.AppendAllText( @"d:\a.txt", "OnShutdown\r\n" )

    override this.OnStart args =
        File.AppendAllText( @"d:\a.txt", "OnStart ArgCnt=" + args.Length.ToString() + "\r\n" )

// エントリポイント
[<EntryPoint>]
let main args =
    File.AppendAllText( @"d:\a.txt", "main start\r\n" )
    // 起動してやる
    ServiceBase.Run( new ServiceTest() )
    File.AppendAllText( @"d:\a.txt", "main end\r\n" )
    0

この辺については、MSDNの以下を見るとおおむね記載されている。

https://msdn.microsoft.com/ja-jp/library/76477d2t(v=vs.110).aspx

併せて、参照設定で以下を追加しなければならない。

・System.ServiceProcess

(2)インストーラの実装

.NET Frameworkでサービスを実装する場合、インストールするのに「InstallUtil.exe」を使えと指定されている。それについては、MSDNの以下のページに記載がある。

https://msdn.microsoft.com/ja-jp/library/sd8zc8ha(v=vs.110).aspx

問題は、このコマンドでインストールされるサービスのプログラムには、いくつかのおまじないが必要になるということである。でもって、その唱えるべき呪文が正しく記載された情報が見当たらないのが腹が立つ。

まず、実装すべき内容そのものは、以下に記載がある。

https://msdn.microsoft.com/ja-jp/library/system.serviceprocess.serviceprocessinstaller(v=vs.110).aspx

だが、ここに書かれている情報は不完全で、これだけでは動かない。糞が。

概ね、やらなければならないこととしては

  1. System.Configuration.Install.Installerクラスを継承したクラスを実装する
  2. 上記のクラスのメンバとして、System.ServiceProcess.ServiceProcessInstallerと、System.ServiceProcess.ServiceInstallerのインスタンスを持たせる。
  3. コンストラクタで、上記メンバの初期化とSystem.Configuration.Install.Installer.Installersへの追加を行う

ということである。

なので、プログラム的には以下となる。

// 名前空間にはProject.WindowsServiceを指定しなければならない。
namespace Project.WindowsService

open System.ServiceProcess
open System.Configuration.Install
open System.ComponentModel

 // System.Configuration.Install.Installerクラスを継承したサブクラスを実装する。
// その際、このクラスにはRunInstaller(true)の属性を指定してやらなければならない。
// さらに、このクラスはProject.WindowsServiceの名前空間に所属していなければならない。
[<RunInstaller(true)>]
type public MyProjectInstaller() =
    inherit Installer()

    let processInstaller =
        new ServiceProcessInstaller(
            Account = ServiceAccount.LocalSystem
        )

    let serviceInstaller1 =
        new ServiceInstaller(
            StartType = ServiceStartMode.Manual,
            ServiceName = "ServiceTest",
            DisplayName = "ServiceTest",
            Description = "Some string that descript this service desuwa."
        )

    do
        base.Installers.AddRange
            [|
                processInstaller :> Installer;
                serviceInstaller1 :> Installer
            |]

ここで、赤背景で塗ってある箇所は、MSDNにも真っ当に記載されていないポイントなので気を付ける必要がある。

併せて、System.Configuration.Installへの参照設定を追加してやる必要もある。

上記2つのプログラムを適当にコンパイルしてやって(一方はProject.WindowsServiceの名前空間を指定する必要があり、もう一方はmain関数が存在する必要があるから、モジュールにならざるを得ない。そのため、少なくともソースコードのファイルは2つに別れなければならないはず)、「installutil EXEファイル名」と指定してやれば、サービスに登録できるはず。

とりあえず、64bit環境なので管理者権限で起動したPowerShellから、以下のように実行してやる。

------------------------------------------------------------------
Windows PowerShell
Copyright (C) 2016 Microsoft Corporation. All rights reserved.
PS C:\Windows\system32> $Env:Path += ";C:\Windows\Microsoft.NET\Framework64\v4.0.30319"
PS C:\Windows\system32> cd D:\MO\F#\ServiceTest\ServiceTest\obj\Debug
PS D:\MO\F#\ServiceTest\ServiceTest\obj\Debug> installutil ServiceTest.exe
Microsoft(R) .NET Framework Installation utility Version 4.7.2046.0
Copyright (C) Microsoft Corporation. All rights reserved.

トランザクションのインストールを実行中です。
インストール段階を開始しています。
D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe アセンブリの進行状態については、ログ ファイルの内容を参照してください。
ファイルは D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog にあります。
アセンブリ 'D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe' をインストールしています。
該当するパラメーター:
   logtoconsole =
   assemblypath = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe
   logfile = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog
サービス 'ServiceTest' をインストールしています...
サービス 'ServiceTest' は正常にインストールされました。
EventLog ソース ServiceTest をログ Application に作成しています...
インストール段階が正常に完了しました。コミット段階を開始しています。
D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe アセンブリの進行状態については、ログ ファイルの内容を参照してください。
ファイルは D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog にあります。
アセンブリ 'D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe' をコミットしています。
該当するパラメーター:
   logtoconsole =
   assemblypath = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.exe
   logfile = D:\MO\F#\ServiceTest\ServiceTest\obj\Debug\ServiceTest.InstallLog
コミット段階が正常に終了しました。
トランザクション インストールが完了しました。
PS D:\MO\F#\ServiceTest\ServiceTest\obj\Debug>
------------------------------------------------------------------

そうすると、以下のようにサービスに登録される。

ソースに記載している通り、d:\a.txtにデバッグ用の文字列を出力させているので、「開始」-「一時停止」-「再開」-「停止」と操作してやると、以下のような出力が得られる。

------------------------------------------------------------------
main start
OnStart ArgCnt=0
OnPause
OnContinue
main end
------------------------------------------------------------------

また、登録したサービスを削除するには「installutil /u EXEファイル名」と指定してやればいい。