Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Quando avete un metodo sincrono in una libreria, potreste essere tentati di esporre una controparte asincrona che lo incapsula in Task.Run:
public T Foo() { /* synchronous work */ }
// Don't do this in a library:
public Task<T> FooAsync()
{
return Task.Run(() => Foo());
}
Questo articolo spiega perché questo approccio è quasi sempre errato per le librerie e come considerare i compromessi.
Scalabilità contro scaricamento
La programmazione asincrona offre due vantaggi distinti:
- Scalabilità : ridurre il consumo di risorse liberando thread durante le attese di I/O.
- Offload: spostare il lavoro in un thread diverso per mantenere la velocità di risposta (ad esempio, mantenere libero un thread dell'interfaccia utente) o ottenere il parallelismo.
Questi vantaggi richiedono approcci diversi. La distinzione critica: il wrapping di un metodo sincrono in Task.Run aiuta ad alleggerire il carico, ma non influisce sulla scalabilità.
Perché Task.Run non migliora la scalabilità
Un'implementazione veramente asincrona riduce il numero di thread utilizzati durante un'operazione a esecuzione prolungata. Un Task.Run wrapper blocca ancora un thread: sposta semplicemente il blocco da un thread a un altro:
public static class TimerExampleWrong
{
public static Task SleepAsync(int millisecondsTimeout)
{
return Task.Run(() => Thread.Sleep(millisecondsTimeout));
}
}
Public Module TimerExampleWrong
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Return Task.Run(Sub() Thread.Sleep(millisecondsTimeout))
End Function
End Module
Confrontare questo approccio con un'implementazione veramente asincrona che non utilizza thread durante l'attesa:
public static class TimerExampleRight
{
public static Task SleepAsync(int millisecondsTimeout)
{
var tcs = new TaskCompletionSource<bool>();
var timer = new Timer(
_ => tcs.TrySetResult(true), null, millisecondsTimeout, Timeout.Infinite);
tcs.Task.ContinueWith(
_ => timer.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
Public Module TimerExampleRight
Public Function SleepAsync(millisecondsTimeout As Integer) As Task
Dim tcs As New TaskCompletionSource(Of Boolean)()
Dim tmr As New Timer(
Sub(state) tcs.TrySetResult(True), Nothing, millisecondsTimeout, Timeout.Infinite)
tcs.Task.ContinueWith(
Sub(t) tmr.Dispose(), TaskScheduler.Default)
Return tcs.Task
End Function
End Module
Entrambe le implementazioni vengono completate dopo il ritardo specificato, ma la seconda implementazione non blocca alcun thread durante l'attesa. Per le applicazioni server che gestiscono molte richieste simultanee, tale differenza influisce direttamente sul numero di richieste che un server può elaborare contemporaneamente.
Il trasferimento è responsabilità del consumatore
L'incapsulamento delle chiamate sincrone in Task.Run è utile per scaricare il lavoro da un thread dell'interfaccia utente. Tuttavia, l'utente, non la libreria, deve gestire questo incapsulamento.
public static class UIOffloadExample
{
public static int ComputeIntensive(int input)
{
int result = 0;
for (int i = 0; i < input; i++)
{
result += i;
}
return result;
}
public static async Task ConsumeFromUIThreadAsync()
{
int result = await Task.Run(() => ComputeIntensive(10_000));
Console.WriteLine($"Result: {result}");
}
}
Public Module UIOffloadExample
Public Function ComputeIntensive(input As Integer) As Integer
Dim result As Integer = 0
For i As Integer = 0 To input - 1
result += i
Next
Return result
End Function
Public Async Function ConsumeFromUIThreadAsync() As Task
Dim result As Integer = Await Task.Run(Function() ComputeIntensive(10_000))
Console.WriteLine($"Result: {result}")
End Function
End Module
Il consumer conosce il contesto: se si trovano in un thread dell'interfaccia utente, la granularità necessaria e se l'offload aggiunge valore. La libreria non lo fa.
Perché le librerie non dovrebbero esporre i wrapper asincroni su sincroni
Quando una libreria espone solo il metodo sincrono (e non un wrapper asincrono), i consumer traggono vantaggio in diversi modi:
- Superficie di attacco API ridotta: meno metodi per apprendere, testare e gestire.
- Nessuna aspettativa di scalabilità fuorviante: gli utenti sanno che solo i metodi esposti come asincroni offrono effettivamente vantaggi di scalabilità.
-
Controllo del consumatore: i chiamanti scelgono se e come scaricare, al livello di granularità corretto. Un'applicazione server ad alta larghezza di banda può chiamare direttamente il metodo sincrono, evitando un sovraccarico non necessario da
Task.Run. - Prestazioni migliori: i wrapper asincroni aggiungono sovraccarico tramite allocazioni, commutatori di contesto e pianificazione del pool di thread. Per le operazioni con granularità fine, questo sovraccarico può essere significativo.
Eccezioni alla regola
Alcune classi di base espongono metodi asincroni in modo che le classi derivate possano eseguirne l'override con implementazioni veramente asincrone. La classe base fornisce una modalità asincrona su sincronizzazione come impostazione predefinita.
Ad esempio, Stream espone ReadAsync e WriteAsync. Le implementazioni di base eseguono il wrapping dei metodi sincroni Read e Write. Classi derivate come FileStream e NetworkStream eseguono l'override di questi metodi con implementazioni di I/O asincrone che offrono vantaggi reali di scalabilità.
Analogamente, TextReader fornisce ReadToEndAsync sulla classe base come wrapper e StreamReader esegue l'override con un'implementazione autenticamente asincrona che chiama ReadAsync internamente.
Queste eccezioni sono valide perché:
- Il modello è progettato per il polimorfismo. I chiamanti interagiscono con il tipo base.
- I tipi derivati forniscono sostituzioni veramente asincrone.
Linee guida
Esporre metodi asincroni da una libreria solo quando l'implementazione offre vantaggi reali di scalabilità rispetto alla controparte sincrona. Non implementare metodi asincroni esclusivamente per lo scaricamento. Lasciare tale scelta al consumer.