C# Parallel For bringt falsche Ergebnisse - was mache ich falsch?

Kokujou

Lieutenant
Registriert
Dez. 2017
Beiträge
948
Hallo erstmal. Zuerst mein grundsätzlicher Aufbau:

C#:
var test = Parallel.For(1, ExecutionTimes + 1, () => new IterationOutput(), (round, state, output) =>
{
    return PlayNewGame(output, P1Oya: true);
}, finalOutput =>
{
    Interlocked.Add(ref totalOutput.P1Wins, finalOutput.P1Wins);
    Interlocked.Add(ref totalOutput.P2Wins, finalOutput.P2Wins);
    Interlocked.Add(ref totalOutput.Draws, finalOutput.Draws);
});

Der Grundaufbau ist sehr simpel. Eigentlich will ich nur mehrere virtuelle Spiele parallel laufen lassen und den Ausgang der Spiele am Ende ausgeben. Allerdings haut das nicht so ganz hin. Es gibt folgende Fehler:

1. Ich schreibe Logs an einen externen Prozess. Allerdings verwendet der komische Zählervariablen. Statt in irgendeiner zufälligen Reihenfolge die Zahlen 1-x auszugeben hab ich mal 3 einsen, mal nur 6en. wenn ich einen lock einbaue ist es noch schlimmer und er gibt immer dieselbe Zahl aus. Gut ich nehme an es liegt daran dass ich in den Outputbuffer parallelisiert schreibe, aber trotzdem.

2. Der Output ist falsch!, wie ihr seht benutze ich die Variable output. In jeder der Iterationen wird der Counter erhöht je nachdem wer das Spiel gewonnen hat. Am Ende füge ich Interlocked die Zahlen zusammen. Jetzt erkläre man mir warum ich da immer nur 1 stehen habe. bei 40 Iterationen müsste ich doch auf die Summe 40 kommen statt auf 1.
 
Sorry, aber für sowas gibt es einen Debugger. Soll man hier raten was das alles für Variablen sind und welcher Code da noch beteiligt ist, oder wie?
 
  • Gefällt mir
Reaktionen: marcOcram, Physikbuddha, herliTz_234 und eine weitere Person
Och kommt schon Leute seit wann kann man denn bitte mit nem Debugger parallelen Code debuggen. Ich habs versucht es ist nicht machbar. Was glaubt ihr wozu ich dieses Logging brauche.

ich gebe euch genau den Code den ihr braucht oder wäre es euch lieber ich würde euch die 200 Zeilen um die Ohren hauen die es tatsächlich gibt. Das will doch kein schwein lesen.

Der Punkt ist, dass die Zählervariable in der Parallel.For offenbar nicht zählt. Was ich mache sagte ich ebenfalls bereits: Ich erzeuge einen prozess, beschreibe seine Ausgabe und das ist dann quasi mein Logg.

Da ich nur schreibe und niemals entferne dürften sich die Fehler durch die parallelität eigentlich bestenfalls in ner Art komischen Formatierung äußern. Dass z.B. irgendwelche Schriften verschmelzen.

und auch was mit dem IterationOutput passiert habe ich euch gesagt. In meinem Code setze ich irgendwo am Ended es Spiels, was ja am Ende synchron abläuft, dem SPielergebniss entsprechen P1Wins, P2Wins, Draws. Im prinzip inkrementiere ich hier nur. Eigentlich sollte auch das mit einer lokalen Variable gehen denn inkrementieren sollte ja wohl eigentlich immer möglich sein egal von wo ich darauf zugreife.

Ich hab schon oft mit parallelität gearbeitet und immer die dümmsten und nicht nachvollziehbarsten Fehler bekommen obwohl ich eigentlich immer das tue, was selbstverständlich ist: Alles so konzipieren dass keine globalen Variablen benötigt werden.

Meine KI z.B. kriegt im Parallelbetrieb irgendwelche nicht nachvollziehbaren Fehler und ich könnte nichtmal anfangen zu raten warum. Ich meine die Kartensammlungen werden allesamt im Thread selbst erzeugt, kein Zugriff von außen. Die Spieler werden ebenfalls so erzeugt. Es gibt absolut nichts was irgendwie zu Konflikten führen könnte und trotzdem sind irgendwelche Sammlungen plötzlich leer.

Ich hab mir sogut wie jedes Tutorial durchgelesen was ich zum Thema parallelität finden konnte und hoffte ihr könnt mir vielleicht mal einen Tipp geben woran es dauernd scheitert. Denn ich weiß langsam nicht mehr weiter. es wäre ja fast einfacher das ganze in C von grundauf selbst zu machen.
 
Also ich konnte damals schon in Visual Studio 2010 den wirklich mächtigen Multithread-Debugger nutzen, auch wenn ich ihn heute nicht mehr allzu oft brauche.

Du hast drei Lambda-Ausdrücke. Hast du da schonmal Haltepunkte gesetzt? Hast du dir mit Debug.Log() die Zwischenergebnisse in die Konsole geprintet?

Oder hast du überhaupt mal probiert, ob die parallele Ausführung überhaupt funktioniert? Vielleicht ist deine Spieleengine einfach nicht threadsafe. Als Anregung etwas Pseudocode, und auch wirklich nur Pseudocode, weil ich noch faul in meinem Bett mit dem Tablet liege und meinen Urlaub genieße. :D

C#:
var gameTasks = Enumerable.Range(1, ExecutionTimes).Select(g => Task.Run(() => PlayNewGame()));
GameResult totalResult = (await Task.WhenAll(gameTasks)).Aggregate((resultA, resultB) => resultA + resultB);

public static GameResult operator +(GameResult a, GameResult b)
{
    return new GameResult()
    {
        P1Wins = a.P1Wins + b.P1Wins,
        P2Wins = a.P2Wins + b.P2Wins,
        Draws = a.Draws + b.Draws
    }
}
 
Um @Enurian zu unterstützen.

Dein Code-Fragment ist so richtig. Minimiert funktioniert es. Ohne zu raten und den restlichen Code kann man da nicht helfen.

C#:
class Output {
    public int P1Wins { get; set; }
    public int P2Wins { get; set; }
    public int Draws { get; set; }
}

Random random = new Random(Guid.NewGuid().GetHashCode());

Output totalOutput = new Output();
object synchronizeOutput = new object();
object synchronizeRandom = new object();

Parallel.For<Output>(0, 1000, () => new Output(), (round, state, output) =>
{
    double randomValue;
    lock (synchronizeRandom) {
        randomValue = random.NextDouble() * 3.0;
    } 
    if (randomValue < 1) {
        output.P1Wins++;
    } else if (randomValue < 2) {
        output.P2Wins++;
    } else {
        output.Draws++;
    }
    return output;
}, finalOutput =>
{
    lock (synchronizeOutput)
    {
        totalOutput.P1Wins += finalOutput.P1Wins;
        totalOutput.P2Wins += finalOutput.P2Wins;
        totalOutput.Draws += finalOutput.Draws;
    }
});

Console.WriteLine($"Total P1Wins: {totalOutput.P1Wins}");
Console.WriteLine($"Total P2Wins: {totalOutput.P2Wins}");
Console.WriteLine($"Total Draws: {totalOutput.Draws}");

Ergebnis:
Code:
Total P1Wins: 321
Total P2Wins: 332
Total Draws: 347

Behauptung: Debugge mal deine Klasse PlayNewGame und schau ob die bis zum Ende der Schleife den Wert von output schon erhöht hat.
EDIT: Wird das Spiel mit dem Konstruktor denn auch synchron gespielt (gestartet)? Hast du es erst einmal mit einer klassischen synchronen For-Schleife probiert?
 
Zuletzt bearbeitet: (Random ist nicht thread-safe)
  • Gefällt mir
Reaktionen: Enurian
Du beschwerst dich über den Output von irgendeinem Logger, zeigst ihn aber nicht. Kühne Vermutung: da ist er wohl nicht threadsafe oder wird nicht so (oft) aufgerufen wie du dir vorstellst.

Du beschwerst dich über die Summe der Ergebnisse deiner PlayNewGame Methode, zeigst aber nicht wie die zustande kommen. Da ist wohl was nicht threadsafe oder wird nicht so aufgerufen, wie du dir vorstellst.

Ich kann dir auf jeden Fall versprechen, dass es an deinem Code liegt und kein Bug in .Net ist.
Ja, der Debugger funktioniert auch einwandfrei beim multithreaden und ist hier die beste Möglichkeit den Fehler zu finden. Am besten führst du das aber erstmal ohne Parallelisierung aus, um zu sehen dass das überhaupt das Problem ist.

Kokujou schrieb:
Denn ich weiß langsam nicht mehr weiter. es wäre ja fast einfacher das ganze in C von grundauf selbst zu machen.
Klingt als wäre das tatsächlich eine gute Lektion für dich, um zu verstehen was da alles passiert und wie viel dir .Net abnimmt.
Spoiler: nein, es ist nicht einfacher.
 
  • Gefällt mir
Reaktionen: new Account()
finalOutput.P1Wins

Würde spontan vermuten, dass P1Wins nicht als "volatile" gekennzeichnet wurde,
du kannst auch Volatile.Read(ref finalOutput.P1Wins) versuchen.
Ein Debugger hilft hier tatsächlich wenig, da hier oftmals Heisenbugs bei paralleler Programmierung vorkommen.
Im Debug Modus sind die Optimierungen beispielsweise deaktiviert, sodass man
volatile nicht braucht, dass Programm im Release Modus dann aber nicht funktioniert.
 
@umask007 Wie kommst du darauf, dass er nicht im Debug Modus unterwegs ist? Davon würde ich aktuell mal ausgehen.
Volatile kann ich mir hier nicht als Lösung vorstellen, zudem zitiere ich dazu mal Eric Lippert:
Frankly, I discourage you from ever making a volatile field. Volatile fields are a sign that you are doing something downright crazy: you're attempting to read and write the same value on two different threads without putting a lock in place.
Er hat ja die Locks für das Aufsummieren.
 
Also wenn auf finalOutput.P1Wins von verschiedenen Threads zugegriffen wird, muss auch dieses Feld geschützt werden. Der Wert wird Interlocked.Add als Kopie mitgegeben, es gibt also keinen Schutz.
War aber nur eine Vermutung, die ich nicht getestet habe.
Der TE war sich sicher, dass die richtigen Werte addiert werden, also muss es irgendeinen Grund geben.
 
finalOutput ist das Ergebnis der parallelen Stränge und kann somit ohne Schutz gelesen werden (der Wert ändert sich nicht mehr). Da dürfte auch kein volatile helfen, da im "finally" nicht mehr geschrieben wird. totalOutput hingegen muss geschützt werden, da das "finally" von den parallelen Strängen auch aufgerufen wird (auch konkurrent zueinander, man weiß ja nicht wann welcher Strang fertig ist). Dementsprechend ist das mit dem Interlocked ausreichend.

Ich habe bei mir nur ein lock genommen, da Referenzen nur mit Feldern und nicht mit Properties funktionieren.
 
Zuletzt bearbeitet:
ich versteh einfach nicht warum so viel "geschützt" undso werden muss. Es ist ja nicht so als würde ich irgendwelche gemeinsamen Variablen verwenden.

Mein Ansatz für paralleles Arbeiten ist eine Funktion zu konstruieren die keinerlei externe Variablen verwendet und wenn doch dann diese höchstens Lesend verwendet. Beim Lesen kanns ja wohl nicht zu Wertkonflikten kommen.

Dann hab ich eine Simple statische abgeschlossene Funktion a la Eingabe -> Verarbeitung -> Ausgabe und dann kombiniere ich die Ausgabe im Main Thread. Klingt doch richtig oder? ...

Ich werd mir mal die geposteten codeschnippsel schnappen und sie genauso minimal austesten. wenn das bei mir funktioniert baue ich es graduell mit meinem Code wieder auf und sehe so hoffentlich welche Stelle meines Codes nicht funktioniert...

Ganz nebenbei: Ich hba es leider noch nicht geschafft irgendeine Art von parallelem Debugging zum Laufen zu bekommen, darum der ungekonnte Versuch des Loggings. Ich denke mir solange ich nur hinzufüge und nicht lösche kann da doch eigentlich nicht viel schiefgehen hust von wegen hust.

Vermutlich kommt es jetzt aber zu Konflikten WÄHREND des schreibens.
also aus -> abc -> def = abcdef wird adefbc oder so
 
Zurück
Oben