wrz
14
2012

Komunikacja pomiędzy procesami za pomocą MemoryMappedFile

Czasami zdarza się, że potrzebujemy wymienić jakieś proste informacje pomiędzy procesami a nie chcemy budować całej infrastruktury komunikacyjnej w stylu WCF, socketów itp. Na taką prostą sytuację wymagającą komunikacji interprocesowej ostatnio natrafiłem.Otóż mam sobie aplikację WPF-ową która działa w Tray-u. Okno główne odpala tylko po kliknięciu na Tray-a. Oczywiście chcemy, żeby w danej chwili uruchomiona była tylko jedna instancja takiej aplikacji, więc zrobiłem zabezpieczenie na Mutexie (działa to w ten sposób, że aplikacja próbuje utworzyć nowy obiekt typu Mutex, a w przypadku kiedy już taki istnieje to znaczy że mamy odpaloną inna instancję aplikacji i tę należy zamknąć). I działa to OK, tylko problem w tym że jeśli aplikacja wisi tylko w Tray-u i ktoś klika na ikonkę aplikacji to nic się nie dzieje (nie otwiera nowego okna ani nie pokazuje starego). A chcemy, żeby ten "drugi" proces nim zostanie zamknięty wymusił w jakiś sposób otwarcie głównego okienka należącego do "pierwszego" procesu. 

Prostym i niezwykle wydajnym rozwiązaniem jest użycie w tym przypadku MemoryMappedFile. Należy on do przestrzeni System.IO i pełni dwie funkcje:

  • Zapewnia "obróbkę" w pamięci bardzo dużych plików
  • Umożliwia dostęp do danego obszaru pamięci wielu procesom
Nam chodzi oczywiście o to drugie zastosowanie. W moim przypadku rozwiązanie problemu polega na implementacji dwóch kroków:
  1. Ustawienie przez proces "zamykany" wartości w pamięci informującej o konieczności uruchomienia okna aplikacji
  2. Sprawdzanie przez główny proces wartości tej pamięci i w przypadku zmiany odpalenia okna

Implementację pierwszego kroku prezentuje fragment kodu:

private bool SingleInstanceCheck()
        {
            bool created;
            _singleInstanceMutex = new Mutex(true, "VU.ClientGUI.Mutex", out created);
            if (!created)
            {
                var mappedFile = System.IO.MemoryMappedFiles.MemoryMappedFile.CreateOrOpen("ShowWindow", 1);
                using (var access = mappedFile.CreateViewAccessor(0,1))
                {
                    access.Write(0, true);
                }
                Shutdown();
                return false;
            }
            return true;
        }

W tej funkcji mamy najpierw sprawdzanie istnienia mutexu a przypadku kiedy on istnieje następuje ustawienie wartości pamięci. Obiekty typu MemoryMappedFile są identyfikowane prosto po nazwie, przy ich inicjalizacji zawsze musimy podać ich oczekiwany rozmiar. W naszym przypadku używamy najprostszego możliwego typu Boolean, który oczywiście zajmuje tylko 1 bajt.

Jeśli chodzi o implementację drugiego kroku, to musimy zapewnić mechanizm okresowego sprawdzania zawartości pamięci. Ja zastosowałem najprostsze rozwiązanie: osobny wątek który to robi:

var thread = new Thread(new ThreadStart(() =>
                                                        {
                                                            var file = System.IO.MemoryMappedFiles.MemoryMappedFile.CreateOrOpen("ShowWindow", 1);
                                                            var accessor = file.CreateViewAccessor(0, 1);
                                                            while (true)
                                                            {
                                                                if (accessor.ReadBoolean(0))
                                                                {
                                                                    if (ShowMainWindowRequest != null)
                                                                    {
                                                                        ShowMainWindowRequest(this, null);
                                                                        accessor.Write(0, false);
                                                                    }
                                                                }
                                                                Thread.Sleep(200);
                                                            }
                                                        }));
            thread.Start();

Jak widać, pobieramy zawartość pamięci korzystając z tej samej nazwy co w kroku 1. Po pobraniu wartości i sprawdzeniu, że jest true delegujemy do wątku GUI zadanie pokazania istniejącego okna. W przypadku WPF taki zadanie wygląda tak:

application.ShowMainWindowRequest += (o, e) => Dispatcher.Invoke((Action)Show);

Metoda Show() to standardowa metoda Window która oczywiście wyświetla okno.

Jak widać, użycie MemoryMappedFile jest niezwykle proste a może rozwiązać całkiem sporą gamę problemów :)