So oder zumindest sehr ähnlich waren meine Fragen, bevor ich das erste Mal beim Erstellen von Dokumenten mit dem Open Xml SDK in ein Deadlock gelaufen bin (siehe mein voriger Post zu Multithreading und Open Xml).Was ist denn nun der Unterschied und wozu brauche ich es überhaupt?
Andre Krämer hat mit seinem Kommentar zu meinem letzten Post sehr recht. All zu schnell ist man geneigt zu übersehen, dass Anwendungen basierend auf Web Services sich bereits in einer multithreading Umgebung befinden. Daher empfehle ich, in ähnlichen Szenarien grundsätzlich alle Zugriffe auf Open Xml Parts in einen kritischen Abschnitt aufzunehmen, auf die mindestens eine der folgenden Aussagen zutrifft:
- die maximale Größe des Parts ist nicht absehbar,
- die maximale Größe des Parts ist absehbar und wird voraussichtlich jenseits der 8 MB liegen.
Aber zurück zum Thema: Es gibt zwei Möglichkeiten einen kritischen Abschnitt zu definieren. Zum einen die lock Anweisung und zum anderen die Monitor.Enter() Methode.
Die Klasse Monitor stellt einen Mechanismus zur Verfügung, der den Zugriff auf Objekte steuert. Hierzu wird einem einzelnen Thread ein exklusives Zugriffsrecht auf ein Objekt gewährt. Die Zugriffssperre wird mit dem Aufruf der Methode Enter vergeben. Kein anderer Thread kann dann eine weitere Sperre auf das Objekt erhalten. Die lock Anweisung bewirkt nichts anderes, als dass der Compiler selbst einen try-finally-Block um die Anweisung erzeugt.
Grundsätzlich ist es also egal, welcher Weg der Umsetzung gewählt wird. Es ist lediglich von den eigenen Anforderungen an den Kontext abhängig.
Ist es wirklich egal?
Ja, ist es, denn der Compiler macht aus der lock Anweisung nichts anderes als ein Monitor.Enter und Monitor.Exit in einem try-finally-Block. Dass diese Aussage auch korrekt ist, veranschaulicht ein kleines Beispiel. Zur Einfachheit habe ich nur einen leeren Rumpf für beide Varianten erstellt.Nun vergleichen wir die durch den Compiler generierte IL. Dazu benutze ich den Redgate Reflector. Man sieht, dass es wirklich nahezu identisch ist. Das lock Statement bewirkt in der Tat nichts anderes, als dass der Compiler für einen try- finally –Block sorgt – it’s magic…![]()
Weiter oben schrieb ich, dass die Auswahl kontextabhängig sei. In meinem konkreten Beispiel sollte die Synchronisation der Aufrufe in Abhängigkeit der Stream Größe stattfinden. Ich habe daher den direkten Aufruf des Monitor.Enter() gewählt, damit ich nicht den Quellcode einmal mit lock Anweisung und einmal ohne vorhalten muss. Es wird anhand eines HighWaterMark entschieden, ob das Aufbauen eines kritischen Blocks notwendig ist. Mit dem try-finally-Block um die Methodenaufrufe wird sichergestellt, dass der Monitor auch wieder freigegeben wird.
Eine wesentliche Schwäche existiert nach wie vor. In dem Falle eines Deadlocks, wäre die Anwendung zur Zeit nicht mehr reaktionsfähig. Es ist keine Art von Timeout vorgesehen. Am Beispiel meines letzten Posts, ist deutlich, dass diese Situation schnell unverschuldet entstehen kann, wenn das CLR lock sich nicht lösen lässt, da nativer Code einen Mutex hält.![]()
Was tun, wenn der CLR lock nicht aufgehoben wird?
Aus diesem Grund existiert die Methode TryEnter. Es kann über eine bestimmte Zeitspanne versucht werden, eine exklusive Sperre zu erhalten. Etwas ungewöhnlich finde ich nur, dass im Falle eines Timeout eine ArgumentOutOfRangeException geworfen wird.Es gibt bereits verschiedene Implementierungen eines TimeOut Ansatzes. Einen sehr interessanten, der dem von Ian Griffith sehr ähnlich ist, habe ich bei Eric de Carufel gefunden:
using System; using System.Linq; using System.Threading; namespace MultithreadHelper { public class Safe : IDisposable { private readonly object[] _padlocks; private readonly bool[] _securedFlags; private Safe(object padlock, int milliSecondTimeout) { _padlocks = new[] {padlock}; _securedFlags = new[] {Monitor.TryEnter(padlock, milliSecondTimeout)}; } private Safe(object[] padlocks, int milliSecondTimeout) { _padlocks = padlocks; _securedFlags = new bool[_padlocks.Length]; for (int i = 0; i < _padlocks.Length; i++) _securedFlags[i] = Monitor.TryEnter(padlocks[i], milliSecondTimeout); } public bool Secured { get { return _securedFlags.All(s => s); } } public static void Lock(object[] padlocks, int millisecondTimeout, Action codeToRun) { using (var bolt = new Safe(padlocks, millisecondTimeout)) if (bolt.Secured) codeToRun(); else throw new TimeoutException(string.Format("Safe.Lock wasn't able to acquire a lock in {0}ms", millisecondTimeout)); } public static void Lock(object padlock, int millisecondTimeout, Action codeToRun) { using (var bolt = new Safe(padlock, millisecondTimeout)) if (bolt.Secured) codeToRun(); else throw new TimeoutException(string.Format("Safe.Lock wasn't able to acquire a lock in {0}ms", millisecondTimeout)); } #region Implementation of IDisposable public void Dispose() { for (int i = 0; i < _securedFlags.Length; i++) if (_securedFlags[i]) { Monitor.Exit(_padlocks[i]); _securedFlags[i] = false; } } #endregion } }
0 Kommentare:
Kommentar veröffentlichen