2018/03/21

共有メモリにInterlocked.Increment

共有メモリとして確保した領域に対して、Interlocked.Incrementで値を加算したい。

まず、共有メモリに対する値の読み書きは、MemoryMappedViewAccessorクラスMemoryMappedViewStreamクラスを用いてアトミックでないアクセスを行うか、全て自己責任で生のアドレスを使ってどうにかするしか方法は無い。

無論、アトミックでない方法を用いることはできない。

open System.Threading
open System.IO.MemoryMappedFiles

[<EntryPoint>]
let main args =
    // 共有メモリ
    let mmapFile : MemoryMappedFile =
        MemoryMappedFile.CreateOrOpen( "abc", 4L )
    let acc =
        mmapFile.CreateViewAccessor()
    
    // 値を取得して
    let mutable v = acc.ReadInt32( 0L )

    // アトミックに加算する!
    Interlocked.Increment( &v ) |> ignore
    
    // 書き込む
    acc.Write( 0L, v )

    0

ジョークにもならない。

だが、Interlocked.Incrementが取る引数はbyrefのintである。C#ならば「public static int Increment( ref int location )」、F#ならば「static member Increment : location:int byref -> int」だ。通常、共有メモリ上の領域を直接Interlocked.Incrementの引数として与える方法がない。

ならば、Win32APIにあるInterlockedIncrement関数を使えばよいではないか。環境依存ではないかという良心の呵責も聞こえてくるが、もとより.NETだと思えばもはやどうでもよろしい。

open System.Runtime.InteropServices
open Microsoft.FSharp.NativeInterop
open System.Threading
open System.IO.MemoryMappedFiles

[<DllImport("Kernel32")>] extern int32 InterlockedIncrement( nativeint p );

[<EntryPoint>]
let main args =
    // 共有メモリ
    let mmapFile : MemoryMappedFile =
        MemoryMappedFile.CreateOrOpen( "abc", 4L )
    let handle =
        mmapFile.CreateViewAccessor().SafeMemoryMappedViewHandle

    // 共有メモリ領域のアドレスを取得する
    let mutable p : nativeptr<byte> =
        NativePtr.ofNativeInt( nativeint 0 )
    handle.AcquirePointer( &p )

    // アドレスを指定して、アトミックに加算する
    InterlockedIncrement( NativePtr.toNativeInt p ) |> ignore

    handle.ReleasePointer()

    0

このコードは、一応想定通りに動作する。はずだ。InterlockedIncrementで加算している個所を複数のプロセスやスレッドでゴリゴリ実行しても、おかしなことにはならないはずだ。

だが環境依存ということを除いても致命的な欠陥がある。

32bitのコードでしか動作しないのだ。

64bit版でビルドすると、Kernel32.dllにInterlockedIncrementというエントリがない、と言って怒られる。



どういうことなのか、アンマネージドなC++のコードで試してみる。



こんなものをビルドしてアセンブラを確認してみると、



直接マシン語命令に展開されている匂いがする。

というか、こんなページもあった。

https://msdn.microsoft.com/ja-jp/library/2ddez55b.aspx

結局、32bit版では(互換性のためか?)Kernel32.dllにInterlockedIncrement関数(本物の関数だ)が用意されているが、今では呼び出し元に直接マシン語コードを生成するようになっているから、64bit版ではそんなものは提供されないのではないか。

となると、P/Invokeで逃げるという手段も塞がれる。.NETでは、共有メモリを使ってプロセス間でInterlockedIncrementはできないのだろうか。


そう思っていたら、なんとも都合の良いことに、Visual Studio 2017がこの間アップデートされて15.6が公開されたのだが、その中にこんな記述を見つけた

「NativePtr.ByRef のサポートの追加」

リンク先を辿っていくと、IntPtrの値を Interlocked.CompareExchangeに渡したいとか何とか、書いてあるように見える。

ということで、やってみた。

open Microsoft.FSharp.NativeInterop
open System.Threading
open System.IO.MemoryMappedFiles

[<EntryPoint>]
let main args =
    // 共有メモリ
    let mmapFile : MemoryMappedFile =
        MemoryMappedFile.CreateOrOpen( "abc", 4L );
    let handle =
        mmapFile.CreateViewAccessor().SafeMemoryMappedViewHandle

    // 共有メモリ領域のアドレスを取得する
    let mutable p : nativeptr<byte> =
        NativePtr.ofNativeInt( nativeint 0 )
    handle.AcquirePointer( &p )

    // 型を変える
    let pint : nativeptr<int> = NativePtr.ofNativeInt( NativePtr.toNativeInt( p ) )

    // アドレスを指定して、アトミックに加算する
    Interlocked.Increment( NativePtr.toByRef pint )

    handle.ReleasePointer()

    0


ここで、プロジェクトの設定を変えて、ターゲットF#ランタイムをFSharp.Core 4.4.3.0に変えてやらなければならない。



そうすると、NativePtr.toByRefが使えるようになる。

どうもこいつは、「nativeptr<'T> -> byref<'T>」なる関数の様で、Interlocked.Incrementの引数がbyref intであるため、指定するアドレスとしてはnativeptrでなければならない。なので、著しく見苦しいが事前に共有メモリ領域のアドレスとして取得したnativeptrの値をnativeptrにキャストしている。

恐ろしくばっちいが、コンパイラを騙しているだけで実行時に負荷にはならないと思えば、まぁいいか。とりあえず、これで32bit版でも64bit版でも動作するし、P/Invokeにも頼らずに、目的を達することができるようになった。