Ich bin mir bewusst, Sammlung wurde geändert Ausnahmeproblem, aber ich kann es nicht in diesem Fall zu sehen. Ich weiß, wie ich es beheben kann, ich möchte nur verstehen, warum es hier vorkommt.Sammlung wurde unabhängig von der Sperre geändert
Also, ich habe eine Reihe von TaskCompletionSources und ich habe ein LockObject, das den Zugriff auf diesen Satz schützt. In einer Aufgabe (T1) möchte ich TCS erstellen und bis zu 3 Sekunden warten, bis die Aufgabe abgeschlossen ist.
In der anderen Aufgabe (T2) möchte ich eine halbe Sekunde warten und dann die Aufgabe, auf die T1 wartet, abschließen.
Der Satz von TCSs ist nicht gerade von Nutzen in diesem Codeausschnitt, aber im eigentlichen Programm, an dem ich gerade arbeite, ist es die Liste einer bestimmten Anzahl verschiedener Kellner zu halten, die alle einmal benachrichtigt werden sollen ist abgeschlossen und dies sollte auch die Liste der Kellner löschen. In diesem Snippet haben wir nur einen Kellner (T1), aber die Menge der TCSs muss verwendet werden, um das Problem zu reproduzieren.
Das Programm erzeugt die folgende Ausgabe:
T1 start.
Wait start.
Add start.
Add end.
T2 start.
CompleteAndClear start.
Completing 1 TCSs.
Remove start.
Remove end.
Wait end.
Wait succeeded.
T1 end.
Unhandled Exception: System.AggregateException: One or more errors occurred. (Collection was modified; enumeration operation may not execute.) ---> System.InvalidOperationException: Collection was modified; enumeration operation may not execute.
at System.Collections.Generic.HashSet`1.Enumerator.MoveNext()
at ConsoleApp1.Program.CompleteAndClear() in Program.cs:line 104
at ConsoleApp1.Program.<T2Async>d__5.MoveNext() in Program.cs:line 45
...
Was ich nicht verstehe:
- Warum die Ausnahme ausgelöst wird?
- Wie kann die Remove-Methode starten und enden, während CompleteAndClear noch die Sperre hält?
Für mich scheint es, wie TrySetResult das Warten verursacht mit dem gleichen Gewinde fertig gestellt werden, der die Sperre hält - so der aktuelle Thread springt auf die WaitAsync Funktion, geht zu entfernen, wird Sperre dann dadurch umgangen, dass Dieser Thread enthält die Sperre von CompleteAndClear (Sperren werden vom selben Thread wiedereingeführt), und seit das Entfernen den HashSet geändert hat, wird die Ausnahme aufgerufen. Aber die Absicht war, dass der Thread, der CompleteAndClear ausführt, nur die Aufgaben als abgeschlossen markiert, indem er ihre Ergebnisse setzt, dann löscht er die Menge und gibt die Sperre frei und erst dann kann Remove die Sperre eingeben und es sollte "TCS not found."
Die triviale fix im Code ist
Remove(tcs);
mit
if (!res) Remove(tcs);
, die perfekt funktioniert zu ersetzen, aber geht nicht mit der Absicht zusammen. Eine andere besteht darin, eine Kopie des Satzes zu erstellen, bevor sie gelöscht wird, und Ergebnisse auf die Kopie zu setzen, die den Fall vollständig löst.
Code:
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading;
namespace ConsoleApp1
{
class Program
{
static object lockObject = new object();
static HashSet<TaskCompletionSource<bool>> completionSources = new HashSet<TaskCompletionSource<bool>>();
static void Main(string[] args)
{
MainAsync().Wait();
}
static async Task MainAsync()
{
Task t1 = T1Async();
Task t2 = T2Async();
await t1;
await t2;
}
static async Task T1Async()
{
Console.WriteLine("T1 start.");
if (await WaitAsync()) Console.WriteLine("Wait succeeded.");
else Console.WriteLine("Wait failed.");
Console.WriteLine("T1 end.");
}
static async Task T2Async()
{
Console.WriteLine("T2 start.");
await Task.Delay(500);
CompleteAndClear();
Console.WriteLine("T2 end.");
}
static async Task<bool> WaitAsync()
{
Console.WriteLine("Wait start.");
bool res = false;
TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>();
using (CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(3000))
{
using (CancellationTokenRegistration cancellationTokenRegistration = cancellationTokenSource.Token.Register(() => { tcs.TrySetResult(false); }))
{
Add(tcs);
res = await tcs.Task;
Remove(tcs);
}
}
Console.WriteLine("Wait end.");
return res;
}
static void Add(TaskCompletionSource<bool> TaskCompletionSource)
{
Console.WriteLine("Add start.");
lock (lockObject)
{
completionSources.Add(TaskCompletionSource);
}
Console.WriteLine("Add end.");
}
static void Remove(TaskCompletionSource<bool> TaskCompletionSource)
{
Console.WriteLine("Remove start.");
lock (lockObject)
{
if (!completionSources.Remove(TaskCompletionSource)) Console.WriteLine("TCS not found.");
}
Console.WriteLine("Remove end.");
}
static void CompleteAndClear()
{
Console.WriteLine("CompleteAndClear start.");
lock (lockObject)
{
if (completionSources.Count > 0)
{
Console.WriteLine("Completing {0} TCSs.", completionSources.Count);
foreach (TaskCompletionSource<bool> tcs in completionSources)
tcs.TrySetResult(true);
Console.WriteLine("Clearing TCS list.");
completionSources.Clear();
}
}
Console.WriteLine("CompleteAndClear end.");
}
}
}
Danke für schöne Erklärung. Du hast mich auf "' TrySetResult' kann beliebigen Code aufrufen. " - Das wusste ich nicht. Die Doku ist zu kurz für meinen Geschmack. Wenn ich verstehe, was Sie hier sagen, ist das Standardverhalten von 'erwarten'' 'ExecuteSynchronously' und Sie können das überschreiben, wenn Sie' TaskCreationOptions' beim Erstellen von TCS verwenden. Ist das richtig? Wenn es jedoch richtig ist, erwähnen Sie in den Artikeln, die Sie verlinkt haben, dass diese Optionen manchmal nicht respektiert werden (aber diese scheinen Randfälle zu sein), also können wir uns hier darauf verlassen, dass wir 'Continuations Asynchronous' niemals ausführen im Synchronisierungsmodus laufen? – Wapac
Zwei weitere Fragen, wenn ich darf: Sie haben geschrieben, dass rekursive Sperren schlecht waren, also empfehlen Sie generell zu vermeiden, 'lock()' construct zu verwenden? Wenn ja, was empfehlen Sie stattdessen? Und die zweite ist - wenn ich etwas nicht verpasse, würde die Verwendung eines nicht-rekursiven Sperrmechanismus hier überhaupt nicht helfen, oder? – Wapac
Ich meine meine erste Frage im zweiten Kommentar hier ist wirklich - wenn ich meine 'lock()' s nicht-rekursiv (durch Design meines Codes), ist es immer noch besser, sie zu vermeiden und verschiedene Konstrukte verwenden? – Wapac