C#: yield return in lock-Kontext?
Ist es zulässig, in einem lock-Kontext yield return ... zu machen?
Der Compiler wirft keinen Fehler, jedoch bekomme ich eine Exception zur Laufzeit an der Stelle, wo der lock-Kontext verlassen wird.
Da ich in der aufrufenden Funktion einen asynchronen Call mache, kann es doch sein, dass sich der Threadkontext ändert und somit der lock() in einem anderen Thread geschlossen wird, als er geöffnet wurde - was ja nicht vorgesehen ist, da lock sich auf einen Threadkontext bezieht.
public IEnumerable<SomeClass> GetAffectedItems(string par)
{
lock (_lockObject)
{
foreach (var e in _myList)
{
if (e.x == par)
{
yield return e;
}
}
} // <- Problem hier!!!
}
Aufruf mit:
public async Task foo()
{
...
...
IEnumerable<SomeClass> items = GetAffectedItems("abc");
foreach (var i in items)
{
...
...
await some_async_function();
...
...
}
}
Jedenfalls bekomme ich eine Exception:
Object synchronization method was called from an unsynchronized block of code.
Kann meine obige Überlegung der Grund für die Exception sein? Dann wäre es ja hochgefährlich, yield return in einem lock-Kontext durchzuführen, oder?
1 Antwort
Am Code ist nichts falsch, dass was ich dort sehe, macht genau dass was die Exception auswirft.
Schau dir mal genau an wie yield return in C# funktioniert, du benutzt es leider nicht effizient bzw. wie man es eigentlich nutzen sollte;
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/statements/yield
zum ersten würde ich aync rausnehmen, da du sonst nicht gut debuggen kannst
Bei yield return wird Iterator-State-Machine verwendet, was bedeutet, dass der Code angehalten und später fortgesetzt werden kann. Das führt dazu, dass der lock-Kontext verlassen wird, bevor die Iteration abgeschlossen ist.
Da yield return die Methode beim ersten Auftreten verlässt und später beim Aufruf des nächsten Werts fortgesetzt wird, endet der lock-Block, sobald der erste Wert zurückgegeben wird. Danach wird der Code außerhalb des lock-Blocks fortgesetzt, was bedeutet, dass keine Synchronisierung mehr vorhanden ist, wenn der Rest der Sammlung durchlaufen wird.
meiner Meinung nach führt dass sehr wahrscheinlich zu deiner Exception .
Du kannst die Ergebnisse vor dem Verlassen des lock-Blocks vollständig sammeln und anschließend außerhalb des lock-Blocks zurückgeben. Das vermeidet den yield return-Mechanismus innerhalb des lock-Kontexts.
Wenn du die Enumeration mit yield return trotzdem benutzen willst, wovon ich dir aber abrate, musst du schauen, dass der gesamte Enumerationsprozess innerhalb des lock-Blocks erfolgt. Dies schließt den lock-Block erst nach Abschluss der Iteration
Vom zweiten würde ich aber eher wie gesagt abraten wenn die Iteration lange dauert oder wenn andere Threads ebenfalls auf den lock-Block zugreifen wollen, sonst bekommst du wahrscheinlich andere Probleme.
Du kannst die Ergebnisse vor dem Verlassen des lock-Blocks vollständig sammeln und anschließend außerhalb des lock-Blocks zurückgeben. Das vermeidet den yield return-Mechanismus innerhalb des lock-Kontexts.
ja, klar. ich will ja nur verstehen, WARUM das passiert, nicht wie ich es behebe.
endet der lock-Block, sobald der erste Wert zurückgegeben wird.
Bist du dir sicher? lock ist ja äquivalent zu
Monitor.Enter(_lockObject);
try
{
foreach(var e in _myList)
{
if (e.x == par)
{
yield return e;
}
}
}
finally
{
Monitor.Exit(_lockObject);
}
und da wird erst in finally Block der Lock released.
Hier liest man das auch:
https://stackoverflow.com/questions/2847586/yield-returns-within-lock-statement
zum ersten würde ich aync rausnehmen, da du sonst nicht gut debuggen kannst
Wie meinst du das? Ich MUSS asynchrone Funktionen aufrufen, da komme ich nicht dran vorbei.
Wenn ich in der äußeren Schleife keine asynchronen Methoden aufrufe, dann geht es.
Diese Beobachtung war ja der Grund für meine Idee, dass der Thread nach dem Abarbeiten der asynchronen Methode in einem anderen Thread landet und dies die Exception auslöst.