Llamar al método asíncrono sincrónicamente

231

Tengo un asyncmetodo:

public async Task<string> GenerateCodeAsync()
{
    string code = await GenerateCodeService.GenerateCodeAsync();
    return code;
}

Necesito llamar a este método desde un método sincrónico.

¿Cómo puedo hacer esto sin tener que duplicar el GenerateCodeAsyncmétodo para que esto funcione sincrónicamente?

Actualizar

Sin embargo, no se encontró una solución razonable.

Sin embargo, veo que HttpClientya implementa este patrón

using (HttpClient client = new HttpClient())
{
    // async
    HttpResponseMessage responseAsync = await client.GetAsync(url);

    // sync
    HttpResponseMessage responseSync = client.GetAsync(url).Result;
}
Catalin
fuente
1
Esperaba una solución más simple, pensando que asp.net manejó esto mucho más fácilmente que escribir tantas líneas de código
Catalin
¿Por qué no abrazar el código asíncrono? Idealmente, querría más código asíncrono, no menos.
Paulo Morgado
54
[¿Por qué no abrazar el código asincrónico?] ¡Ja, puede ser precisamente porque uno está adoptando el código asincrónico que necesitan esta solución a medida que se convierten grandes partes del proyecto! No puedes reconstruir Roma en un día.
Nicholas Petersen
1
@NicholasPetersen a veces la biblioteca de terceros puede obligarlo a hacer esto. Ejemplo de creación de mensajes dinámicos en el método WithMessage a partir de FluentValidation. No hay una API asíncrona para esto debido al diseño de la biblioteca: las sobrecargas de WithMessage son estáticas. Otros métodos para pasar argumentos dinámicos a WithMessage son extraños.
H.Wojtowicz

Respuestas:

278

Puede acceder a la Resultpropiedad de la tarea, lo que hará que su hilo se bloquee hasta que el resultado esté disponible:

string code = GenerateCodeAsync().Result;

Nota: en algunos casos, esto puede llevar a un punto muerto: su llamada para Resultbloquear el hilo principal, evitando así que se ejecute el resto del código asincrónico. Tiene las siguientes opciones para asegurarse de que esto no suceda:

¡Esto no significa que deba agregar sin pensar .ConfigureAwait(false)después de todas sus llamadas asíncronas! Para un análisis detallado de por qué y cuándo debe usar .ConfigureAwait(false), consulte la siguiente publicación de blog:

Heinzi
fuente
33
Si invocar se resultarriesga a un punto muerto, ¿cuándo es seguro obtener el resultado? ¿Cada llamada asincrónica requiere Task.Runo ConfigureAwait(false)?
Robert Harvey
44
No hay un "hilo principal" en ASP.NET (a diferencia de una aplicación GUI), pero el punto muerto aún es posible debido a cómo AspNetSynchronizationContext.Post serializa las continuaciones asíncronas:Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action)); _lastScheduledTask = newTask;
noseratio
44
@RobertHarvey: Si no tienes control sobre la implementación del método asíncrono que estás bloqueando, entonces sí, debes envolverlo Task.Runpara mantenerte seguro. O use algo como WithNoContextreducir la conmutación de subprocesos redundantes.
noseratio
10
NOTA: Las llamadas .Resultaún pueden llegar a un punto muerto si la persona que llama está en el grupo de subprocesos. Tomemos un escenario en el que el grupo de subprocesos sea de tamaño 32 y las 32 tareas se estén ejecutando y Wait()/Resultesperando una 33a tarea aún por programar que quiera ejecutarse en uno de los subprocesos en espera.
Warty
55

Debería obtener el camarero ( GetAwaiter()) y finalizar la espera para completar la tarea asincrónica ( GetResult()).

string code = GenerateCodeAsync().GetAwaiter().GetResult();
Diego Torres
fuente
38
Nos hemos encontrado con puntos muertos utilizando esta solución. Ten cuidado.
Oliver
66
MSDNTask.GetAwaiter : este método está destinado al uso del compilador en lugar de usarse en el código de la aplicación.
foka
Todavía recibí el mensaje emergente de diálogo de error (en contra de mi voluntad), con los botones 'Cambiar a' o 'Reintentar' ... sin embargo, la llamada realmente se ejecuta y regresa con una respuesta adecuada.
Jonathan Hansen
30

Debería poder hacer esto usando delegados, expresión lambda

private void button2_Click(object sender, EventArgs e)
    {

        label1.Text = "waiting....";

        Task<string> sCode = Task.Run(async () =>
        {
            string msg =await GenerateCodeAsync();
            return msg;
        });

        label1.Text += sCode.Result;

    }

    private Task<string> GenerateCodeAsync()
    {
        return Task.Run<string>(() => GenerateCode());
    }

    private string GenerateCode()
    {
        Thread.Sleep(2000);
        return "I m back" ;
    }
Faiyaz
fuente
Este fragmento no se compilará. El tipo de retorno de Task.Run es Task. Consulte este blog de MSDN para obtener una explicación completa.
Apareció el
55
Gracias por señalar, sí, devuelve el tipo de tarea. Reemplazar "string sCode" por Task <string> o var sCode debería resolverlo. Agregar un código de compilación completo para mayor facilidad.
Faiyaz
20

Necesito llamar a este método desde un método sincrónico.

Es posible con GenerateCodeAsync().Resulto GenerateCodeAsync().Wait(), como sugiere la otra respuesta. Esto bloquearía el hilo actual hasta que se GenerateCodeAsynchaya completado.

Sin embargo, su pregunta está etiquetada con , y también dejaste el comentario:

Esperaba una solución más simple, pensando que asp.net manejó esto mucho más fácil que escribir tantas líneas de código

Mi punto es que no deberías estar bloqueando un método asincrónico en ASP.NET. Esto reducirá la escalabilidad de su aplicación web y puede crear un punto muerto (cuando se publica una awaitcontinuación en el interior ). El uso para descargar algo en un subproceso de grupo y luego bloquear dañará la escalabilidad aún más, ya que incurre en +1 subproceso adicional para procesar una solicitud HTTP dada.GenerateCodeAsyncAspNetSynchronizationContextTask.Run(...).Result

ASP.NET tiene soporte incorporado para métodos asincrónicos, ya sea a través de controladores asincrónicos (en ASP.NET MVC y API web) o directamente a través AsyncManagery PageAsyncTasken ASP.NET clásico. Deberías usarlo. Para más detalles, verifique esta respuesta .

nariz
fuente
Estoy sobrescribiendo el SaveChanges()método de DbContext, y aquí llamo a los métodos asíncronos, por lo que desafortunadamente el controlador asíncrono no me ayudará en esta situación
Catalin
3
@RaraituL, en general, no combina código asíncrono y de sincronización, elija otro modelo. Puede implementar ambos SaveChangesAsyncy SaveChanges, simplemente asegúrese de que no se los llame a ambos en el mismo proyecto ASP.NET.
noseratio
44
No todos los .NET MVCfiltros admiten código asíncrono, por ejemplo IAuthorizationFilter, por lo que no puedo usarlo asynctodo el tiempo
Catalin
3
@Noseratio ese es un objetivo poco realista. Hay demasiadas bibliotecas con código asíncrono y síncrono, así como situaciones en las que no es posible usar un solo modelo. MVC ActionFilters no admite código asincrónico, por ejemplo.
Justin Skiles
99
@Noserato, la pregunta es sobre llamar al método asincrónico desde síncrono. En algún momento no puedes cambiar la API que estás implementando. Supongamos que implementa una interfaz síncrona desde un marco de terceros "A" (no puede reescribir el marco de manera asincrónica), pero la biblioteca de terceros "B" que está tratando de usar en su implementación solo es asíncrona. También producto resultante también es la biblioteca y se puede utilizar en cualquier lugar incluyendo ASP.NET etc.
dimzon
19

Microsoft Identity tiene métodos de extensión que llaman a métodos asíncronos sincrónicamente. Por ejemplo, hay un método GenerateUserIdentityAsync () e igual CreateIdentity ()

Si observa UserManagerExtensions.CreateIdentity () se verá así:

 public static ClaimsIdentity CreateIdentity<TUser, TKey>(this UserManager<TUser, TKey> manager, TUser user,
        string authenticationType)
        where TKey : IEquatable<TKey>
        where TUser : class, IUser<TKey>
    {
        if (manager == null)
        {
            throw new ArgumentNullException("manager");
        }
        return AsyncHelper.RunSync(() => manager.CreateIdentityAsync(user, authenticationType));
    }

Ahora veamos qué hace AsyncHelper.RunSync

  public static TResult RunSync<TResult>(Func<Task<TResult>> func)
    {
        var cultureUi = CultureInfo.CurrentUICulture;
        var culture = CultureInfo.CurrentCulture;
        return _myTaskFactory.StartNew(() =>
        {
            Thread.CurrentThread.CurrentCulture = culture;
            Thread.CurrentThread.CurrentUICulture = cultureUi;
            return func();
        }).Unwrap().GetAwaiter().GetResult();
    }

Entonces, este es su contenedor para el método asíncrono. Y, por favor, no lea los datos del Resultado: potencialmente bloqueará su código en ASP.

Hay otra forma, que es sospechosa para mí, pero también puedes considerarla

  Result r = null;

            YourAsyncMethod()
                .ContinueWith(t =>
                {
                    r = t.Result;
                })
                .Wait();
Vitaliy Markitanov
fuente
3
¿Cuál considera que es el problema con la segunda forma que ha sugerido?
David Clarke
@DavidClarke probablemente sea el problema de seguridad de subprocesos al acceder a una variable no volátil desde múltiples subprocesos sin bloqueo.
Theodor Zoulias
9

Para evitar puntos muertos siempre trato de usar Task.Run() cuando tengo que llamar a un método asíncrono sincrónicamente que @Heinzi menciona.

Sin embargo, el método tiene que modificarse si el método asíncrono usa parámetros. Por ejemplo Task.Run(GenerateCodeAsync("test")).Resultda el error:

Argumento 1: no se puede convertir de ' System.Threading.Tasks.Task<string>' a 'System.Action'

Esto podría llamarse así:

string code = Task.Run(() => GenerateCodeAsync("test")).Result;
Ogglas
fuente
6

La mayoría de las respuestas en este hilo son complejas o resultarán en un punto muerto.

El siguiente método es simple y evitará un punto muerto porque estamos esperando que termine la tarea y solo entonces obteniendo su resultado:

var task = Task.Run(() => GenerateCodeAsync()); 
task.Wait();
string code = task.Result;

Además, aquí hay una referencia al artículo de MSDN que habla exactamente de lo mismo: https://blogs.msdn.microsoft.com/jpsanders/2017/08/28/asp-net-do-not-use-task-result- en contexto principal /

kamalpreet
fuente
1

Prefiero un enfoque sin bloqueo:

            Dim aw1=GenerateCodeAsync().GetAwaiter()
            While Not aw1.IsCompleted
                Application.DoEvents()
            End While
Zibri
fuente
0

Bueno, estoy usando este enfoque:

    private string RunSync()
    {
        var task = Task.Run(async () => await GenerateCodeService.GenerateCodeAsync());
        if (task.IsFaulted && task.Exception != null)
        {
            throw task.Exception;
        }

        return task.Result;
    }
Jiří Herník
fuente
-1

La otra forma podría ser si desea esperar hasta que finalice la tarea:

var t = GenerateCodeService.GenerateCodeAsync();
Task.WhenAll(t);
string code = t.Result;
frablaser
fuente
1
Eso es completamente incorrecto. WhenAll también devuelve una tarea, que no estás esperando.
Robert Schmidt
-1

EDITAR:

La tarea tiene el método Wait, Task.Wait (), que espera a que se resuelva la "promesa" y luego continúa, lo que la hace sincrónica. ejemplo:


async Task<String> MyAsyncMethod() { ... }

String mySyncMethod() {

    return MyAsyncMethod().Wait();
}
Avi Tshuva
fuente
3
Por favor, explique su respuesta. ¿Cómo se usa? ¿Cómo ayuda específicamente a responder la pregunta?
Scratte
-2

Si tiene un método asincrónico llamado " RefreshList ", puede llamar a ese método asincrónico desde un método no asincrónico como se muestra a continuación.

Task.Run(async () => { await RefreshList(); });
dush88c
fuente