Fast jeder Fehler in nebenläufigem Code hat dieselbe Ursache: gemeinsamer veränderlicher Zustand. Zwei Threads lesen und schreiben
dieselbe Variable, dasselbe Feld, dieselbe Sammlung, und ihre Schritte verschränken sich so, dass das Ergebnis nicht stimmt. Diese Lektion zeigt,
wie das aussieht, wie Sie es mit einem Test sichtbar machen und welche vier Werkzeuge .NET dagegen hat: lock, SemaphoreSlim, Interlocked
und Daten, die sich gar nicht ändern.
Warum count++ nicht zählt
count++ sieht aus wie ein Schritt, sind aber drei: lesen, eins addieren, zurückschreiben. Lesen zwei Threads denselben Wert, schreiben beide
denselben Wert plus eins zurück, und ein Zählschritt ist verloren (lost update). Acht Aufgaben zählen je eine Million:
// zaehler.cs: eight tasks count to a million each, without and with synchronization
#:property PublishAot=false
using System.Diagnostics;
using System.Globalization;
CultureInfo.CurrentCulture = CultureInfo.GetCultureInfo("de-DE");
const int Tasks = 8;
const int PerTask = 1_000_000;
int plain = 0;
Measure("count++ ohne alles", () => plain++, () => plain);
int interlocked = 0;
Measure("Interlocked.Increment", () => Interlocked.Increment(ref interlocked), () => interlocked);
int locked = 0;
var gate = new Lock();
Measure("lock (Lock)", () =>
{
lock (gate)
{
locked++;
}
}, () => locked);
void Measure(string name, Action increment, Func<int> result)
{
var watch = Stopwatch.StartNew();
Task.WaitAll(Enumerable.Range(0, Tasks).Select(_ => Task.Run(() =>
{
for (int i = 0; i < PerTask; i++)
{
increment();
}
})));
Console.WriteLine($"{name,-22} {result(),10:N0} von {Tasks * PerTask:N0}, {watch.Elapsed.TotalMilliseconds,5:N0} ms");
}
Auf einem Testrechner mit 32 logischen Kernen, zwei Läufe; Ihre Zahlen sind andere:
count++ ohne alles 1.605.433 von 8.000.000, 76 ms
Interlocked.Increment 8.000.000 von 8.000.000, 204 ms
lock (Lock) 8.000.000 von 8.000.000, 520 ms
count++ ohne alles 1.660.527 von 8.000.000, 80 ms
Interlocked.Increment 8.000.000 von 8.000.000, 204 ms
lock (Lock) 8.000.000 von 8.000.000, 535 ms
Ohne Synchronisation gehen vier von fünf Zählschritten verloren, und jeder Lauf verliert andere. Das ist das Tückische an diesen Fehlern: Sie sind nicht falsch, sie sind manchmal falsch, mal viel, mal wenig, mal gar nicht. Ein Test mit einem einzigen Aufruf findet sie nie.