| 

.NET C# Java Javascript Exception

3
Gegeben sei eine Klasse die von IEnumerable ableitet und intern ein Dictionary verwendet. Auf diesem Dictionary sollen verschiedene "Verwaltungsoperationen" ausgeführt werden. Beispielsweise: "Zähle alle Items mit dem Status Failed" oder "Entferne alle Items mit dem Status Proceeded". Solche Operationen geschehen in der Regel in Thread A.

Weiterhin wird die Liste von Thread B verwendet, der eine Foreach-Schleife durchläuft und immer GetEnumerator aufruft um die Items zu verarbeiten. Diese Durchläufe müssen gestoppt oder pausiert werden können. Dabei kann der Thread aber selbst nicht abgebrochen oder angehalten werden.

In GetEnumerator durchlaufe ich per Foreach das Dictionary:
public Item GetEnumerator()
{
var snapshot;

lock(this.Items)
snapshot = new List<string>(this.Items.Keys);

foreach(var snapshotItem in snapshot)
{
if(this.IsStoped)
yield break;

Item operationItem;
lock(this.Items)
{
if(this.Items.ContainsKey(snapshotItem))
operationItem = this.Items[snapshotItem];
}

if(operationItem == null)
{
operationItem = new Item();

lock(this.Items)
{
if(!this.Items.ContainsKey(snapshotItem ))
continue;

this.Items[snapshotItem] = operationItem;
}
}

// do something

yield return operationItem;
}
}


Meine Fragen:

1. ist dieses Vorgehen so in Ordnung oder gibt es daran etwas auszusetzen?
2. Welcher Overhead entsteht bei der Verwendung von lock?

In meinem Fall habe ich zwei Locks pro Schleifenitteration und bin mir nicht sicher ob ich damit nicht vielleicht zuviel Overhead generiere. Die Idee ist einfach, dass das Erstellen des Items eine gewisse Zeit in Anspruch nimmt und in dieser Zeit könnte die Liste für andere Dinge gebraucht werden. Zum Beispiel könnte ihr Inhalt in der Zeit gelöscht werden, weshalb ich im zweiten lock auch prüfen muss ob der Key überhaupt noch vorhanden ist, den ich bearbeite.
22.02.2011
Hendrik Lösch 1,5k 1 9
4 Antworten
6
Ich würde dir gern eine hilfreiche Antwort geben, aber die einzige, die mir dazu einfällt und die ich auch vertreten kann, ist folgende:

Es ist unmöglich, eine seriöse Aussage über das Laufzeitverhalten zu machen, wenn es nicht gemessen wird. Das weiß ich aus Erfahrung. Und selbst die Messergebnisse können sich auf zwei unterschiedlichen Systemen stark voneinander unterscheiden. Und sie hängen erst recht davon ab, wie aktiv die entsprechenden Threads sind, die sich ggf. gegenseitig blockieren.

Wenn es um eine Optimierung geht, würde ich eher an den offensichtlichen Schwachstellen ansetzen. Eine davon sind z.B. die Zeilen

if(this.Items.ContainsKey(snapshotItem))
operationItem = this.Items[snapshotItem];


so ist es wesentlich effektiver, daraus folgendes zu machen:

this.items.TryGetValue(snapshotItem, out operationItem);


Ferner kann Dein Code einen Fehler produzieren, weil operationItem kein Initialwert zugewiesen wird (es ist nicht sicher, dass die Variable auf null gesetzt wird). Die Zuweisung des Initialwertes kann dagegen entfallen, wenn TryGetValue verwendet wird.

Daher mein Fazit: Erst messen, dann optimieren (falls es dann überhaupt noch nötig ist).
22.02.2011
Andreas Ganzer 1,4k 1 8
Auf TryGetValue hätte ich auch selbst kommen können. Man muss dazu sagen, dass der Code nur ein Beispiel ist, die eigentliche Implementierung ist komlizierter.
Hendrik Lösch 22.02.2011
4
Wenn du die Items lockst, dann wartet immer der Thread der verliert. Und zwar so lange, bis das lock aufgehoben wird.
Die lock-Operation an sich kostet kaum Performance, nur das gegenseitige Warten.

Wenn es dir um Performance geht, solltest du dennoch eine non-blocking List oder Queue verwenden.
Hier findest du nähere informationen dazu.
http://en.wikipedia.org/wiki/Non-blocking_algorithm

Google nach: non blocking oder lock-free .net hilft.
22.02.2011
Florian Mätschke 370 1 7
1
Ich würde beide Locks in ein größeres zusammenfassen. Du könntest auch immer gleich 10 items aus this.Items rausziehen und dafür nur einmal locken. Danach arbeitest Du die items ohne lock ab.

Ich würde schätzen, dass Du so 1000000 mal pro Sekunde locken kannst. Anhand dieses Wertes kannst Du abschätzen, ob es für dich ein Problem werden wird. Erwarte nur nicht, dass du bei mehr CPUs auch mehr Performance kriegst - dafür lockst Du bei weitem zu viel.
22.02.2011
alexander 860 2 9
1
Ich möchte noch auf einen weiteren Aspekt hinweisen: IEnumerable<T> hat nicht nur eine Syntax, sondern auch eine Semantik, und zu der gehört

Ein Enumerator bleibt gültig, solange die Auflistung nicht geändert wird. Wenn Änderungen an der Auflistung vorgenommen werden, z. B. durch Hinzufügen, Ändern oder Löschen von Elementen, wird der Enumerator unwiederbringlich ungültig gemacht, und sein Verhalten ist nicht definiert.


Mir scheint, Du verwendest viel Aufwand darauf, irgendwie damit klarzukommen, dass Thread A das Dictionary verändert, während Thread B darüber iteriert. Aber die Definition von IEnumerable<T> bzw. IEnumerator<T> würde es verlangen, in so einem Fall eine InvalidOperationException zu werfen. Siehe dazu auch die Doku auf MSDN.
22.02.2011
Matthias Hlawatsch 13,2k 4 9
Danke für den Hinweis. Das schmeißt das bisherige Konzept völlig über den Haufen. Mhm, es roch an sich schon eigenartig weil man den Start/Stop Aufruf nicht an der verarbeitenden sondern der verarbeiteten Klasse aufruft.
Hendrik Lösch 23.02.2011
Ein Ausweg könnte sein, am Anfang von GetEnumerator das ganze Dictionary zu kopieren (und nicht nur die Keys in eine Liste zu ziehen) - flach oder sogar tief, je nachdem, was Du mit den Items so anstellst. Dann sieht Thread B stabil den Zustand beim ersten MoveNext() auf dem Enumerator. Mit der Semantik von IEnumerable vielleicht gerade noch vereinbar. Ansonsten bleibt Dir wohl nur, auf höherer Ebene eine Synchronisation einzubauen (ich will jetzt iterieren vs. ich will jetzt verwalten) oder das Konzept insgesamt zu hinterfragen.
Matthias Hlawatsch 23.02.2011

Stelle deine .net-Frage jetzt!
TOP TECHNOLOGIES CONSULTING GmbH