いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

.NET クラスライブラリ探訪-039 (System.IO.MemoryMappedFiles.MemoryMappedFile)(メモリマップトファイル, ランダムアクセス, 共有メモリ, 4.0)


System.IO.MemoryMappedFiles.MemoryMappedFileクラスは、.NET 4.0から
追加されたクラスです。文字通り、メモリ上にマッピングされたファイルを扱います。
MMFと略したりします。


メモリマップトファイルは、昔からWin32 APIとして用意されていましたが
4.0より.NET Frameworkにクラスとして登場しました。


MemoryMappedFileの特徴は、ランダムアクセスが速い事です。
メモリ内にマップされているのでシークする必要がありません。
逆にシーケンシャルアクセスは苦手です。


逆にFileStreamはシーケンシャルアクセスが得意で、ランダムアクセスが苦手です。


また、MemoryMappedFileは共有メモリとしても利用出来ます。
つまり複数のプロセス間で共有することが出来ます。


で、実際の利用方法ですが
MemoryMappedFileオブジェクトは、newするのではなくMemoryMappedFileクラス
に用意されている以下のstaticメソッドを利用して構築します。

  • CreateFromFile
    • ディスク上のファイルからMemoryMappedFileを作成します。
  • CreateNew
    • ディスク上のファイルと関連づけられていないMemoryMappedFileを作成します。
  • CreateOrOpen
    • ディスク上のファイルと関連づけられていないMemoryMappedFileを作成もしくは開きます。
  • OpenExisting
    • 指定された名前の既にマッピングされているMemoryMappedFileを開きます。


上記のどのメソッドを利用する場合でも、通常MemoryMappedFileオブジェクトを
構築する際は、以下のように2つのusingを利用します。

using (var mmf = MemoryMappedFile.CreateFromFile(BIN_FILE_NAME))
{
    using (var accessor = mmf.CreateViewAccessor())
    {
        // データの読み書きを行う。
    }
}

一つ目のusingでMemoryMappedFileオブジェクトを取得し、二つ目のusingで
データにアクセスするためのMemoryMappedViewAccessorを取得します。


後は取得したMemoryMappedViewAccessorでデータを読み書きします。


データの読み書きには以下のメソッドを利用します。

  • ReadXXX (XXXの部分にはそれぞれの型名が入る。(int32, byte, double, charなど))
    • 指定された型でデータを取得します。
  • Read
    • T型の構造体を指定してデータを読み取ります。
  • ReadArray
    • T型の配列を指定してデータを読み取ります。
  • Write
    • 指定したデータを書き込み。(各型用にオーバーロードが用意されています。)
  • Write
    • T型の構造体を指定してデータを書き込みます。
  • WriteArray
    • T型の配列を指定してデータを書き込みます。


以下、データの基本的な読み書きのサンプルです。
以下のサンプルでは最初に大きなバイナリファイルを作成して
そのファイルをメモリ上にマッピングし、データを読み書きしています。

using System;
using System.IO;
using System.IO.MemoryMappedFiles;

namespace MemoryMappedFilesSample
{
    class Program
    {
        private const string BIN_FILE_NAME = "bigdata.bin";

        static void Main(string[] args)
        {
            if (File.Exists(BIN_FILE_NAME))
            {
                File.Delete(BIN_FILE_NAME);
            }

            //
            // 元となるバイナリファイルを作成.
            //
            File.WriteAllBytes(BIN_FILE_NAME, new byte[1000000]);
            
            //
            // 元々ファイルが存在する状態での読み込み.
            //
            using (var mmf = MemoryMappedFile.CreateFromFile(BIN_FILE_NAME))
            {
                using (var accessor = mmf.CreateViewAccessor())
                {
                    //
                    // 書き換える前のデータを確認.
                    //
                    Console.WriteLine(accessor.ReadByte(500000));

                    //
                    // データを書き換える.
                    //
                    accessor.Write(500000, (byte) 77);
                    
                    //
                    // 書き換えた位置のデータを取得して確認.
                    //
                    Console.WriteLine(accessor.ReadByte(500000));

                    //
                    // 配列を指定して指定範囲のデータを取得.
                    //
                    byte[] buf = new byte[10];
                    accessor.ReadArray<byte>(499996, buf, 0, buf.Length);

                    Console.WriteLine(string.Join<byte>(",", buf));

                    //
                    // 配列を指定して指定範囲のデータを設定.
                    //
                    buf = new byte[] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
                    accessor.WriteArray<byte>(499996, buf, 0, buf.Length);

                    buf = new byte[10];
                    accessor.ReadArray<byte>(499996, buf, 0, buf.Length);
                    Console.WriteLine(string.Join<byte>(",", buf));
                }
            }
        }
    }
}

実行結果は以下のようになります。

0
77
0,0,0,0,77,0,0,0,0,0
10,20,30,40,50,60,70,80,90,100


次に、MemoryMappedFileを共有メモリとして利用しているサンプルです。
以下のサンプルは2つのexeを用意して、一つはデータの書き込み、一つはデータの読み込みを
同じ共有メモリに対して行います。その際、MemoryMappedFileには同じ名前を指定するようにします。


まず書き込み側。

using System;
using System.IO.MemoryMappedFiles;
using System.Threading;

namespace CreateNewMemoryMappedFile
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var mutex = new Mutex(true, "MMF_MUTEX"))
            {
                Console.WriteLine("===LOCK=== {0} {1}", DateTime.Now.ToLongTimeString(), DateTime.Now.Millisecond);

                //
                // 新規でMemory Mapped Fileを作成.
                //
                using (var mmf = MemoryMappedFile.CreateNew("data.bin", 500))
                {
                    using (var accessor = mmf.CreateViewAccessor())
                    {
                        accessor.Write(10, 12345);

                        mutex.ReleaseMutex();
                        Console.WriteLine("12345 -- write");
                        Console.WriteLine("===RELEASE=== {0} {1}", DateTime.Now.ToLongTimeString(), DateTime.Now.Millisecond);

                        Console.WriteLine("CreateNewMemoryMappedFile -- Write");
                        Console.ReadLine();
                    }
                }
            }
        }
    }
}


次に読み込み側

using System;
using System.IO.MemoryMappedFiles;
using System.Threading;

namespace ReadSharedMemoryMappedFile
{
    class Program
    {
        static void Main(string[] args)
        {
            Mutex mutex = null;
            while (mutex == null)
            {
                try
                {
                    mutex = Mutex.OpenExisting("MMF_MUTEX");
                }
                catch
                {
                    // re-try.
                    Console.WriteLine("Retry....");
                }                
            }

            //
            // すでに存在するMemory Mapped Fileを読み込み.
            //
            using (mutex)
            {
                if (mutex.WaitOne())
                {
                    Console.WriteLine("===LOCK=== {0} {1}", DateTime.Now.ToLongTimeString(), DateTime.Now.Millisecond);

                    int data = -1;
                    using (var mmf = MemoryMappedFile.OpenExisting("data.bin"))
                    {
                        using (var accessor = mmf.CreateViewAccessor())
                        {
                            data = accessor.ReadInt32(10);
                        }
                    }

                    mutex.ReleaseMutex();
                    Console.WriteLine("{0} -- Read", data);
                    Console.WriteLine("===RELEASE=== {0} {1}", DateTime.Now.ToLongTimeString(), DateTime.Now.Millisecond);

                    Console.WriteLine("ReadSharedMemoryMappedFile -- Read");
                    Console.ReadLine();
                }
            }
        }
    }
}


最後に二つを起動するプログラム

using System;
using System.Diagnostics;

namespace MemoryMappedFilesSample2
{
    class Program
    {
        static void Main(string[] args)
        {
            //
            // 2つのExeを順に起動.
            //
            Process.Start("CreateNewMemoryMappedFile.exe");
            Process.Start("ReadSharedMemoryMappedFile.exe");

            Console.WriteLine("MemoryMappedFileSample2 -- Main");
            Console.ReadLine();
        }
    }
}

実行すると、書き込み側では

12345 -- Write

と表示され、読み込み側では

12345 -- Read

と表示されます。


また、説明の部分でも記述しましたがaccessorにデータを設定する際に
構造体を指定することも出来ます。(Read, Write)
こうするとデータを纏めて読み書きできます。


構造体を利用する際の注意点ですが、この場合の構造体には
参照を含む型は定義できません。つまり構造体のメンバーとして

public char[] Chars;

とは定義できません。定義できるのは参照を含まない型のみです。(intなど)


んじゃ、文字列はどうするの?となるのですが
文字列の場合は、バイト配列に変換してWriteArrayで書き込みます。
当然読み込みはReadArrayを利用します。


以下、構造体と文字列を使用したサンプルです。

using System;
using System.IO.MemoryMappedFiles;
using System.Runtime.InteropServices;
using System.Text;

namespace MemoryMappedFilesSample3
{
    class Program
    {
        static void Main(string[] args)
        {
            //
            // 500バイトのMMFを新規作成する.
            //
            using (var mmf = MemoryMappedFile.CreateNew("data.bin", 500))
            {
            	using (var accessor = mmf.CreateViewAccessor())
                {
                    //
                    // 構造体と文字列を書き込み.
                    //    構造体を指定する場合、その構造体に参照を含む型が存在していると
                    //    エラーとなる。
                    //
                    // 文字列を書き込む場合は、Encodingクラスを利用してバイト配列に変換し
                    // WriteArray<T>メソッドを利用して書き込む。
                    //
                    // 読み込む場合は、ReadArray<T>を利用して読み込む.
                    //
                    // 構造体のサイズを取得する場合は、Marshal.SizeOfメソッドを利用する.
                    //
                    Data data;
                    data.X = 100;
                    data.Y = 200;

                    byte[] bytes = Encoding.UTF8.GetBytes("Hello World");

                    //
                    // データ書き込み.
                    //
                    const int START_POSITION = 10;
                    int structSize = Marshal.SizeOf(data);

                    accessor.Write<Data>(START_POSITION, ref data);
                    accessor.WriteArray<byte>(START_POSITION + structSize, bytes, 0, bytes.Length);                    

                    //
                    // データ読み込み.
                    //
                    Data data2;
                    accessor.Read<Data>(START_POSITION, out data2);

                    byte[] bytes2 = new byte[bytes.Length];
                    structSize = Marshal.SizeOf(data2);
                    accessor.ReadArray<byte>(START_POSITION + structSize, bytes2, 0, bytes2.Length);

                    //
                    // 確認.
                    //
                    Console.WriteLine(data2);
                    Console.WriteLine(Encoding.UTF8.GetString(bytes2));
                }
            }
        }
    }

    struct Data
    {
        public int X;
        public int Y;

        public override string ToString()
        {
            return string.Format("X={0}, Y={1}", X, Y);
        }
    }
}

実行すると

X=100, Y=200
Hello World

と表示されます。



最後に、unsafeブロックを使用して直接ポインタ経由で
データを読み書きするサンプルです。


ポインタを取得するには、まずメモリマップトファイルのビューハンドルを
取得し、そこからメモリブロックのポインタを取得します。
ビューハンドルはMemoryMappedViewAccessorの以下のメソッドで取得します。

accessor.SafeMemoryMappedViewHandle

ビューハンドルを取得したらそのままAcquirePointerメソッドでポインタを取ります。
後はそのポインタを利用して読み書きします。


以下、サンプルです。

using System;
using System.IO.MemoryMappedFiles;

namespace MemoryMappedFilesSample4
{
    class Program
    {
        static void Main(string[] args)
        {
            unsafe
            {
                using (var mmf = MemoryMappedFile.CreateOrOpen("data.bin", 500))
                {
                    //
                    // ポインタを利用してデータ書き込み.
                    //
                	using (var accessor = mmf.CreateViewAccessor())
                    {
                        //
                        // メモリマップトファイルのビューハンドルを取得し
                        // メモリブロックのポインタを取得.
                        //
                    	byte* p = null;
                        accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref p);

                        //
                        // 取得したポインタを任意の型のポインタにキャストしてデータを設定.
                        //
                        Data* data = (Data*) p;

                        data->X = 100;
                        data->Y = 200;
                    }

                    //
                    // ポインタを取得してデータを読み込み.
                    //
                    using (var accessor = mmf.CreateViewAccessor())
                    {
                        //
                        // メモリマップトファイルのビューハンドルを取得し
                        // メモリブロックのポインタを取得.
                        //
                        byte* p = null;
                        accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref p);

                        //
                        // 書き込み時に利用した型のポインタにキャストしてデータを読み込み.
                        //
                        Data* data = (Data*) p;

                        Console.WriteLine(*data);
                    }                
                }
            }
        }
    }

    struct Data
    {
        public int X;
        public int Y;

        public override string ToString()
        {
            return string.Format("X={0}, Y={1}", X, Y);
        }
    }
}

================================
過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。