c# - method - ¿Debo llamar a ConfigureAwait(falso) en cada operación esperada?
call async method c# await (1)
Si entiendo el documento correctamente, debería agregar
ConfigureAwait(false)
a cadaawait
que no esté en un método que tenga código que deba ejecutarse en el hilo de la interfaz de usuario
Sí. El comportamiento predeterminado en las aplicaciones de interfaz de usuario es para el código después de await
para continuar en el hilo de la interfaz de usuario. Cuando el subproceso de la interfaz de usuario está ocupado, pero su código no necesita acceder a la interfaz de usuario, no tiene sentido esperar que el subproceso de la interfaz de usuario esté disponible.
(Nota: esto intencionalmente omite algunos detalles no relevantes aquí).
Pero, ¿qué pasa con cualquier código de terceros al que llame que también realice operaciones
StreamReader
enStreamReader
?
Siempre y cuando evites los bloqueos por otros medios, esto solo afectará el rendimiento, no la corrección. Y el problema de un código de terceros potencialmente deficientemente pobre no es un problema nuevo.
En otras palabras: siga ambas mejores prácticas.
También estoy un poco desilusionado por tener que colocar
ConfigureAwait(false)
todas partes, pensé que el objetivo de la palabra claveawait
era eliminar las llamadas explícitas de la biblioteca de tareas, ¿no debería haber una palabra clave diferente para la espera de un curriculum vitae sin contexto? ¿entonces? (por ejemplo,awaitfree
).
ConfigureAwait
no es un método TPL.
await
está generalizado, por lo que se puede utilizar en tipos arbitrarios siempre que admitan los métodos necesarios. Para un ejemplo al azar, puede agregar un método de extensión para que una Task
devuelva un tipo que permita que el código después de await
continúe en un nuevo hilo dedicado. Esto no requeriría una nueva versión del compilador con una palabra clave nueva.
Pero sí, es un nombre largo.
Si necesito aplicarlo en todas partes, ¿podría simplificarlo a un método de extensión?
Sí, eso está perfectamente bien.
Aquí hay un código asíncrono que escribí y que exporta la configuración de mi aplicación a un archivo de texto simple. No puedo evitar pensar que no me parece correcto. ¿Es esta la forma correcta de hacerlo?
Como escribí en los comentarios, yo no usaría ese enfoque en absoluto ... pero si quieres, tienes mucha duplicación de código ahí de la que puedes deshacerte. Y con eso desaparecido, ya no se ve tan mal.
/* SettingsCollection omitted, but trivially implementable using
Dictionary<string, string>, NameValueCollection,
List<KeyValuePair<string, string>>, whatever. */
SettingsCollection GetAllSettings()
{
return new SettingsCollection
{
{ nameof(this.DefaultStatus ), this.DefaultStatus },
{ nameof(this.ConnectionString ), this.ConnectionString },
{ nameof(this.TargetSystem ), this.TargetSystem.ToString("G") },
{ nameof(this.ThemeBase ), this.ThemeBase },
{ nameof(this.ThemeAccent ), this.ThemeAccent },
{ nameof(this.ShowSettingsButton), this.ShowSettingsButton ? "true" : "false" },
{ nameof(this.ShowActionsColumn ), this.ShowActionsColumn ? "true" : "false" },
{ nameof(this.LastNameFirst ), this.LastNameFirst ? "true" : "false" },
{ nameof(this.TitleCaseCustomers), this.TitleCaseCustomers ? "true" : "false" },
{ nameof(this.TitleCaseVehicles ), this.TitleCaseVehicles ? "true" : "false" },
{ nameof(this.CheckForUpdates ), this.CheckForUpdates ? "true" : "false" }
};
}
public async Task Export(String fileName)
{
using( StreamWriter wtr = new StreamWriter( fileName, append: false ) )
foreach (var setting in GetAllSettings())
await ExportSetting( wtr, setting.Key, setting.Value ).ConfigureAwait(false);
}
Leí este artículo https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html ; sin embargo, veo una contradicción:
Soy consciente del problema de bloquear el subproceso UI porque los bloques de subprocesos de interfaz de usuario esperan que se complete una operación asíncrona, pero la misma operación asincrónica se sincroniza con el contexto de subproceso de interfaz de usuario, por lo que la operación asincrónica no puede ingresar al subproceso de interfaz de usuario. El hilo de UI no parará de esperar.
El artículo nos dice que la solución consiste en no bloquear el hilo de la interfaz de usuario, de lo contrario, debe usar ConfigureAwait(false)
todas partes :
Tendría que usar para cada espera en el cierre transitivo de todos los métodos llamados por el código de bloqueo, incluidos todos los códigos de terceros y de terceros.
Sin embargo, más adelante en el artículo, el autor escribe:
Previniendo el punto muerto
Hay dos mejores prácticas (ambas cubiertas en mi publicación introductoria) que evitan esta situación:
- En sus métodos asincrónicos de "biblioteca", use
ConfigureAwait(false)
siempre que sea posible.- No bloquear en tareas; usa
async
todo el camino hacia abajo.
Estoy viendo una contradicción aquí: en la sección "no hagas esto", escribe que tener que usar ConfigureAwait(false)
todas partes sería la consecuencia de bloquear el hilo de UI, pero en su lista de "mejores prácticas" dice nosotros para hacer justamente eso: "use ConfigureAwait(false)
siempre que sea posible". - aunque supongo que "siempre que sea posible" excluiría el código de un tercero, pero en el caso de que no haya un código de terceros, el resultado será el mismo si bloqueo el hilo de UI o no.
En cuanto a mi problema específico, aquí está mi código actual en un proyecto MVVM de WPF:
MainWindowViewModel.cs
private async void ButtonClickEventHandler()
{
WebServiceResponse response = await this.client.PushDinglebopThroughGrumbo();
this.DisplayResponseInUI( response );
}
WebServiceClient.cs
public class PlumbusWebServiceClient {
private static readonly HttpClient _client = new HttpClient();
public async Task<WebServiceResponse> PushDinglebopThroughGrumbo()
{
try
{
using( HttpResponseMessage response = await _client.GetAsync( ... ) )
{
if( !response.IsSuccessStatusCode ) return WebServiceResponse.FromStatusCode( response.StatusCode );
using( Stream versionsFileStream = await response.Content.ReadAsStreamAsync() )
using( StreamReader rdr = new StreamReader( versionsFileStream ) )
{
return await WebServiceResponse.FromResponse( rdr );
}
}
}
catch( HttpResponseException ex )
{
return WebServiceResponse.FromException( ex );
}
}
}
Si entiendo el documento correctamente, debería agregar ConfigureAwait(false)
a cada await
que no esté en un método que tenga código que deba ejecutarse en el subproceso UI, que es todo método dentro de mi método PushDinglebopThroughGrumbo
, sino también todo el código en WebServiceResponse.FromResponse
(qué llamadas await StreamReader.ReadLineAsync
). Pero, ¿qué pasa con cualquier código de terceros al que llame que también realice operaciones StreamReader
en StreamReader
? No tendré acceso a su código fuente, por lo que sería imposible.
También estoy un poco desilusionado por tener que colocar ConfigureAwait(false)
todas partes, pensé que el objetivo de la palabra clave await
era eliminar las llamadas explícitas de la biblioteca de tareas, ¿no debería haber una palabra clave diferente para la espera de un curriculum vitae sin contexto? ¿entonces? (por ejemplo, awaitfree
).
Entonces, ¿mi código debería verse así?
MainWindowViewModel.cs
(unmodified, same as above)
WebServiceClient.cs
public class PlumbusWebServiceClient {
private static readonly HttpClient _client = new HttpClient();
public async Task<WebServiceResponse> PushDinglebopThroughGrumbo()
{
try
{
using( HttpResponseMessage response = await _client.GetAsync( ... ).ConfigureAwait(false) ) // <-- here
{
if( !response.IsSuccessStatusCode ) return WebServiceResponse.FromStatusCode( response.StatusCode );
using( Stream versionsFileStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false) ) // <-- and here
using( StreamReader rdr = new StreamReader( versionsFileStream ) )
{
return await WebServiceResponse.FromResponse( rdr ).ConfigureAwait(false); // <-- and here again, and inside `FromResponse` too
}
}
}
catch( HttpResponseException ex )
{
return WebServiceResponse.FromException( ex );
}
}
}
... Hubiera pensado que llamar a ConfigureAwait(false)
solo sería necesario en la llamada más alta dentro del método PlumbusWebServiceClient
, es decir, la llamada GetAsync
.
Si necesito aplicarlo en todas partes, ¿podría simplificarlo a un método de extensión?
public static ConfiguredTaskAwaitable<T> CF<T>(this Task<T> task) {
return task.ConfigureAwait(false);
}
using( HttpResponseMessage response = await _client.GetAsync( ... ).CF() )
{
...
}
... aunque esto no alivia todo el alboroto.
Actualización: segundo ejemplo
Aquí hay un código asíncrono que escribí y que exporta la configuración de mi aplicación a un archivo de texto simple. No puedo evitar pensar que no me parece correcto. ¿Es esta la forma correcta de hacerlo?
class Settings
{
public async Task Export(String fileName)
{
using( StreamWriter wtr = new StreamWriter( fileName, append: false ) )
{
await ExportSetting( wtr, nameof(this.DefaultStatus ), this.DefaultStatus ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.ConnectionString ), this.ConnectionString ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.TargetSystem ), this.TargetSystem.ToString("G") ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.ThemeBase ), this.ThemeBase ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.ThemeAccent ), this.ThemeAccent ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.ShowSettingsButton), this.ShowSettingsButton ? "true" : "false" ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.ShowActionsColumn ), this.ShowActionsColumn ? "true" : "false" ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.LastNameFirst ), this.LastNameFirst ? "true" : "false" ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.TitleCaseCustomers), this.TitleCaseCustomers ? "true" : "false" ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.TitleCaseVehicles ), this.TitleCaseVehicles ? "true" : "false" ).ConfigureAwait(false);
await ExportSetting( wtr, nameof(this.CheckForUpdates ), this.CheckForUpdates ? "true" : "false" ).ConfigureAwait(false);
}
}
private static async Task ExportSetting(TextWriter wtr, String name, String value)
{
String valueEnc = Uri.EscapeDataString( value ); // to encode line-breaks, etc.
await wtr.WriteAsync( name ).ConfigureAwait(false);
await wtr.WriteAsync( ''='' ).ConfigureAwait(false);
await wtr.WriteLineAsync( valueEnc ).ConfigureAwait(false);
}
}