Thread.SleepとAsync.Sleepの違い

2017/07/08

F#の本で非同期処理について触れている個所では、Thread.SleepではなくAsync.Sleepを使って説明されていた。

この本

ところで、この2つは何が違うのか?

どうやら調べてみると、以下のような違いがあるらしい。

(1)Thread.Sleepは本当にスレッドの動作を停止するらしい

(2)Async.Sleepはスレッドの動きを停止するのではなく、指定された時間経過後に後続の処理をスレッドプールで再開する。

わかりにくいのが、Async.Sleepを使って以下のように記述した場合、

let func2 =
    async {
        (* ここで何かの処理を行う(前半) *)
        do! Async.Sleep 1000
        (* ここで何かの処理を行う(後半) *)
    }

結局のところ、前半の処理を行った後、上記であれば1秒間待ち合わせてから、後半の処理を行うことになるわけで、少なくとも表面的には、以下のようなコードと挙動に相違がない。

let func2 =
    async {
        (* ここで何かの処理を行う(前半) *)
        Thread.Sleep 1000
        (* ここで何かの処理を行う(後半) *)
    }

ただ、厳密にいえば、Async.Sleepを使った場合には、前半の処理と後半の処理とで、実行するスレッドが異なる場合があるので、スレッド毎に依存する何かを使っている場合には、アプリケーションから見える挙動に相違があるともいえる。

例えば、以下のようにすると、スレッドのIDが変わっていることが確認できる。

module  FTest
open System 
open System.Threading

let func2 =
    async {
        printf "%d\n" Thread.CurrentThread.ManagedThreadId
        do! Async.Sleep 1000
        printf "%d\n" Thread.CurrentThread.ManagedThreadId
    }

[<EntryPoint>]
let main(_) = 
    Async.Start func2
    Thread.Sleep 3600000
    0

実行結果はこうなる。




では、スレッドのIDが変わるかもしれない、ということ以外に何か相違はないのか? だとしたら、より直感的に動作してくれるThread.Sleepを使ったほうがいいのではないか? という気がしてくるが、どうやらそうではないらしい。


Thread.Sleepを使った場合は、スレッドを停止させてしまうため、.NETのスレッドプールは、ほかの処理を受け入れられるように追加でスレッドを生成するようだ。一方、Async.Sleepでは必要に応じて(つまり一定時間経過後に)タスクをスレッドプールに放り込むだけなので、スレッド数が増大することがないらしい。

だから、asyncワークフロー内で逐次的に処理されるんだったら一緒じゃねぇかと言って、非同期的なAPIを使わず同期的なAPIを使ってしまっては効率が低下するらしい。

ということで、ちょっと試してみた。

まずはThread.Sleepを使った場合。

module  FTest
open System 
open System.Threading

let func1 =
    async {
        Thread.Sleep 3600000
    }

[<EntryPoint>]
let main(_) = 
    for i = 0 to 10000 do
        Async.Start func1

    Thread.Sleep 3600000
    0

これを実行してしばらくリソースモニターを見ていると、スレッド数が増加し続けていく。




Async.Sleepを使った以下のようなコードでは、

module  FTest
open System 
open System.Threading

let func2 =
    async {
        do! Async.Sleep 3600000
    }

[<EntryPoint>]
let main(_) = 
    for i = 0 to 10000 do
        Async.Start func2

    Thread.Sleep 3600000
    0

スレッド数の増加がみられない。




上でも書いたが、なんでもこれはSleepに限らずI/Oの場合も同様らしいので、できるのであれば同期型のI/O命令を使用するのではなく、非同期型のI/O命令を使って、asyncないのdo!やlet!で待ち合わせるようにしたほうがいいらしい。そうでないと、過剰なスレッドの生成によるメモリの無駄遣いや、コンテキストスイッチの多発による性能の劣化が生じるのだという。


と、いうようなことが、.NET Flameworkの本に書いてあった。


この本


アプリケーションドメインを分離した実装について考えてみる

2017/07/03

先日からプロセス間通信の方法を探っているのは、ものを作るにあたって、プロセスを分離することで万一の障害発生時に影響範囲を限定することで、全体の信頼性を向上することができないかと考えているためである。

だが、調べてみると、.NETではアプリケーションドメインを分離するという手口も使えるらしい

これはなんか期待できそうだと思い、ちょっとやってみた。

open System
open System.Threading
open System.Reflection

let LoopCount = 100000UL

// アプリケーションドメイン境界を越えてアクセスされる型
type public CFoo() =
    inherit MarshalByRefObject()

    // 以下は"domain2"のアプリケーションドメインで実行される
    member public this.goo arg =
        ( arg / 2UL )

// mainはデフォルトのアプリケーションドメインで実行される
[<EntryPoint>]
let main argv = 
    // 新規にアプリケーションドメインを作る
    let ad2 = AppDomain.CreateDomain( "domain2", null, null )

    // 新しいアプリケーションドメインにアセンブリをロードし、
    // CFooにアクセスするプロキシを生成する
    let rfoo =
        ad2.CreateInstanceAndUnwrap(
            Assembly.GetEntryAssembly().FullName,
            typeof<CFoo>.FullName
        ) :?> CFoo

    // 意味もなくメソッドgooを呼び出し続ける処理
    let rec foo cnt sum =
        if cnt < LoopCount then
            foo ( cnt + 1UL ) ( sum + ( rfoo.goo cnt ) )
        else
            sum

    // CFoo.gooを100000回呼び出す処理を5回繰り返す
    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 )

    0

上記を実行すると、以下となる。

2499950000
time=53.564900(ms), count/sec=1866894.178837
2499950000
time=51.071400(ms), count/sec=1958043.053451
2499950000
time=51.042200(ms), count/sec=1959163.202213
2499950000
time=51.086900(ms), count/sec=1957448.974199
2499950000
time=51.556400(ms), count/sec=1939623.402720
続行するには何かキーを押してください . . .

CFoo.gooが、mainを実行している既存のアプリケーションドメインとは異なるアプリケーションドメインで実行されているという証拠は示していないが、その辺りは長くなるから割愛する。その辺りは、CFoo.gooに「Thread.GetDomain().FriendlyName」を表示する処理を入れて実行してみればわかる。

むしろここで問題になるのは性能である。上記は、プロセスを分離してWCFで通信を行うものと同じ処理を行っているのだが、その時には秒間で2万回がせいぜいだった。しかし、今回は195万回を超える回数を実行できている。圧倒的に高速である。ここまで違うとなると、是が非でもこちらを使いたくなる。

では、次はアプリケーションドメインを分離することで、プロセスを分けるのと同じような効果が得られるのかどうかを考えてみる。とはいっても闇雲に調べてもきりがないから、一番気になる、下記のような観点に絞って、問題の有無を調査してみる。

  • 未処理の例外が発生した時の影響範囲を限定できるのか?
  • 問題が生じたときに、狙ったアプリケーションドメインだけ殺すことができるのか?

まず1点目について、下記のような処理を試してみた。

open System
open System.Threading
open System.Reflection

// アプリケーションドメイン境界を越えてアクセスされる型
type public CFoo() =
    inherit MarshalByRefObject()

    // 以下のメソッドは"domain2"のアプリケーションドメインで実行される
    member public this.goo () =
        // 呼ばれたらすぐに例外を発生させる
        raise <| System.Exception "bomb1"
        printf "CFoo.goo %s\n" ( Thread.GetDomain().FriendlyName )

[<EntryPoint>]
let main argv = 
    printf "main start %s\n" ( Thread.GetDomain().FriendlyName )

    // アプリケーションドメインを作る
    let ad2 = AppDomain.CreateDomain( "domain2", null, null )

    // とりあえず、自分自身のexeをロードして、CFooオブジェクトを作る
    let rfoo =
        ad2.CreateInstanceAndUnwrap(
            Assembly.GetEntryAssembly().FullName,
            typeof<CFoo>.FullName
        ) :?> CFoo

    // 呼び出す
    rfoo.goo ()

    Thread.Sleep 10000

    printf "main end %s\n" ( Thread.GetDomain().FriendlyName )
    0

実行結果は以下となる。

main start ADTest.exe

ハンドルされていない例外: System.Exception: bomb1
   場所 Program.CFoo.goo() 場所 D:\MO\F#\ADTest\ADTest\Program.fs:行 12
   場所 Program.CFoo.goo()
   場所 Program.main(String[] argv) 場所 D:\MO\F#\ADTest\ADTest\Program.fs:行 30
続行するには何かキーを押してください . . .

余裕でプロセス全体が玉砕。

スタックトレースを見れば明らかだが、CFoo.gooで発生した例外は、アプリケーションドメインの境界を越えてmainにまで伝播し、そこでも例外が処理されずにプロセスダウンに至っている。

なので、CFoo.gooの呼び出し前後で例外を捕まえるようにすれば、プロセスダウンを回避できる。それについては当たり前すぎるので、プログラム例は割愛する。

次に、新規に作成したアプリケーションドメイン内で例外が発生したらどうなるのか試してみた。すなわち、確実にmainにまで伝播しない例外が発生して、それが未処理となった場合の挙動を試してみる。

open System
open System.Threading
open System.Reflection

// アプリケーションドメイン境界を越えてアクセスされる型
type public CFoo() =
    inherit MarshalByRefObject()

    // 以下のメソッドは"domain2"のアプリケーションドメインで実行される
    member public this.goo () =
        // 3秒後に別スレッドで例外を発生させる
        async {
            Thread.Sleep 3000
            raise <| System.Exception "bomb1"
        } |> Async.Start
        printf "CFoo.goo %s\n" ( Thread.GetDomain().FriendlyName )

[<EntryPoint>]
let main argv = 
    printf "main start %s\n" ( Thread.GetDomain().FriendlyName )

    // アプリケーションドメインを作る
    let ad2 = AppDomain.CreateDomain( "domain2", null, null )

    // とりあえず、自分自身のexeをロードして、CFooオブジェクトを作る
    let rfoo =
        ad2.CreateInstanceAndUnwrap(
            Assembly.GetEntryAssembly().FullName,
            typeof<CFoo>.FullName
        ) :?> CFoo

    // 呼び出す
    rfoo.goo ()
    printf "main rfoo.goo called %s\n" ( Thread.GetDomain().FriendlyName )
    Thread.Sleep 10000
    printf "main end %s\n" ( Thread.GetDomain().FriendlyName )
    0

こうなる。

main start ADTest.exe
CFoo.goo domain2
main rfoo.goo called ADTest.exe

ハンドルされていない例外: System.Exception: bomb1
   場所 Program.goo@13.Invoke(Unit unitVar) 場所 D:\MO\F#\ADTest\ADTest\Program.fs:行 14
   場所 Microsoft.FSharp.Control.AsyncBuilderImpl.callA@839.Invoke(AsyncParams`1 args)
--- 直前に例外がスローされた場所からのスタック トレースの終わり ---
   場所 Microsoft.FSharp.Control.CancellationTokenOps.Start@1292-1.Invoke(ExceptionDispatchInfo edi)
   場所 .$Control.loop@425-51(Trampoline this, FSharpFunc`2 action)
   場所 Microsoft.FSharp.Control.Trampoline.ExecuteAction(FSharpFunc`2 firstAction)
   場所 Microsoft.FSharp.Control.TrampolineHolder.Protect(FSharpFunc`2 firstAction)
   場所 .$Control.-ctor@509-1.Invoke(Object state)
   場所 System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
   場所 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   場所 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   場所 System.Threading.ThreadPoolWorkQueue.Dispatch()
   場所 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
続行するには何かキーを押してください . . .

俺としてはここで、"domain2"だけが終了させられて、既定のドメイン(ここではADTest.exe)の処理は続行してほしいのだが、そうはいかない。見事にプロセスごと崩御なされる。この野郎。

なんでも、CLRの未処理例外に対するデフォルトのエスカレーションポリシーなるものがあり、未処理例外が起きた場合には、プロセスを終了させる処理を行うようになっているらしい。でもって、この挙動を変えるためには、CLRのホストインタフェースを叩いてやらねばならぬらしい。何を言っているかというと、別のプログラム(例えばアンマネージなC++のプログラム)から、COMのインタフェースを通じてCLRを呼び出して、マネージコードを実行させるような処理を自作してやれば、エスカレーションポリシーを変更することができる。

結局のところ、C#やF#で作ったマネージexeファイルを直接実行した場合でも、Windows自体がCLRのロードやマネージコードのロードを行っているだけであるため、なんか設定を変えるかどうかすれば挙動を変えられるのではないかという気もするのだが、起動する過程に介入するような方法が見当たらなかった。

だから、自前で以下のようなC++のプログラムを作ってみた。

#include "stdafx.h"
#include <mscoree.h>
#include <metahost.h>
#include "ADTest3.h"

#pragma comment(lib, "mscoree.lib")

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    HRESULT hr;
    DWORD dwResult;
    ICLRMetaHost *pMetaHost;
    ICLRRuntimeInfo *pRuntimeInfo;
    ICLRRuntimeHost *pRuntimeHost;
    ICLRControl *pCLRControl;
    ICLRPolicyManager *pCLRPolicyManager;

    hr = CLRCreateInstance( CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID*)&pMetaHost );
    hr = pMetaHost->GetRuntime( L"v4.0.30319", IID_ICLRRuntimeInfo, (LPVOID*)&pRuntimeInfo );
    hr = pRuntimeInfo->GetInterface( CLSID_CLRRuntimeHost, IID_ICLRRuntimeHost, (LPVOID*)&pRuntimeHost );
    hr = pRuntimeHost->GetCLRControl( &pCLRControl );
    hr = pCLRControl->GetCLRManager( IID_ICLRPolicyManager, (LPVOID*)&pCLRPolicyManager );
    hr = pCLRPolicyManager->SetUnhandledExceptionPolicy( eHostDeterminedPolicy );

    hr = pRuntimeHost->Start();
    hr = pRuntimeHost->ExecuteInDefaultAppDomain( L"ADTest.exe", L"prog", L"bar", NULL, &dwResult );

    hr = pRuntimeHost->Stop();

    hr = pCLRPolicyManager->Release();
    hr = pCLRControl->Release();
    hr = pRuntimeHost->Release();
    hr = pRuntimeInfo->Release();
    hr = pMetaHost->Release();

    return 0;
}

コメントはないが、適当に察してほしい。ICLRPolicyManager::SetUnhandledExceptionPolicyで、未処理例外が生じたときの挙動を変更できる。

上記のC++のプログラムから、下記のようなF#のプログラムを起動する。

module prog

open System
open System.Threading
open System.IO
open System.Reflection
open System.Runtime.InteropServices

// 未処理例外が生じたときに呼び出されるハンドラ
// (ここでは、例外を握りつぶすことはできない)
let handler ( e:UnhandledExceptionEventArgs ) =
    File.AppendAllText( @"d:\a.txt", "handler called " + ( Thread.GetDomain().FriendlyName ) + "\r\n" )
    let ex = e.ExceptionObject :?> Exception
    File.AppendAllText( @"d:\a.txt", ex.StackTrace + "\r\n" )
    AppDomain.Unload <| AppDomain.CurrentDomain

type public CFoo() =
    inherit MarshalByRefObject()
    member public this.goo () =
        AppDomain.CurrentDomain.UnhandledException.Add handler

        async {
            // 3秒後に別スレッドで例外を発生させる
            Thread.Sleep 3000
            raise <| System.Exception "bomb1"
        } |> Async.Start
        File.AppendAllText( @"d:\a.txt", "CFoo.goo " + ( Thread.GetDomain().FriendlyName ) + "\r\n" )



let bar ( str : string ) =
    File.AppendAllText( @"d:\a.txt", "bar start " + ( Thread.GetDomain().FriendlyName ) + "\r\n" )

    // 新規にアプリケーションドメインを作る
    let ad2 = AppDomain.CreateDomain( "domain2", null, null )
    let rfoo =
        ad2.CreateInstanceAndUnwrap(
            typeof<CFoo>.Assembly.FullName,
            typeof<CFoo>.FullName
        ) :?> CFoo

    // 呼び出す
    rfoo.goo ()
    File.AppendAllText( @"d:\a.txt", "rfoo.goo called " + ( Thread.GetDomain().FriendlyName ) + "\r\n" )

    // ここで待っている間に、domain2内の別のスレッドで例外が発生するはず
    Thread.Sleep 5000

    // 例外が発生したアプリケーションドメインがどうなったのか?
    try
        File.AppendAllText( @"d:\a.txt", ad2.FriendlyName + "\r\n" )
    with
    | _ as e ->
        File.AppendAllText( @"d:\a.txt", "domain2 was unloaded \r\n" )

    File.AppendAllText( @"d:\a.txt", "bar end " + ( Thread.GetDomain().FriendlyName ) + "\r\n" )
    0

// C++で作った自作のローダ(?)から起動した場合は、下記は実行されない
[<EntryPoint>]
let main argv =
    bar "a"


これを実行すると、以下のような結果が(d:\a.txtに)出力される。(※、一応書いておくと、C++の実行バイナリとF#の実行バイナリは同一フォルダに格納する必要がある)

bar start DefaultDomain
CFoo.goo domain2
rfoo.goo called DefaultDomain
handler called domain2
   場所 Microsoft.FSharp.Core.Operators.Raise[T](Exception exn)
   場所 prog.goo@25-5.Invoke(Unit unitVar)
   場所 Microsoft.FSharp.Control.AsyncBuilderImpl.callA@839.Invoke(AsyncParams`1 args)
--- 直前に例外がスローされた場所からのスタック トレースの終わり ---
   場所 Microsoft.FSharp.Control.CancellationTokenOps.Start@1292-1.Invoke(ExceptionDispatchInfo edi)
   場所 .$Control.loop@425-51(Trampoline this, FSharpFunc`2 action)
   場所 Microsoft.FSharp.Control.Trampoline.ExecuteAction(FSharpFunc`2 firstAction)
   場所 Microsoft.FSharp.Control.TrampolineHolder.Protect(FSharpFunc`2 firstAction)
   場所 .$Control.-ctor@509-1.Invoke(Object state)
   場所 System.Threading.QueueUserWorkItemCallback.WaitCallback_Context(Object state)
   場所 System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   場所 System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
   場所 System.Threading.QueueUserWorkItemCallback.System.Threading.IThreadPoolWorkItem.ExecuteWorkItem()
   場所 System.Threading.ThreadPoolWorkQueue.Dispatch()
   場所 System.Threading._ThreadPoolWaitCallback.PerformWaitCallback()
domain2 was unloaded 
bar end DefaultDomain

わかりにくいが、期待通り、未処理例外が生じたときに、追加で作った"domain2"のアプリケーションドメインだけがアンロードされ、デフォルトのアプリケーションドメインは処理が続行している。

もうちょっと正確に言うと、例外が未処理のままだった場合にはCLRは何もしないで無視するという設定で動作しているのだが、追加で作った"domain2"のアプリケーションドメインでUnhandledExceptionのイベントを登録して、そこでアプリケーションドメインのアンロードを明示的に行うようにしている。

後は、C++で作った処理をF#で実行するようにしてやればいい。実際、上記のF#のコードでは、C++のローダ(?)まがいの物から起動した場合にはmain関数は実行されないが、直接EXEを叩いた場合にはmainから実行される。なので、このmain関数でCLRを(もう1つ追加で)ロードして、明示的に自分自身のアセンブリを読み込んでやれば、同じことがF#だけで実現できるはずである。

でももう疲れた。だれかやってくれないかな。狙ったアプリケーションドメインだけを殺す術については、日を改めて書く。

.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ファイル名」と指定してやればいい。




RAIDをやめた

2017/05/30

同じく記録だけこのしておく。

メインのPCは今までAdaptec RAID 2405でRAID 1Eにしていたのだが、やめてしまった。

RAIDにしていても、
  1. 個人用PCだと、メディア(HDDとかSSD)は数が少ない。相対的にそれ以外のパーツが多くて、障害発生原因はメディアばかりではないということになる。業務システムでの数の暴力とは条件が違いすぎる。
  2. 同一筐体にある同一RAIDグループに属するメディアは、なんとなく同時に壊れる気がする。同じ環境で同じような読み書きをしているのだから。だから、1つでも壊れると、全部交換したくなる。
  3. というか、メディア障害よりも、ソフトウェア的な問題のほうが多い。間違って消したとか。
  4. 実は個人用PCだと、しばらく使えなかったところで、死ぬわけではない。
  5. メディア障害が発生したら、壊れた奴だけ代えて業務続行という使い方をしない。そういう時はOSごと再インストールしている。
ということで、あまりRAIDのありがたみを感じなかった。

それよりもは、6に書いた通り、OSやソフトウェアはインストール媒体から再インストールして、ユーザデータはバックアップから戻すという使い方が多い。

結果、RAIDよりバックアップが重要だ、という方向に宗旨替えすることにした。


電源ボックスを交換した

2017/05/28

記録だけ残しておく。

メインマシンの電源ボックスを変えた。

今のところ何の問題もなく動き続けてくれてはいるのだが、買ってからおよそ7年半経過して、さすがになんとなく怪しい感じがするから、悪いけど引退してもらった。

買ったのはこれ。



SSR-750RMS

1万7千円ぐらいした。

特に絵はない。それに、今のところ、何事もなく動いているため、特に述べるべき感想もない。

そういえば、前の奴もSeasonicだった。外した時の写真を撮っておいたから、面白みもないがさらしておく。



外観はまぁ、ほこりが見えてますね、という程度か。 外から見ると、中に入っているファンがなんだかよくわからなくて、怪しい雰囲気を感じる。



San Ace 120と60でした。さすがに羽根の表面は汚れているが、肝心の中身のほうには、あまりちりやほこりが詰まっている感じがしない。



アップにしてみても、あまり汚れが目立たない。

本当はここで、中にぎっちりとほこりが詰まっていて、その中にゴキブリの卵や死体が点々と埋もれているような、あるいは、ふたを開けた途端にパンドラの箱を開けたがごとくこの世のありとあらゆる悪いものが噴出してくるような、そういう絵面を期待していたのだが、全然そんなことはなかった。

正直言って、面白くない。

7年半蓄えたお前のエネルギーはこの程度だったのかと。

F#での引数なしの関数の扱いについて

2017/05/16

なんとなく、F#での引数なしの関数の書き方がよくわからなくなってきたから、整理してみた。

1.単なる関数の場合

以下のプログラムを考える。

module  FTest
open System


// printfの戻り値であるUnit値を持つ定数になる
let func1 =     // unit
    printf "func1\n"


// unit値を引数にとる関数値になる
let func2 () =  // unit -> unit
    printf "func2\n"


[<EntryPoint>]
let main args =
    printf "main start\n"

    func1       // 意味なし
    func1       // 意味なし
    func2 ()    // ここで実行される
    func2 ()    // ここで実行される

    printf "main end\n"
    0


実行結果は以下となる。



上記では、func1自体は関数ではなくunit値の定数になっている。関数本体の実行はmainが呼ばれる前に行われてしまっている。

func2はunit値を引数に取る関数値である。C言語に例えるのなら関数ポインタの値であり、俺が思うところの引数なしの関数として振る舞ってくれる。

だから、func2については「func2 ()」を記述したところで、その関数の中身である「printf "func2\n"」が実行される。一方、func1はmain関数の中では実行されていない。

2.クラスのメソッドの場合
混乱するのが以下の場合だ。

module  FTest
open System


type CTest() =
    member this.func1 =
        printf "CTest.func1\n"


    member this.func2 () =
        printf "CTest.func2\n"


[<EntryPoint>]
let main args =
    printf "main start\n"
    let c = new CTest()

    c.func1       // ここで実行される
    c.func1       // ここで実行される
    c.func2 ()    // ここで実行される
    c.func2 ()    // ここで実行される

    printf "main end\n"
    0


これを実行すると下記となる。



func1の動きがさっきと違う。さっきと似た様なもんだろうと思うのだが、今度はmain関数の中で実行されている。何なのか?

まぁ、よくわからんが、たぶん自己識別子であるthisが暗黙の引数として扱われている、ということなのではないだろうか。だから「c.func1」が記述されたところで、初めて実行されると。

ということで、次にこうしてみた。

3.スタティックなメソッドの場合

module  FTest
open System


type CTest() =
    static member func1 =
        printf "CTest.func1\n"

    static member func2 () =
        printf "CTest.func2\n"


[<EntryPoint>]
let main args =
    printf "main start\n"
    let c = new CTest()

    CTest.func1       // ここで実行される
    CTest.func1       // ここで実行される
    CTest.func2 ()    // ここで実行される
    CTest.func2 ()    // ここで実行される

    printf "main end\n"
    0


実行するとこうなった。



予想外な結果である。俺としては、CTest.func1は、タイミングは別としても1回だけしか実行されないのではないかと考えたのだが、そうでもないらしい。

4.let束縛にする

なんかむかついたから、こうしてみた。

module  FTest
open System


type CTest() =
    let func1 = printf "CTest.func1\n"


    static member func2 () =
        printf "CTest.func2\n"


    member this.func3 = func1

[<EntryPoint>]
let main args =
    printf "main start\n"
    let c = new CTest()
    printf "new CTest() owari\n"

    CTest.func2 ()  // ここで実行される
    CTest.func2 ()  // ここで実行される
    c.func3         // 意味なし
    c.func3         // 意味なし

    printf "main end\n"
    0


実行結果はこうなる。



で、ここまでやって、ようやく気がついた。

2番目や3番目のfunc1のように、クラスのメソッドにした場合はなぜにメソッドが呼ばれるまで実行されないのか? それは極単純に「member……」と書くか「let……」と書くかの違いに応じて、扱いが変わっているのではないか? わからんけど。

だから、4番目の例では、newしたタイミングでlet束縛の「printf "CTest.func1\n"」が実行されて、「c.func3」は無視されていると思われる。

5.まとめ

引数なしの「member……」と「let……」の扱いが相違するのはなんとなく直観に反する気もするけど、とりあえず、引数なしの関数にはダミーの引数であるunit値を指定させたほうがいいらしい。

少なくともそうすれば、定数のつもりなのか、引数なしの関数のつもりなのかが明らかになるし、関数が実行されるタイミングを間違えることもなくなるだろう。

INTELとRealtecのNICを比べてみた 結果だけ書いておく

2017/04/24

勉強がてら、F#とXAMLで何か作ってみようということで、ネットワークの帯域を食いつぶすだけのベンチマークソフトをでっちあげてみた。



単に、TCPでつないで、全力でごみデータを送受信するだけ。よくあるベンチマークソフトで、今のところ何の独創性もない。

ところで、ここで1つ気になったことがある。とりあえず通信できるようになったところで、2台のマシンをつないでつながるか試したところ、なんか通信速度が安定しない。理屈の上では、上り下りともに1Gbps程度は出るはずなのだが、なんか、送信と受信のどちらか一方の帯域が低い。



上記だと、送信は概ね上限に達しているように見えるが、受信は79.4Mbpsで、理論値の10%にも達していない。また、常に送信は早くて受信が遅いということではなく、時々この力関係が入れ替わったりするから、よくわからない。

ロジック上は、TCPのクライアントとサーバとで、送受信ともに同じようなことをやっているわけで、とりわけ原因があるように見受けられない。まさかNICのチップが蟹さんだからなのか? と思って、いろいろやってみた。

まず構成は下記のようになっている。



PC1をクライアント側として使用する。こいつはオンボードのINTEL NICを使っておく。

PC2をサーバ側として、INTEL NICとRealtek NICを切り替えて使う。

そのうえで、HUBの間にかます場合とクロスで直結する場合を試してみる。

■パターン1 INTEL - Realtek HUBあり

PC1側



PC2側



理想的な性能が得られていない。


■パターン2 INTEL - INTEL HUBあり

PC1側



PC2側



なぜか予想以上に性能が出ない。速度的に、半二重として認識されているのか?

■パターン3 INTEL - Realtek 直結

PC1側



PC2側



さっきよりはいいが、やっぱり理論値には遠い。


■パターン4 INTEL - INTEL 直結

PC1側



PC2側



理論値に近い速度が出ている。

しかし、やっている間に気づいたが、何回か抜き差しすると、ちょっと性能が落ちたり上がったりする。Realtekでも、理論値程度の速度が出るのではないか? と思って何回かやってみたが、理想的な結果は得られなかった。

とりあえず、原因も対処法もまだ調べてない。何はともあれ、PlanexのHUBがかなりできない子だということだけはわかった。

フィードバックHUBのログアウトの方法

2017/04/15

下らないことに気が付いたから書いておく。

Windows 10でMicrosoftにゴルァを入れるのにフィードバックHUBというソフトを使うことができるのだが、これ、一度ログインするとログオフする術がない。

1.フィードバックHUBを起動すると、サインインとやらを求める画面が表示される。



2.サインインする方法を聞かれるので、とりあえずMicrosoftアカウントを選択して「続行」を押下する。



3.ユーザ名とパスワードを聞かれるので入力してやり、「サインイン」ボタンを押下する。



4.苦情を入力できる画面になる。





ここで問題になるのは、この画面をいくら探してもログオフする方法が見当たらないということである。なおかつ、フィードバックHUBを一度閉じて再起動すると、ログインの確認が行われずにいきなり、上記の画面が表示されてしまう。

気持ち悪いことこの上ない。

だから、ログオフする方法を探ってみた。

5.まず、フィードバックHUBの合同記号(「≡」←これ)を選択する。すると、左半分の文字が消える。(背景色と同じで区別がつかないが、おそらくメニューを表示する領域が広がるのだと思われる)



6.画面左下の、歯車の左側の領域を選択する。まさに衝撃的な設計だが、この何もない領域は選択可能なボタンとして機能するらしい。



7.サインアウトと書いてある文字を選択する。明らかに意図的なのだろうが、あえてわざわざ判読しづらい色で書かれているが、これは選択可能なオブジェクトである。右下に見えている「閉じる」ボタンに惑わされてはいけない。



8.無事にログオフできた。



フィードバックHUBを再起動しても、何も聞かれずいきなりログインするとかいうことがなくなる。

だが、上記の画面で「サインイン」ボタンを押下すると、下記のように、アカウントがモロ出しになってしまう。マワシの外れた力士がごとくだ。



しかも、この画面で「続行」ボタンを押下すると、パスワードを聞かれることもなくログインしてしまう。全く意味がない。

なので、アカウントの情報を削除する必要がある。

9.スタートメニューから「設定」を起動する。


10.「アカウント」を選択する。



11.左側のメニューから「メール&アプリのアカウント」を選択する



12.他のアプリで使われるアカウントというところに表示されている、さっき自分がログインするときに使ったアカウントを選択し「取り出す」ボタンを押下する。



「取り出す」ってなんだよ。

13.確認のメッセージが表示されるので「はい」を選択する。






14.さっきと同じく、フィードバックHUBを起動すると、サインインを求める画面が表示される。



15.上記で「続行」ボタンを押下しても、アカウントが表示されなくなる。