Wie bereits im vorherigen Beitrag als Idee angesprochen, habe ich die Möglichkeit der dynamischen Anpassung des Abfrage-Intervalls der SharedMemoyDependency-Klasse umgesetzt. Um diese Idee umzusetzen mussten einige Änderungen sowohl in der SharedMemory-Klasse als auch in der SharedMemoryDependency-Klasse eingebracht werden. Im Zuge der Änderungen habe ich die Methode AddObject() der SharedMemory-Klasse in InsertObject() umbenannt. Meiner Meinung nach beschreibt der neue Methodenname die Funktion der Methode besser da ja kein Objekt einer bestehenden Auflistung hinzugefügt, sondern in das SharedMemory-Segment eingefügt wird. Dabei wurde auch gleich für die beiden Methoden InsertObject(object) und GetObject() eine neue Überladung mit einem zusätzlichen Parameter von Typ bool eingefügt. Der zusätzliche Parameter autoLock legt fest ob die Sperre des Mutex der Klasse von der jeweiligen Methode verwaltet wird, oder vom Aufrufer der Methode. Auch habe ich die InsertObject()-Methode dahingehend Überarbeitet, dass jetzt nur noch alle 200 Millisekunden ein Objekt in das SharedMemory-Segment eingefügt werden kann. Sollte der Zeitintervall seit dem letzten Einfügen kürzer sein, wird in einer Schleife die Methode blockiert, bis der Zeitintervall erreicht ist. Eine ähnlich Begrenzung als minimalen Zeitintervall zwischen zwei Abfragen wird auch in der SharedMemoryDependency-Klasse verwendet. Hier wird der Zeitraum mittels einer Konstanten auf 20 Millisekunden begrenzt. Dies sind Werte die ich für sinnvoll erachte. Bei kleinen Objekten im SharedMemory-Segment können kürzere Intervalle vielleicht Sinn machen. Doch in der Regel sind es meist größere und komplexere Objekte die zwischen zwei Prozessen ausgetauscht werden sollen und ein DataSet oder ähnliches Objekt alle 200 Millisekunden zu aktualisieren müsste mehr als ausreichend sein. Im folgenden Diagramm sind noch einmal die Klassen SharedMemory und SharedMemoryDependency sowie die verwendetet Enumeration zu sehen.

In der Klasse SharedMemory ist die neue Eigenschaft SerializedObjektSize vom Typ Int32 verfügbar. Mit ihr kann die serialisierte Größe des Objekts im Speicher abgerufen werden. Diese ist in sofern von Interesse, da die Größe eines serialisierten Objekts doch sehr stark von der Größe des Objekts vor der Serialisierung abweicht. Der Typ Int32 benötigt normalerweise 4 byte auf einer x86 Plattform. Serialisiert belegt ein Int32-Wert 54 byte im Speicher auf derselben Plattform. In der Regel braucht sich nicht um die Größe des Objekts welches im SharedMemory-Segment gespeichert werden soll gekümmert zu werden, da die benötigte Größe von der Methode InsertObject(object) automatisch ermittelt wird. Sollte doch einmal die serialisierte Größe eines Objekts ermittelt werden müssen, stehen hierfür die statischen Methode GetMinMemorySize(object) und GetOptMemorySize(object) zur Verfügung. Die erste Methode gibt die genaue serialisierte Größe des Objekts zurück. Die zweite Methode gibt die Größe optimiert auf 8 byte Blöcke zurück wobei der zurückgegebene Wert immer auf auf die nächsten 8 byte aufgerundet wird. Im Falle eines In32-Wertes gibt die erste Methode 54 und die zweite Methode 56 byte zurück. Die Größe des Objekts, welches im SharedMemory-Segment gespeichert werden kann wurde auf 500 Mb begrenzt. Größere Objekte dürften nur in den wenigsten Fällen benötigt werden. Auch gibt es für größere Objekte bessere Methoden wie etwa die Verwendung eines Memory-mapped File.
Soweit zu den Anpassungen und nun zur eigentlichen Dynamisierung des Abfrage-Intervalls der SharedMemoryDependency-Klasse. Das ganze kann nur funktionieren, wenn ein Verkürzen des Intervalls sofort geschieht und die Verlängerung in Abständen vorgenommen wird. Das es geklappt hat kann man an folgendem Video sehen.
IPC Testing mit dynamischen Abfrageintervall
Um keine Änderungen im SharedMemory Segment zu übersehen, muss zur dynamischen Anpassung des Abfrage-Intervalls ein Algorithmus mit aggressiver Verkürzung und progressiver Verlängerung des Intervalls gefunden werden. Ich muss vorausschicken, ich bin kein Algorithmus-Profi. Deshalb gibt es bestimmt elegantere Implementierungen als die, die ich mir ausgedacht habe. Zur Ermittlung der Zeit die zwischen zwei erkannten Änderungen liegt, verwende ich eine Instanz der Stopwatch-Klasse. Diese wird im in der Methode Start(), welche die Überwachung auslöst, gestartet. Wenn nun in der Methode CheckSegment(), der Delegat der verwendeten Timer-Klasse, eine Änderung im SharedMemory Segment erkannt wird, wird die ermittelte Zeit optimiert und an den Timer übergeben. Anschließend wird die Stopwatch-Instanz neu gestartet. Hier das Listing der beschriebenen Methode:
private void CheckSegment(object state)
{
// den aufrufenden Timer als neue Instanz erzeugen.
Timer stateTimer = (Timer)state;
// den aktuellen Wert aus dem Schlüssel-Segment lesen
int i = this.segment.GetStatus();
// wenn Rückgabewert 1, würde das Objekt im
// SharedMemory Segment geändert
if (i == 1)
{
// prüfen ob autoAdjust aktiv ist
if (this.autoAdjust)
{
// prüfen ob die stopWatch läuft.
if (this.stopWatch.IsRunning)
{// stopWatch anhaltenthis.stopWatch.Stop();
// und die verstrichene Zeit in Millisekunden an die// Optimierungs-Methode übergebenthis.OptimizeIntervall(this.stopWatch.ElapsedMilliseconds);
// und die geänderte pollTime einstellenstateTimer.Change(0, this.pollTime);
}
}
// das Objekt aus dem SharedMemory-Segment holen
object data = this.segment.GetObject(true);
// EventHandler initialisieren
EventHandler<SharedMemoryNotificationEventArgs> handler = this.OnChanged;
if (handler != null)
{
// Event auslösen
handler(this, new SharedMemoryNotificationEventArgs(data, this.segment.SerializedObjectSize, this.pollTime));
}
// Änderung signalisieren
this.hasChanged = true;
// wenn autoAdjust aktiv
if (this.autoAdjust)
{
// stopWatch rücksetzen und neu starten.
this.stopWatch = Stopwatch.StartNew();
}
}
else if (i == -1)
{
// Objekt nicht im Speicher vorhanden oder es kann nicht
// darauf zugegriffen werden.
// Den aufrufenden Timer zerstören
stateTimer.Dispose();
// Überwachung als beendet signalisieren
this.watching = false;
// Ausnahme auslösen
throw new SharedMemoryException(
"Auf das Schlüssel-Segment im Speicher kann nicht zugegriffen werden.");
}
}Nachdem die Ermittlung der benötigten Zeit zwischen zwei Änderungen geklärt ist, kommt jetzt die Optimierung des Zeitintervalls an die Reihe. Um den ganzen Vorgang erst einmal zu starten, wird beim Initialisieren der Klasse der kleinste erlaubte Intervall von 20 Millisekunden verwendet. Bei Eintritt der ersten erkannten Änderung, liegt jetzt ein Zeitraum vor. Als Intervall für die Abfragen auf Änderungen, habe ich 10% des verstrichenen Zeitraums veranschlagt; also theoretisch immer 10 Abfragen zwischen zwei erkannten Änderungen. 10 Abfragen deshalb, um auch auf einen kürzeren Zeitraum zwischen zwei Änderungen regieren zu können. Zunächst wird also ein temporärer Intervall aus dem verstrichenen Zeitraum berechnet. Jetzt wird geprüft ob dieser temporäre Intervall kleiner als der aktuelle und größer als der minimal erlaubte Intervall ist. Sollte diese Bedingung erfüllt sein, wird der temporäre Intervall als neue Polltime verwendet und somit der Abfrageintervall sofort verkürzt. Um auch die maximale Polltime zu verringern, wird nach dreimaligem Unterschreiten der maximalen Polltime der Wert dieser mit dem gerade ermittelten Intervall ersetzt. Sollte der temporäre Intervall größer als der maximal ermittelte Intervall sein, wird nach dreimaligem überschreiten des Maximalwertes der Wert neu gesetzt. Somit ist gewährleistet, dass eine Verkürzung des Intervalls sofort eintritt und eine Verlängerung erst nach mehrmaligem Überschreiten des Maximalwertes. Hier nun das Listing der Methode OptimizeIntervall:
private void OptimizeIntervall(long intervall)
{
// prüfen ob der Parameter intervall eine zulässige Größe aufweist.
if (intervall > (long)int.MaxValue)
{
// intervall ist zu groß für einen int32-Wert.
// auf den größtmöglichen Integerwert setzen.
intervall = (long)int.MaxValue;
}
// intervall in einen int32-Wert casten.
int tempTime = (int)intervall;
// temporäre Polltime erstellen mit theoretisch 10 Abfragen
// zwischen zwei erkannten Änderungen
int tempPollTime = tempTime / 10;
// prüfen ob der aktuelle Intervall größer als der minimale Intervall ist
if (this.pollTime == minPollTime)
{
// aktuellen Intervall auf ermittelten Intervall setzen
this.pollTime = tempPollTime;
}
// prüfen ob die temporäre Polltime kleiner als die aktuelle Polltime
// und größer als der minimal erlaubte Intervall
if (tempPollTime < this.pollTime && tempPollTime > minPollTime)
{
// tempPollTime ist kleiner, pollTime auf aktuellen Wert setzen.
this.pollTime = tempPollTime;
// prüfen wie oft der Intervall schon kleiner war
// als die maxPollTime
// Wenn der Intervall bereits 3 mal kleiner war,
if (this.minTimeCounter >= 3)
{
// maxPollTime auf aktuellen Wert als neuen Vergleich setzen
this.maxPollTime = tempPollTime;
// Counter zurücksetzen
this.minTimeCounter = 0;
}
else
{
// Counter erhöhen
this.minTimeCounter++;
}
}
// prüfen ob der aktuelle Intervall größer ist als
// der größte gemessene Intervall.
if (tempPollTime > this.maxPollTime)
{
// prüfen wie oft der Intervall schon größer war
// als die maxPollTime.
// Wenn der Intervall bereits 3 mal größer war,
if (this.maxTimeCounter >= 3)
{
// maxPollTime auf aktuellen Wert als neuen Vergleich setzen
this.maxPollTime = tempPollTime;
// aktuelle pollTime auf aktuellen Wert setzen
this.pollTime = tempPollTime;
// Counter zurücksetzen
this.maxTimeCounter = 0;
}
else
{
// Counter erhöhen
this.maxTimeCounter++;
}
}
}Im angehängten Demo sowie im obigen Video kann man sehr gut erkennen wie die Dependency-Klasse auf Veränderungen des Intervalls zwischen den Änderungen im SharedMemory-Segment reagiert. Natürlich können in der vorliegenden Lösung auch feste Abfrageintervalle verwendet werden. Es braucht im Konstruktor nur ein Wert größer 0 angegeben zu werden.
IpcTest_Dynamic_PollTime
Wenn ihnen der Artikel gefallen hat oder er für sie hilfreich war, bitte "kicken" sie ihn.
