Megjegyzés
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhat bejelentkezni vagy módosítani a címtárat.
Az oldalhoz való hozzáféréshez engedély szükséges. Megpróbálhatja módosítani a címtárat.
Az olyan felhasználói felületi keretrendszerek, mint a Windows Forms, a WPF és a .NET MAUI egy SynchronizationContext telepíthetnek a felhasználói felületi szálukra. Amikor ezekben a környezetekben egy feladatot hajt végre, a folytatás automatikusan visszatér a felhasználói felület szálához. A konzolalkalmazások nem telepítenek SynchronizationContext, ami azt jelenti, hogy await a folytatások a szálkészleten futnak. Ez a cikk ismerteti a következményeket, és bemutatja, hogyan hozhat létre egy egyszálas üzenetszivattyút, amikor szüksége van rá.
Alapértelmezett viselkedés egy konzolalkalmazásban
Konzolalkalmazásban a SynchronizationContext.Currentnull-t ad vissza. Amikor egy metódus vezérlést ad át egy await pontnál, a folytatás bármely rendelkezésre álló szálkészlet szálon fut.
static void DefaultBehaviorDemo()
{
DemoAsync().GetAwaiter().GetResult();
}
static async Task DemoAsync()
{
var d = new Dictionary<int, int>();
for (int i = 0; i < 10_000; i++)
{
int id = Thread.CurrentThread.ManagedThreadId;
d[id] = d.TryGetValue(id, out int count) ? count + 1 : 1;
await Task.Yield();
}
foreach (var pair in d)
Console.WriteLine(pair);
}
A program futtatásának reprezentatív kimenete:
[1, 1]
[3, 2687]
[4, 2399]
[5, 2397]
[6, 2516]
Az 1. szál (a főszál) csak egyszer jelenik meg az első szinkron iteráció során, mielőtt await Task.Yield() felfüggesztené a metódust. Minden további iteráció szálkészlet-szálakon fut.
Modern aszinkron belépési pontok
A C# 7.1-től kezdődően a Main deklarálható mint async Task vagy async Task<int>. A C# 9-es és újabb verzióiban a legfelső szintű utasításokat await közvetlenül használhatja:
// Top-level statements (C# 9+)
await DemoAsync();
// async Task Main (C# 7.1+)
static async Task Main()
{
await DemoAsync();
}
Ezek a belépési pontok nem telepítenek SynchronizationContext. A futtatókörnyezet létrehoz egy bootstrap, amely meghívja az aszinkron metódust, és blokkolja a visszaadott Task műveletet, hasonlóan a .GetAwaiter().GetResult() hívásához. A folytatások továbbra is futnak a szálkészleten.
Ha szál affinitásra van szüksége
Számos konzolalkalmazás esetében a folytatások futtatása a szálkészleten rendben van. Egyes forgatókönyvek azonban megkövetelik, hogy az összes folytatás egyetlen szálon fusson:
- Szerializált végrehajtás: Több egyidejű aszinkron művelet is zárolás nélkül osztja meg az állapotot a folytatások ugyanazon a szálon való futtatásával.
- Könyvtárkövetelmények: Egyes kódtárak vagy COM-objektumok affinitást igényelnek egy adott szálhoz.
- Egységtesztelés: A tesztelési keretrendszerekhez az aszinkron kód determinisztikus, egyszálas végrehajtására lehet szükség.
Egyszálas SynchronizationContext létrehozása
Az összes folytatás egyetlen szálon való futtatásához két dologra van szükség:
- Az SynchronizationContext a metódussor, amelynek Post a metódussorai szálbiztos gyűjteményben működnek.
- Üzenetszivattyú hurok, amely feldolgozza a célszálon lévő üzenetsort.
Az egyéni környezet
A környezet a BlockingCollection<T> gyártók (az aszinkron folytatások) és a fogyasztó (a szivattyúzási ciklus) koordinálására szolgál:
sealed class SingleThreadSynchronizationContext : SynchronizationContext
{
private readonly
BlockingCollection<KeyValuePair<SendOrPostCallback, object?>> _queue = new();
public override void Post(SendOrPostCallback d, object? state)
{
_queue.Add(new KeyValuePair<SendOrPostCallback, object?>(d, state));
}
public void RunOnCurrentThread()
{
while (_queue.TryTake(out KeyValuePair<SendOrPostCallback, object?> workItem,
Timeout.Infinite))
{
workItem.Key(workItem.Value);
}
}
public void Complete() => _queue.CompleteAdding();
}
Class SingleThreadSynchronizationContext
Inherits SynchronizationContext
Private ReadOnly _queue As New _
BlockingCollection(Of KeyValuePair(Of SendOrPostCallback, Object))()
Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
_queue.Add(New KeyValuePair(Of SendOrPostCallback, Object)(d, state))
End Sub
Public Sub RunOnCurrentThread()
Dim workItem As New KeyValuePair(Of SendOrPostCallback, Object)(Nothing, Nothing)
While _queue.TryTake(workItem, Timeout.Infinite)
workItem.Key.Invoke(workItem.Value)
End While
End Sub
Public Sub Complete()
_queue.CompleteAdding()
End Sub
End Class
Az AsyncPump.Run metódus
AsyncPump.Run telepíti az egyéni környezetet, meghívja az aszinkron metódust, és a metódus befejezéséig a hívó szálon folytatja a műveleteket:
static class AsyncPump
{
public static void Run(Func<Task> func)
{
SynchronizationContext? prevCtx = SynchronizationContext.Current;
try
{
var syncCtx = new SingleThreadSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
Task t;
try
{
t = func();
}
catch
{
syncCtx.Complete();
throw;
}
t.ContinueWith(
_ => syncCtx.Complete(), TaskScheduler.Default);
syncCtx.RunOnCurrentThread();
t.GetAwaiter().GetResult();
}
finally
{
SynchronizationContext.SetSynchronizationContext(prevCtx);
}
}
Class AsyncPump
Public Shared Sub Run(func As Func(Of Task))
Dim prevCtx As SynchronizationContext = SynchronizationContext.Current
Try
Dim syncCtx As New SingleThreadSynchronizationContext()
SynchronizationContext.SetSynchronizationContext(syncCtx)
Dim t As Task
Try
t = func()
Catch
syncCtx.Complete()
Throw
End Try
t.ContinueWith(
Sub(unused) syncCtx.Complete(), TaskScheduler.Default)
syncCtx.RunOnCurrentThread()
t.GetAwaiter().GetResult()
Finally
SynchronizationContext.SetSynchronizationContext(prevCtx)
End Try
End Sub
Nézze meg működés közben
Cserélje le az alapértelmezett hívást a következőre AsyncPump.Run:
static void AsyncPumpDemo()
{
AsyncPump.Run(async () =>
{
var d = new Dictionary<int, int>();
for (int i = 0; i < 10_000; i++)
{
int id = Thread.CurrentThread.ManagedThreadId;
d[id] = d.TryGetValue(id, out int count) ? count + 1 : 1;
await Task.Yield();
}
foreach (var pair in d)
Console.WriteLine(pair);
});
}
Sub AsyncPumpDemo()
AsyncPump.Run(
Async Function() As Task
Dim d As New Dictionary(Of Integer, Integer)()
For i As Integer = 0 To 9999
Dim id As Integer = Thread.CurrentThread.ManagedThreadId
Dim count As Integer
If d.TryGetValue(id, count) Then
d(id) = count + 1
Else
d(id) = 1
End If
Await Task.Yield()
Next
For Each pair In d
Console.WriteLine(pair)
Next
End Function)
End Sub
kimenet:
[1, 10000]
Az adott szálazonosító a futtatókörnyezettől és a platformtól függően eltérhet, de a fő eredmény az, hogy mind a 10 000 iteráció egyetlen szálon fut: a fő szálon.
Aszinkron void metódusok kezelése
A Func<Task> túlterhelés a visszaadott Taskfolyamaton keresztül követi a befejezést. Az aszinkron void metódusok nem adnak vissza feladatot, hanem az aktuális SynchronizationContext-t a OperationStarted() és OperationCompleted() értesíti. Az aszinkron void metódusok támogatásához terjessze ki a környezetet a kiemelkedő műveletek nyomon követésére:
public static void Run(Action asyncMethod)
{
SynchronizationContext? prevCtx = SynchronizationContext.Current;
try
{
var syncCtx = new AsyncVoidSynchronizationContext();
SynchronizationContext.SetSynchronizationContext(syncCtx);
Exception? caughtException = null;
syncCtx.OperationStarted();
try
{
asyncMethod();
}
catch (Exception ex)
{
caughtException = ex;
syncCtx.Complete();
}
finally
{
syncCtx.OperationCompleted();
}
syncCtx.RunOnCurrentThread();
if (caughtException is not null)
{
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(caughtException).Throw();
}
}
finally
{
SynchronizationContext.SetSynchronizationContext(prevCtx);
}
}
}
sealed class AsyncVoidSynchronizationContext : SynchronizationContext
{
private readonly
BlockingCollection<KeyValuePair<SendOrPostCallback, object?>> _queue = new();
private int _operationCount;
public override void Post(SendOrPostCallback d, object? state)
{
_queue.Add(new KeyValuePair<SendOrPostCallback, object?>(d, state));
}
public override void OperationStarted() =>
Interlocked.Increment(ref _operationCount);
public override void OperationCompleted()
{
if (Interlocked.Decrement(ref _operationCount) == 0)
Complete();
}
public void RunOnCurrentThread()
{
while (_queue.TryTake(out KeyValuePair<SendOrPostCallback, object?> workItem,
Timeout.Infinite))
{
workItem.Key(workItem.Value);
}
}
public void Complete() => _queue.CompleteAdding();
}
Public Shared Sub Run(asyncMethod As Action)
Dim prevCtx As SynchronizationContext = SynchronizationContext.Current
Try
Dim syncCtx As New AsyncVoidSynchronizationContext()
SynchronizationContext.SetSynchronizationContext(syncCtx)
syncCtx.OperationStarted()
Try
asyncMethod()
Catch
syncCtx.Complete()
Throw
Finally
syncCtx.OperationCompleted()
End Try
syncCtx.RunOnCurrentThread()
Finally
SynchronizationContext.SetSynchronizationContext(prevCtx)
End Try
End Sub
End Class
Class AsyncVoidSynchronizationContext
Inherits SynchronizationContext
Private ReadOnly _queue As New _
BlockingCollection(Of KeyValuePair(Of SendOrPostCallback, Object))()
Private _operationCount As Integer
Public Overrides Sub Post(d As SendOrPostCallback, state As Object)
_queue.Add(New KeyValuePair(Of SendOrPostCallback, Object)(d, state))
End Sub
Public Overrides Sub OperationStarted()
Interlocked.Increment(_operationCount)
End Sub
Public Overrides Sub OperationCompleted()
If Interlocked.Decrement(_operationCount) = 0 Then
Complete()
End If
End Sub
Public Sub RunOnCurrentThread()
Dim workItem As New KeyValuePair(Of SendOrPostCallback, Object)(Nothing, Nothing)
While _queue.TryTake(workItem, Timeout.Infinite)
workItem.Key.Invoke(workItem.Value)
End While
End Sub
Public Sub Complete()
_queue.CompleteAdding()
End Sub
End Class
Ha engedélyezve van a műveletkövetés, a szivattyú csak akkor lép ki, ha az összes még függőben lévő aszinkron void metódus befejeződött, nem csak a legfelső szintű feladat.
Gyakorlati szempontok
-
Holtpont kockázata: Ha a blokkokban
AsyncPump.Runfutó kód szinkron módon fut (például hívással.Resultvagy.Wait()olyan feladaton, amelynek a folytatást vissza kell helyeznie a szivattyúba), a szivattyúszál nem tudja feldolgozni ezt a folytatást. Az eredmény holtpont. Ugyanez a probléma szerepel a szinkron burkolók az aszinkron metódusok számára című részben. - Teljesítmény: Az egyszálas szivattyú egy szálra korlátozza az átviteli sebességet. Ezt a megközelítést csak akkor használja, ha a szál affinitása számít.
-
Platformfüggetlen: Az
AsyncPumpitt látható implementáció csak aSystem.Collections.Concurrentés aSystem.Threadingnévterek típusait használja. Minden olyan platformon működik, amelyet .NET támogat.