Wenn ich asynchronen Code mit async / await schreibe, normalerweise ConfigureAwait(false)
um den Kontext nicht zu erfassen, springt mein Code nacheinander von einem Thread-Pool-Thread zum nächsten await
. Dies wirft Bedenken hinsichtlich der Gewindesicherheit auf. Ist dieser Code sicher?
static async Task Main()
{
int count = 0;
for (int i = 0; i < 1_000_000; i++)
{
Interlocked.Increment(ref count);
await Task.Yield();
}
Console.WriteLine(count == 1_000_000 ? "OK" : "Error");
}
Die Variable i
ist ungeschützt und wird von mehreren Thread-Pool-Threads * aufgerufen. Obwohl das Zugriffsmuster nicht gleichzeitig ist, sollte es theoretisch möglich sein, dass jeder Thread einen lokal zwischengespeicherten Wert von erhöht i
, was zu mehr als 1.000.000 Iterationen führt. Ich kann dieses Szenario jedoch nicht in die Praxis umsetzen. Der obige Code wird auf meinem Computer immer in Ordnung gedruckt. Bedeutet dies, dass der Code threadsicher ist? Oder sollte ich den Zugriff auf die i
Variable mit a synchronisieren lock
?
(* Laut meinen Tests erfolgt durchschnittlich alle 2 Iterationen ein Thread-Wechsel.)
quelle
i
nach in jedem Thread zwischengespeichert? Sehen Sie sich dieses SharpLab IL an, um tiefer zu graben.Antworten:
Das Problem mit der Thread-Sicherheit besteht im Lesen / Schreiben von Speicher. Selbst wenn dies in einem anderen Thread fortgesetzt werden könnte, wird hier nichts gleichzeitig ausgeführt.
quelle
i
ist weder deklariertvolatile
noch durch eine Sperre geschützt. Nach meinem Verständnis dürfen der Compiler, der Jitter und die Hardware (CPU) eine solche Optimierung vornehmen.i
.private int <i>5__2
in der Zustandsmaschine ist nicht deklariertvolatile
. Meine Bedenken beziehen sich nicht darauf, dass ein Thread einen anderen Thread unterbricht, der gerade aktualisiert wirdi
. Dies ist in diesem Fall nicht möglich. Meine Bedenken beziehen sich auf einen Thread, der einen veralteten Wert von verwendeti
, der im lokalen Cache des Kerns der CPU zwischengespeichert ist und von einer vorherigen Schleife dort belassen wurde, anstatt einen neuen Wert voni
aus dem Haupt-RAM abzurufen. Der Zugriff auf den lokalen Cache ist billiger als der Zugriff auf den Hauptspeicher. Bei eingeschalteten Optimierungen sind solche Dinge möglich (gemäß dem, was ich gelesen habe).async
Code enthält?i
. Code trifftawait
, Thread A übergibt den gesamten Status an Thread B und geht zurück in den Pool. Gewinde B - Schritteni
. Trefferawait
. Thread B übergibt dann den gesamten Status an Thread C, geht zurück in den Pool usw. usw. Zu keinem Zeitpunkt ist ein gleichzeitiger Zugriff aufi
, es ist keine Thread-Sicherheit erforderlich, es spielt keine Rolle, dass ein Thread-Wechsel stattgefunden hat Der benötigte Status wird an den neuen Thread übergeben, in dem die Fortsetzung ausgeführt wird. Es gibt keinen gemeinsamen Status, weshalb Sie keine Synchronisierung benötigen.Ich glaube, dieser Artikel von Stephen Toub kann etwas Licht ins Dunkel bringen. Dies ist insbesondere eine relevante Passage darüber, was während eines Kontextwechsels passiert:
Bemerkenswert ist, dass der
YieldAwaitable
zurückgegebene vonTask.Yield()
immer zurückkehrtfalse
.quelle
ExecutionContext
von Thread zu Thread auch als Mechanismus zur Ungültigmachung der lokalen Caches des Threads dienen würde. Aber es ist auch nicht unmöglich.async
oderawait
. Eine Cache-Ungültigmachung während eines Kontextwechsels würde Single- und Multithread-Code auf dieselbe Weise beeinflussen.