¿Cómo ejecutaría un método asíncrono de Tarea <T> sincrónicamente?


628

Estoy aprendiendo sobre async / await, y me encontré con una situación en la que necesito llamar a un método asincrónico sincrónicamente. ¿Cómo puedo hacer eso?

Método asíncrono:

public async Task<Customers> GetCustomers()
{
    return await Service.GetCustomersAsync();
}

Uso normal:

public async void GetCustomers()
{
    customerList = await GetCustomers();
}

He intentado usar lo siguiente:

Task<Customer> task = GetCustomers();
task.Wait()

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Task<Customer> task = GetCustomers();
while(task.Status != TaskStatus.RanToCompletion)

También probé una sugerencia desde aquí , sin embargo, no funciona cuando el despachador está en estado suspendido.

public static void WaitWithPumping(this Task task) 
{
        if (task == null) throw new ArgumentNullException(“task”);
        var nestedFrame = new DispatcherFrame();
        task.ContinueWith(_ => nestedFrame.Continue = false);
        Dispatcher.PushFrame(nestedFrame);
        task.Wait();
}

Aquí está la excepción y el seguimiento de pila de la llamada RunSynchronously:

System.InvalidOperationException

Mensaje : RunSynchronously no se puede invocar en una tarea desvinculada a un delegado.

InnerException : nulo

Fuente : mscorlib

StackTrace :

          at System.Threading.Tasks.Task.InternalRunSynchronously(TaskScheduler scheduler)
   at System.Threading.Tasks.Task.RunSynchronously()
   at MyApplication.CustomControls.Controls.MyCustomControl.CreateAvailablePanelList() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 638
   at MyApplication.CustomControls.Controls.MyCustomControl.get_AvailablePanels() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 233
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>b__36(DesktopPanel panel) in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 597
   at System.Collections.Generic.List`1.ForEach(Action`1 action)
   at MyApplication.CustomControls.Controls.MyCustomControl.<CreateOpenPanelList>d__3b.MoveNext() in C:\Documents and Settings\...\MyApplication.CustomControls\Controls\MyCustomControl.xaml.cs:line 625
   at System.Runtime.CompilerServices.TaskAwaiter.<>c__DisplayClass7.<TrySetContinuationForAwait>b__1(Object state)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.DispatcherOperation.InvokeImpl()
   at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
   at System.Threading.ExecutionContext.runTryCode(Object userData)
   at System.Runtime.CompilerServices.RuntimeHelpers.ExecuteCodeWithGuaranteedCleanup(TryCode code, CleanupCode backoutCode, Object userData)
   at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Windows.Threading.DispatcherOperation.Invoke()
   at System.Windows.Threading.Dispatcher.ProcessQueue()
   at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
   at System.Windows.Threading.Dispatcher.InvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
   at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
   at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
   at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
   at System.Windows.Threading.Dispatcher.Run()
   at System.Windows.Application.RunDispatcher(Object ignore)
   at System.Windows.Application.RunInternal(Window window)
   at System.Windows.Application.Run(Window window)
   at System.Windows.Application.Run()
   at MyApplication.App.Main() in C:\Documents and Settings\...\MyApplication\obj\Debug\App.g.cs:line 50
   at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
   at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
   at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
   at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean ignoreSyncCtx)
   at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
   at System.Threading.ThreadHelper.ThreadStart()

46
La mejor respuesta a la pregunta "¿Cómo puedo llamar a un método asíncrono sincrónicamente" es "no". Hay trucos para intentar forzarlo a funcionar, pero todos tienen trampas muy sutiles. En su lugar, realice una copia de seguridad y corrija el código que hace que "necesite" hacer esto.
Stephen Cleary

58
@Stephen Cleary Absolutamente de acuerdo, pero a veces es simplemente inevitable, como cuando su código depende de alguna API de terceros que no utiliza async / wait. Además, si se vincula a las propiedades de WPF cuando se usa MVVM, es literalmente imposible usar async / wait, ya que esto no es compatible con las propiedades.
Contango

3
@StephenCleary No siempre. Estoy construyendo una DLL que se importará en GeneXus . No admite palabras clave asíncronas / en espera, por lo que debo usar solo métodos sincrónicos.
Dinei

55
@StephenCleary 1) GeneXus es una herramienta de tercer punto y no tengo acceso a su código fuente; 2) GeneXus ni siquiera tiene implementaciones de "funciones", por lo que no puedo darme cuenta de cómo podría implementar una "devolución de llamada" con este tipo de cosas. Seguramente sería una solución más difícil que usar Tasksincrónicamente; 3) Estoy integrando GeneXus con el controlador MongoDB C # , que expone algunos métodos solo de forma asíncrona
Dinei

1
@ygoe: El uso de una cerradura compatible con asíncrono, como por ejemplo SemaphoreSlim.
Stephen Cleary

Respuestas:


456

Aquí hay una solución que encontré que funciona para todos los casos (incluidos los despachadores suspendidos). No es mi código y todavía estoy trabajando para entenderlo completamente, pero funciona.

Se puede llamar usando:

customerList = AsyncHelpers.RunSync<List<Customer>>(() => GetCustomers());

El código es de aquí

public static class AsyncHelpers
{
    /// <summary>
    /// Execute's an async Task<T> method which has a void return value synchronously
    /// </summary>
    /// <param name="task">Task<T> method to execute</param>
    public static void RunSync(Func<Task> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        synch.Post(async _ =>
        {
            try
            {
                await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();

        SynchronizationContext.SetSynchronizationContext(oldContext);
    }

    /// <summary>
    /// Execute's an async Task<T> method which has a T return type synchronously
    /// </summary>
    /// <typeparam name="T">Return Type</typeparam>
    /// <param name="task">Task<T> method to execute</param>
    /// <returns></returns>
    public static T RunSync<T>(Func<Task<T>> task)
    {
        var oldContext = SynchronizationContext.Current;
        var synch = new ExclusiveSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(synch);
        T ret = default(T);
        synch.Post(async _ =>
        {
            try
            {
                ret = await task();
            }
            catch (Exception e)
            {
                synch.InnerException = e;
                throw;
            }
            finally
            {
                synch.EndMessageLoop();
            }
        }, null);
        synch.BeginMessageLoop();
        SynchronizationContext.SetSynchronizationContext(oldContext);
        return ret;
    }

    private class ExclusiveSynchronizationContext : SynchronizationContext
    {
        private bool done;
        public Exception InnerException { get; set; }
        readonly AutoResetEvent workItemsWaiting = new AutoResetEvent(false);
        readonly Queue<Tuple<SendOrPostCallback, object>> items =
            new Queue<Tuple<SendOrPostCallback, object>>();

        public override void Send(SendOrPostCallback d, object state)
        {
            throw new NotSupportedException("We cannot send to our same thread");
        }

        public override void Post(SendOrPostCallback d, object state)
        {
            lock (items)
            {
                items.Enqueue(Tuple.Create(d, state));
            }
            workItemsWaiting.Set();
        }

        public void EndMessageLoop()
        {
            Post(_ => done = true, null);
        }

        public void BeginMessageLoop()
        {
            while (!done)
            {
                Tuple<SendOrPostCallback, object> task = null;
                lock (items)
                {
                    if (items.Count > 0)
                    {
                        task = items.Dequeue();
                    }
                }
                if (task != null)
                {
                    task.Item1(task.Item2);
                    if (InnerException != null) // the method threw an exeption
                    {
                        throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
                    }
                }
                else
                {
                    workItemsWaiting.WaitOne();
                }
            }
        }

        public override SynchronizationContext CreateCopy()
        {
            return this;
        }
    }
}

28
Para algunos antecedentes sobre cómo funciona esto, Stephen Toub (Sr. Paralelo) escribió una serie de publicaciones sobre esto. Parte 1 Parte 2 Parte 3
Cameron MacFarland

18
Actualicé el código de John para trabajar sin envolver tareas en lambdas: github.com/tejacques/AsyncBridge . Esencialmente trabajas con bloques asíncronos con la instrucción de uso. Cualquier cosa dentro de un bloque de uso ocurre de forma asincrónica, con una espera al final. La desventaja es que debe desenvolver la tarea usted mismo en una devolución de llamada, pero sigue siendo bastante elegante, especialmente si necesita llamar a varias funciones asíncronas a la vez.
Tom Jacques

17
@StephenCleary Aunque generalmente estoy de acuerdo con usted en que el código debe ser asíncrono hasta el final, a veces se encuentra en una situación inviable en la que hay que forzarlo como una llamada sincrónica. Básicamente, mi situación es que todo mi código de acceso a datos está en modo asíncrono. Necesitaba construir un mapa del sitio basado en el mapa del sitio y la biblioteca de terceros que estaba usando era MvcSitemap. Ahora, cuando se extiende a través de la DynamicNodeProviderBaseclase base, no se puede declarar como asyncmétodo. O tuve que reemplazar con una nueva biblioteca, o simplemente llamar a una operación sincrónica.
justin.lovell

66
@ justin.lovell: Sí, las limitaciones de la biblioteca pueden obligarnos a introducir hacks, al menos hasta que se actualice la biblioteca. Parece que MvcSitemap es una situación en la que se requiere un hack (filtros MVC y acciones secundarias también); Me acaba de disuadir a la gente de esta, en general, debido a los cortes de este tipo son de vías de uso muy a menudo cuando se encuentran no es necesario. Con MVC en particular, algunas API ASP.NET/MVC asumen que tienen una AspNetSynchronizationContext, por lo que este truco en particular no funcionará si está llamando a esas API.
Stephen Cleary

55
Este código no funcionará. Si se llama desde un subproceso de grupo, puede desencadenar un punto muerto de hambre de subprocesos. Su interlocutor bloqueará la espera de que se complete la operación, lo que puede que nunca ocurra si ha agotado el grupo de subprocesos. Ver este artículo .
ZunTzu

318

Tenga en cuenta que esta respuesta tiene tres años. Lo escribí basado principalmente en una experiencia con .Net 4.0, y muy poco con 4.5 especialmente con async-await. En términos generales, es una buena solución simple, pero a veces rompe cosas. Por favor lea la discusión en los comentarios.

.Net 4.5

Solo usa esto:

// For Task<T>: will block until the task is completed...
var result = task.Result; 

// For Task (not Task<T>): will block until the task is completed...
task2.RunSynchronously();

Ver: TaskAwaiter , Task.Result , Task.RunSynchronously


.Net 4.0

Utilizar este:

var x = (IAsyncResult)task;
task.Start();

x.AsyncWaitHandle.WaitOne();

...o esto:

task.Start();
task.Wait();

67
.Resultpuede producir un punto muerto en ciertos escenarios
Jordy Langen

122
Resultpuede provocar un punto muerto en el asynccódigo , como lo describo en mi blog.
Stephen Cleary

8
@StephenCleary Leí tu publicación y la probé yo mismo. Sinceramente, creo que alguien en Microsoft estaba realmente borracho ... Es el mismo problema que los winforms y los hilos de fondo ...
AK_

99
La pregunta se refiere a una tarea que se devuelve mediante el método asincrónico. Tal tipo de Tarea ya puede haberse iniciado, ejecutado o cancelado, por lo que el uso del método Task.RunSynchronously puede dar lugar a InvalidOperationException . Consulte la página de MSDN: Task.RunSynchronously Method . Además, esa Tarea probablemente sea creada por los métodos Task.Factory.StartNew o Task.Run (dentro del método asíncrono), por lo que es peligroso intentar iniciarla nuevamente. Algunas condiciones de carrera pueden ocurrir en tiempo de ejecución. Por otro lado, Task.Wait y Task.Result pueden resultar en un punto muerto.
sgnsajgon

44
Ejecutar Sincrónicamente funcionó para mí ... No sé si me falta algo, pero esto parece preferible a los horrores de la respuesta marcada: solo estaba buscando una forma de desactivar el asíncrono para probar el código que solo estaba allí para detener la interfaz de
usuario

121

Sorprendido, nadie mencionó esto:

public Task<int> BlahAsync()
{
    // ...
}

int result = BlahAsync().GetAwaiter().GetResult();

No es tan bonito como algunos de los otros métodos aquí, pero tiene los siguientes beneficios:

  • no se traga excepciones (como Wait)
  • no envolverá ninguna excepción lanzada en un AggregateException(como Result)
  • funciona para ambos Tasky Task<T>pruébalo tú mismo! )

Además, dado que GetAwaiteres de tipo pato, esto debería funcionar para cualquier objeto que se devuelva desde un método asíncrono (como ConfiguredAwaitableo YieldAwaitable), no solo Tareas.


editar: Tenga en cuenta que es posible que este enfoque (o uso .Result) se interrumpa, a menos que se asegure de agregar .ConfigureAwait(false)cada vez que espere, para todos los métodos asincrónicos a los que se puede acceder BlahAsync()(no solo los que llama directamente). Explicación .

// In BlahAsync() body
await FooAsync(); // BAD!
await FooAsync().ConfigureAwait(false); // Good... but make sure FooAsync() and
                                        // all its descendants use ConfigureAwait(false)
                                        // too. Then you can be sure that
                                        // BlahAsync().GetAwaiter().GetResult()
                                        // won't deadlock.

Si eres demasiado vago para agregar en .ConfigureAwait(false)todas partes, y no te importa el rendimiento, puedes hacerlo alternativamente

Task.Run(() => BlahAsync()).GetAwaiter().GetResult()

1
Funciona para mí para cosas simples. Además, si el método devuelve una IAsyncOperation, primero tenía que convertirla en una tarea: BlahAsync (). AsTask (). GetAwaiter (). GetResult ();
Lee McPherson el

3
Esto provocó un punto muerto dentro de un método web asmx. Sin embargo, envolviendo la llamada al método en un Task.Run () lo hizo funcionar: Task.Run (() => BlahAsync ()). GetAwaiter (). GetResult ()
Augusto Barreto

Este enfoque me gusta más sintácticamente porque no involucra lambdas.
dythim

25
NO edite las respuestas de otras personas para insertar un enlace al suyo. Si crees que tu respuesta es mejor, déjala como un comentario.
Rachel

1
docs.microsoft.com/en-us/dotnet/api/… dice acerca de GetAwaiter()"Este método está destinado al usuario del compilador en lugar de usarse directamente en el código".
Theophilus

75

Es mucho más simple ejecutar la tarea en el grupo de subprocesos, en lugar de intentar engañar al programador para que la ejecute de forma sincrónica. De esa manera puedes estar seguro de que no se estancará. El rendimiento se ve afectado por el cambio de contexto.

Task<MyResult> DoSomethingAsync() { ... }

// Starts the asynchronous task on a thread-pool thread.
// Returns a proxy to the original task.
Task<MyResult> task = Task.Run(() => DoSomethingAsync());

// Will block until the task is completed...
MyResult result = task.Result; 

3
Entonces llamas a task.Wait (). El tipo de datos es simplemente Tarea.
Michael L Perry

1
Supongamos que DoSomethingAsync () es un método asíncrono de larga ejecución en su conjunto (internamente espera una tarea de larga duración), pero devuelve un control de flujo a su llamador rápidamente, por lo que el trabajo del argumento lambda también termina rápidamente. El resultado de Tusk.Run () puede Tarea <Task> o Tarea <Tarea <>> , por lo que está esperando un resultado de tarea externa que se complete rápidamente, pero tarea interna (debido a la espera de un trabajo de larga ejecución en método asíncrono) sigue funcionando Las conclusiones son que probablemente necesitemos usar el enfoque Unwrap () (como se hizo en la publicación @ J.Lennon) para lograr un comportamiento sincrónico del método asíncrono.
sgnsajgon

55
@sgnsajgon Estás equivocado. Task.Run es diferente de Task.Factory.StartNew, ya que automáticamente desenvuelve el resultado. Ver este artículo .
ZunTzu

1
¿Puedo escribir en su Task.Run(DoSomethingAsync)lugar? Esto elimina un nivel de delegados.
ygoe

1
Sí. Sin embargo, ir en la dirección opuesta, ya que en Task<MyResult> task = Task.Run(async () => await DoSomethingAsync());es más explícito y aborda la preocupación de @sgnsajgon de que podría estar devolviendo una Tarea <Tarea <MiResultado>>. La sobrecarga correcta de Task.Run se selecciona de cualquier manera, pero el delegado asíncrono hace que su intención sea obvia.
Michael L Perry

57

Estoy aprendiendo sobre async / await, y me encontré con una situación en la que necesito llamar a un método asincrónico sincrónicamente. ¿Cómo puedo hacer eso?

La mejor respuesta es que no , con los detalles que dependen de cuál es la "situación".

¿Es un captador / establecedor de propiedades? En la mayoría de los casos, es mejor tener métodos asincrónicos que "propiedades asincrónicas". (Para obtener más información, consulte la publicación de mi blog sobre propiedades asincrónicas ).

¿Es esta una aplicación MVVM y quieres hacer un enlace de datos asíncrono? Luego use algo como mi NotifyTask, como se describe en mi artículo de MSDN sobre enlace de datos asíncrono .

¿Es un constructor? Entonces, probablemente desee considerar un método de fábrica asíncrono. (Para obtener más información, consulte la publicación de mi blog sobre constructores asincrónicos ).

Casi siempre hay una mejor respuesta que hacer sync-over-async.

Si no es posible para su situación (y lo sabe haciendo una pregunta aquí que describe la situación ), entonces recomendaría simplemente usar código síncrono. Asíncrono todo el camino es mejor; sincronizar todo el camino es el segundo mejor. Sync-over-async no se recomienda.

Sin embargo, hay algunas situaciones en las que es necesaria la sincronización sobre sincronización. En concreto, se ven limitados por el código de llamada para que tenga que ser de sincronización (y no tienen absolutamente ninguna manera de re-pensar o re-estructura de su código para permitir la asincronía), y que tienen que llamar asíncrono código. Esta es una situación muy rara, pero surge de vez en cuando.

En ese caso, necesitaría usar uno de los hacks descritos en mi artículo sobre desarrollo brownfieldasync , específicamente:

  • Bloqueo (p GetAwaiter().GetResult(). Ej .). Tenga en cuenta que esto puede causar puntos muertos (como lo describo en mi blog).
  • Ejecutar el código en un subproceso de grupo de subprocesos (por ejemplo, Task.Run(..).GetAwaiter().GetResult()). Tenga en cuenta que esto solo funcionará si el código asincrónico se puede ejecutar en un subproceso de grupo de subprocesos (es decir, no depende de una interfaz de usuario o contexto ASP.NET).
  • Bucles de mensajes anidados. Tenga en cuenta que esto solo funcionará si el código asincrónico solo asume un contexto de subproceso único, no un tipo de contexto específico (muchos códigos UI y ASP.NET esperan un contexto específico).

Los bucles de mensajes anidados son los más peligrosos de todos los hacks, ya que provocan reencuentro . El reencuentro es extremadamente difícil de razonar, y (IMO) es la causa de la mayoría de los errores de aplicación en Windows. En particular, si está en el subproceso de la interfaz de usuario y bloquea en una cola de trabajo (esperando que se complete el trabajo asincrónico), entonces el CLR realmente envía algunos mensajes por usted: en realidad manejará algunos mensajes Win32 desde su código . Ah, y no tienes idea de qué mensajes, cuando Chris Brumme dice "¿No sería genial saber exactamente qué se bombeará? Desafortunadamente, bombear es un arte negro que está más allá de la comprensión mortal". , entonces realmente no tenemos esperanza de saberlo.

Entonces, cuando bloqueas así en un hilo de la interfaz de usuario, estás pidiendo problemas. Otra cita de cbrumme del mismo artículo: "De vez en cuando, los clientes dentro o fuera de la empresa descubren que estamos enviando mensajes durante el bloqueo administrado en un STA [subproceso de interfaz de usuario]. Esta es una preocupación legítima, porque saben que es muy difícil para escribir código que sea robusto frente a la reentrada ".

Sí lo es. Código muy difícil de escribir que es robusto frente a la reentrada. Y los bucles de mensajes anidados lo obligan a escribir código que sea robusto frente a la reentrada. Esta es la razón por la cual la respuesta aceptada (y más votada) para esta pregunta es extremadamente peligrosa en la práctica.

Si está completamente fuera de todas las demás opciones: no puede rediseñar su código, no puede reestructurarlo para que sea asíncrono; el código de llamada inmutable lo obliga a sincronizarse; no puede cambiar el código descendente para que se sincronice - no puede bloquear - no puede ejecutar el código asíncrono en un hilo separado - entonces y solo entonces debería considerar aceptar la reentrada.

Si te encuentras en esta esquina, recomendaría usar algo como Dispatcher.PushFramepara aplicaciones WPF , bucle Application.DoEventspara aplicaciones WinForm, y para el caso general, el mío AsyncContext.Run.


Stephen, hay otra pregunta muy similar a la que también proporcionaste una respuesta increíble. ¿Crees que uno de ellos puede cerrarse como duplicado o tal vez fusionar la solicitud o abrir el meta primero (ya que cada q tiene ~ 200K vistas 200+ votos)? Sugerencias?
Alexei Levenkov

1
@AlexeiLevenkov: No me siento bien haciendo eso, por algunas razones: 1) La respuesta a la pregunta vinculada está bastante desactualizada. 2) He escrito un artículo completo sobre el tema que creo que es más completo que cualquier SO Q / A existente. 3) La respuesta aceptada a esta pregunta es extremadamente popular. 4) Me opongo vehementemente a esa respuesta aceptada. Entonces, cerrar esto como un duplicado de eso sería un abuso de poder; cerrando eso como un duplicado de esto (o fusión) podría dar una respuesta peligrosa aún más. Lo dejo, y lo dejo a la comunidad.
Stephen Cleary

Okay. Consideraré mencionarlo en meta que de alguna manera.
Alexei Levenkov

99
Esta respuesta recorre un largo camino sobre mi cabeza. "Usar asíncrono completamente hacia abajo" es un consejo confuso, debido a que claramente no es posible seguirlo. Un programa con un Main()método asíncrono no se compila; en algún momento usted ha conseguido cerrar la brecha entre los mundos síncronas y asíncronas. No es una " situación muy rara" , es necesaria en literalmente cada programa que llama a un método asíncrono. No hay una opción para no "sincronizar a través de la sincronización" , solo una opción para desviar esa carga al método de llamada en lugar de cargarla en la que está escribiendo actualmente.
Mark Amery

1
Excelente. Estoy a punto de ponerasync todos los métodos en mi aplicación ahora. Y eso es mucho. ¿No puede ser esto el valor predeterminado?
ygoe

25

Si estoy leyendo su pregunta correctamente, el código que desea la llamada síncrona a un método asíncrono se está ejecutando en un hilo de despachador suspendido. Y realmente desea bloquear ese hilo sincrónicamente hasta que se complete el método asíncrono.

Los métodos asíncronos en C # 5 funcionan al cortar eficazmente el método en pedazos debajo del capó y devolver uno Taskque puede rastrear la finalización general de todo el shabang. Sin embargo, la forma en que se ejecutan los métodos cortados puede depender del tipo de expresión que se pasa al awaitoperador.

La mayoría de las veces, usará awaituna expresión de tipo Task. La implementación del awaitpatrón por parte de la tarea es "inteligente", ya que difiere de lo SynchronizationContextque básicamente hace que ocurra lo siguiente:

  1. Si el hilo que ingresa awaitestá en un hilo de bucle de mensaje de Dispatcher o WinForms, asegura que los fragmentos del método asíncrono ocurran como parte del procesamiento de la cola de mensajes.
  2. Si el subproceso que ingresa awaitestá en un subproceso de grupo de subprocesos, los fragmentos restantes del método asíncrono se producen en cualquier parte del grupo de subprocesos.

Es por eso que probablemente tengas problemas: la implementación del método asincrónico está tratando de ejecutar el resto en el Dispatcher, a pesar de que está suspendido.

.... retrocediendo! ....

Tengo que hacer la pregunta, ¿por qué? estás tratando de bloquear sincrónicamente un método asíncrono? Hacerlo sería contrario al propósito de por qué el método quería llamarse de forma asincrónica. En general, cuando comience a usar awaitun Dispatcher o un método de IU, querrá convertir todo su flujo de IU en asíncrono. Por ejemplo, si su pila de llamadas era algo como lo siguiente:

  1. [Parte superior] WebRequest.GetResponse()
  2. YourCode.HelperMethod()
  3. YourCode.AnotherMethod()
  4. YourCode.EventHandlerMethod()
  5. [UI Code].Plumbing() - WPF o WinFormscódigo
  6. [Bucle de mensajes] - WPFo WinFormsBucle de mensajes

Luego, una vez que el código se ha transformado para usar asíncrono, generalmente terminará con

  1. [Parte superior] WebRequest.GetResponseAsync()
  2. YourCode.HelperMethodAsync()
  3. YourCode.AnotherMethodAsync()
  4. YourCode.EventHandlerMethodAsync()
  5. [UI Code].Plumbing() - WPF o WinFormscódigo
  6. [Bucle de mensajes] - WPFo WinFormsBucle de mensajes

En realidad respondiendo

La clase AsyncHelpers anterior realmente funciona porque se comporta como un bucle de mensaje anidado, pero instala su propia mecánica paralela en el Dispatcher en lugar de intentar ejecutar en el Dispatcher. Esa es una solución para su problema.

Otra solución es ejecutar su método asíncrono en un hilo de grupo de subprocesos y luego esperar a que se complete. Hacerlo es fácil: puede hacerlo con el siguiente fragmento:

var customerList = TaskEx.RunEx(GetCustomers).Result;

La API final será Task.Run (...), pero con el CTP necesitará los sufijos Ex ( explicación aquí ).


+1 para la explicación detallada, sin embargo, TaskEx.RunEx(GetCustomers).Resultbloquea la aplicación cuando se ejecuta en un hilo de despachador suspendido. Además, el método GetCustomers () normalmente se ejecuta de forma asíncrona, sin embargo, en una situación debe ejecutarse de forma sincrónica, por lo que estaba buscando una manera de hacerlo sin crear una versión de sincronización del método.
Rachel

+1 para "¿por qué estás tratando de bloquear sincrónicamente un método asíncrono?" Siempre hay una manera de usar los asyncmétodos adecuadamente ; los bucles anidados ciertamente deben evitarse.
Stephen Cleary

24

Esto me esta funcionando bien

public static class TaskHelper
{
    public static void RunTaskSynchronously(this Task t)
    {
        var task = Task.Run(async () => await t);
        task.Wait();
    }

    public static T RunTaskSynchronously<T>(this Task<T> t)
    {
        T res = default(T);
        var task = Task.Run(async () => res = await t);
        task.Wait();
        return res;
    }
}

También debe usar el método Task.Unwrap , porque su instrucción Task.Wait provoca la espera de la Tarea externa (creada por Task.Run ), no la espera interna t Tarea pasada como parámetro del método de extensión. Su método Task.Run devuelve no Task <T>, sino Task <Task <T>>. En algunos escenarios simples, su solución puede funcionar debido a las optimizaciones de TaskScheduler, por ejemplo, utilizando el método TryExecuteTaskInline para ejecutar tareas dentro del hilo actual durante la operación de espera. Por favor, mire mi comentario a esta respuesta.
sgnsajgon

1
Eso no es correcto. Task.Run devolverá la tarea <T>. Vea esta sobrecarga msdn.microsoft.com/en-us/library/hh194918(v=vs.110).aspx
Clement

¿Cómo se supone que debe usarse? Este estancamientos en WPF:MyAsyncMethod().RunTaskSynchronously();
ygoe

18

La forma más simple que he encontrado para ejecutar tareas sincrónicamente y sin bloquear el hilo de la interfaz de usuario es usar RunSynchronously () como:

Task t = new Task(() => 
{ 
   //.... YOUR CODE ....
});
t.RunSynchronously();

En mi caso, tengo un evento que se dispara cuando ocurre algo. No sé cuántas veces ocurrirá. Entonces, utilizo el código anterior en mi evento, por lo que cada vez que se dispara, crea una tarea. Las tareas se ejecutan sincrónicamente y funciona muy bien para mí. Me sorprendió que me tomó tanto tiempo descubrir esto considerando lo simple que es. Por lo general, las recomendaciones son mucho más complejas y propensas a errores. Esto fue simple y limpio.


1
Pero, ¿cómo podríamos usar este método cuando el código asíncrono devuelve algo que necesitamos?
S.Serpooshan

16

Lo he enfrentado varias veces, principalmente en pruebas unitarias o en el desarrollo de un servicio de Windows. Actualmente siempre uso esta función:

        var runSync = Task.Factory.StartNew(new Func<Task>(async () =>
        {
            Trace.WriteLine("Task runSync Start");
            await TaskEx.Delay(2000); // Simulates a method that returns a task and
                                      // inside it is possible that there
                                      // async keywords or anothers tasks
            Trace.WriteLine("Task runSync Completed");
        })).Unwrap();
        Trace.WriteLine("Before runSync Wait");
        runSync.Wait();
        Trace.WriteLine("After runSync Waited");

Es simple, fácil y no tuve problemas.


Este es el único que no se estancó para mí.
AndreFeijo

15

Encontré este código en el componente Microsoft.AspNet.Identity.Core, y funciona.

private static readonly TaskFactory _myTaskFactory = new 
     TaskFactory(CancellationToken.None, TaskCreationOptions.None, 
     TaskContinuationOptions.None, TaskScheduler.Default);

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


13

Solo una pequeña nota: este enfoque:

Task<Customer> task = GetCustomers();
task.Wait()

Funciona para WinRT.

Dejame explicar:

private void TestMethod()
{
    Task<Customer> task = GetCustomers(); // call async method as sync and get task as result
    task.Wait(); // wait executing the method
    var customer = task.Result; // get's result.
    Debug.WriteLine(customer.Name); //print customer name
}
public class Customer
{
    public Customer()
    {
        new ManualResetEvent(false).WaitOne(TimeSpan.FromSeconds(5));//wait 5 second (long term operation)
    }
    public string Name { get; set; }
}
private Task<Customer> GetCustomers()
{
    return Task.Run(() => new Customer
    {
        Name = "MyName"
    });
}

Además, este enfoque solo funciona para las soluciones de la Tienda Windows.

Nota: esta forma no es segura para subprocesos si llama a su método dentro de otro método asíncrono (según los comentarios de @Servy)


Le expliqué esta solución, consulte la sección EDITAR.
RredCat

2
Esto puede resultar fácilmente en puntos muertos cuando se llama en situaciones asincrónicas.
Servicio

@Servy tiene sentido. Entonces, como me equivoco usando Wait (timeOut) puede ayudar, ¿verdad?
RredCat

1
Luego, debe preocuparse de que se alcance el tiempo de espera cuando la operación no se haya realizado realmente, lo cual es muy malo, y también el tiempo dedicado a esperar hasta el tiempo de espera en los casos en que se interrumpe (y en ese caso aún continúa encendido cuando no está hecho). Entonces no, eso no soluciona el problema.
Servicio

@Servy Parece que tengo que implementar CancellationTokenmi solución.
RredCat

10

En su código, su primera espera para que se ejecute la tarea pero no la ha iniciado, por lo que espera indefinidamente. Prueba esto:

Task<Customer> task = GetCustomers();
task.RunSynchronously();

Editar:

Dices que tienes una excepción. Publique más detalles, incluido el seguimiento de la pila.
Mono contiene el siguiente caso de prueba:

[Test]
public void ExecuteSynchronouslyTest ()
{
        var val = 0;
        Task t = new Task (() => { Thread.Sleep (100); val = 1; });
        t.RunSynchronously ();

        Assert.AreEqual (1, val);
}

Comprueba si esto funciona para ti. Si no es así, aunque es muy poco probable, es posible que tenga una versión extraña de Async CTP. Si funciona, es posible que desee examinar qué genera exactamente el compilador y cómo la Taskcreación de instancias es diferente de esta muestra.

Editar # 2:

Verifiqué con Reflector que la excepción que describiste ocurre cuando m_actiones null. Esto es un poco extraño, pero no soy un experto en Async CTP. Como dije, deberías descompilar tu código y ver cómo Taskse está instanciando exactamente cómo m_actiones null.


PD: ¿Cuál es el trato con los votos negativos ocasionales? ¿Cuidado para elaborar?


Ajusté mi pregunta para que el código que había intentado fuera un poco más claro. RunSynchronously devuelve un error de RunSynchronously may not be called on a task unbound to a delegate. Google no es de ayuda ya que todos los resultados están en chino ...
Rachel

Creo que la diferencia es que no creo la Tarea y luego trato de ejecutarla. En cambio, la tarea se crea mediante el método asíncrono cuando awaitse usa la palabra clave. La excepción publicada en mi comentario anterior es la excepción que obtengo, aunque es una de las pocas que no puedo buscar en Google y encontrar una causa o resolución.
Rachel

1
asyncy las asyncpalabras clave no son más que sintaxis de azúcar. Compilador genera código para crear Task<Customer>en el GetCustomers()modo que es donde me gustaría ver primero. En cuanto a la excepción, solo publicó un mensaje de excepción, que es inútil sin el tipo de excepción y el seguimiento de la pila. Llame al ToString()método de excepción y publique la salida en la pregunta.
Dan Abramov

@gaearon: publiqué los detalles de la excepción y el seguimiento de la pila en mi pregunta original.
Rachel

2
@gaearon Creo que recibió votos negativos porque su publicación no es aplicable a las preguntas. La discusión trata sobre métodos de espera asíncrona, no sobre métodos simples de devolución de tareas. Además, en mi opinión, el mecanismo de espera asíncrona es un azúcar de sintaxis, pero no tan trivial: hay continuación, captura de contexto, reanudación de contexto local, manejo mejorado de excepciones locales y más. Entonces, no debe invocar el método RunSynchronously en el resultado del método asincrónico, porque por definición el método asincrónico debería devolver la tarea que está actualmente al menos programada, y más de una vez está en estado de ejecución.
sgnsajgon

9

Probado en .Net 4.6. También puede evitar el punto muerto.

Para el método asíncrono que regresa Task.

Task DoSomeWork();
Task.Run(async () => await DoSomeWork()).Wait();

Para el método asíncrono que regresa Task<T>

Task<T> GetSomeValue();
var result = Task.Run(() => GetSomeValue()).Result;

Editar :

Si la persona que llama se está ejecutando en el subproceso del grupo de subprocesos (o si también está en una tarea), aún puede causar un punto muerto en alguna situación.


1
Mi respuesta después de casi 8 años :) El segundo ejemplo: producirá un punto muerto en todo el contexto programado que se usa principalmente (aplicación de consola / .NET core / aplicación de escritorio / ...). aquí tienes más información general de lo que estoy hablando ahora: medium.com/rubrikkgroup/…
W92

Resultes perfecto para el trabajo si desea una llamada sincrónica y, de lo contrario, es totalmente peligroso. No hay nada en el nombre Resulto en la inteligencia Resultque indique que es una llamada de bloqueo. Realmente debería ser renombrado.
Zodman

5

use el siguiente código

Task.WaitAll(Task.Run(async () => await service.myAsyncMethod()));

4

¿Por qué no crear una llamada como:

Service.GetCustomers();

eso no es asíncrono.


44
Eso es lo que haré si no puedo hacer que esto funcione ... crear una versión de sincronización además de una versión asíncrona
Rachel

3

Esta respuesta está diseñada para cualquier persona que use WPF para .NET 4.5.

Si intenta ejecutar Task.Run()en el hilo de la GUI, task.Wait()se bloqueará indefinidamente, si no tiene la asyncpalabra clave en la definición de su función.

Este método de extensión resuelve el problema verificando si estamos en el hilo de la GUI y, de ser así, ejecutando la tarea en el hilo del despachador de WPF.

Esta clase puede actuar como el pegamento entre el mundo asíncrono / espera y el mundo no asíncrono / espera, en situaciones donde es inevitable, como las propiedades MVVM o dependencias de otras API que no usan asíncrono / espera.

/// <summary>
///     Intent: runs an async/await task synchronously. Designed for use with WPF.
///     Normally, under WPF, if task.Wait() is executed on the GUI thread without async
///     in the function signature, it will hang with a threading deadlock, this class 
///     solves that problem.
/// </summary>
public static class TaskHelper
{
    public static void MyRunTaskSynchronously(this Task task)
    {
        if (MyIfWpfDispatcherThread)
        {
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E99213. Task did not run to completion.");
            }
        }
        else
        {
            task.Wait();
            if (task.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E33213. Task did not run to completion.");
            }
        }
    }

    public static T MyRunTaskSynchronously<T>(this Task<T> task)
    {       
        if (MyIfWpfDispatcherThread)
        {
            T res = default(T);
            var result = Dispatcher.CurrentDispatcher.InvokeAsync(async () => { res = await task; });
            result.Wait();
            if (result.Status != DispatcherOperationStatus.Completed)
            {
                throw new Exception("Error E89213. Task did not run to completion.");
            }
            return res;
        }
        else
        {
            T res = default(T);
            var result = Task.Run(async () => res = await task);
            result.Wait();
            if (result.Status != TaskStatus.RanToCompletion)
            {
                throw new Exception("Error E12823. Task did not run to completion.");
            }
            return res;
        }
    }

    /// <summary>
    ///     If the task is running on the WPF dispatcher thread.
    /// </summary>
    public static bool MyIfWpfDispatcherThread
    {
        get
        {
            return Application.Current.Dispatcher.CheckAccess();
        }
    }
}

3

Simplemente llamar .Result;o .Wait()es un riesgo de puntos muertos como muchos han dicho en los comentarios. Como a la mayoría de nosotros nos gustan las líneas, puede usarlas para.Net 4.5<

Adquisición de un valor a través de un método asíncrono:

var result = Task.Run(() => asyncGetValue()).Result;

Llamar sincrónicamente a un método asíncrono

Task.Run(() => asyncMethod()).Wait();

No se producirán problemas de punto muerto debido al uso de Task.Run.

Fuente:

https://stackoverflow.com/a/32429753/3850405


1

Creo que el siguiente método auxiliar también podría resolver el problema.

private TResult InvokeAsyncFuncSynchronously<TResult>(Func< Task<TResult>> func)
    {
        TResult result = default(TResult);
        var autoResetEvent = new AutoResetEvent(false);

        Task.Run(async () =>
        {
            try
            {
                result = await func();
            }
            catch (Exception exc)
            {
                mErrorLogger.LogError(exc.ToString());
            }
            finally
            {
                autoResetEvent.Set();
            }
        });
        autoResetEvent.WaitOne();

        return result;
    }

Se puede usar de la siguiente manera:

InvokeAsyncFuncSynchronously(Service.GetCustomersAsync);

1
Por favor explique la votación
donttellya

2
... Todavía estoy ansioso por saber por qué esta respuesta fue rechazada.
donttellya

No es un verdadero "sincrónico". Cree dos hilos y espere los primeros resultados del otro.
tmt

y todo aparte, esta es una muy mala idea.
Dan Pantry

1
Acabo de escribir un código casi idéntico (línea por línea lo mismo) pero en lugar de usar SemaphoreSlim en lugar del evento de reinicio automático. Ojalá hubiera visto esto antes. Encuentro este enfoque para evitar puntos muertos y mantiene su código asíncrono funcionando igual que en escenarios asincrónicos verdaderos. No estoy seguro de por qué es una mala idea. Parece mucho más limpio que los otros enfoques que he visto anteriormente.
tmrog

0

Esto funciona para mi

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp2
{
    public static class AsyncHelper
    {
        private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None, TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);

        public static void RunSync(Func<Task> func)
        {
            _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }

        public static TResult RunSync<TResult>(Func<Task<TResult>> func)
        {
            return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
        }
    }

    class SomeClass
    {
        public async Task<object> LoginAsync(object loginInfo)
        {
            return await Task.FromResult(0);
        }
        public object Login(object loginInfo)
        {
            return AsyncHelper.RunSync(() => LoginAsync(loginInfo));
            //return this.LoginAsync(loginInfo).Result.Content;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            var someClass = new SomeClass();

            Console.WriteLine(someClass.Login(1));
            Console.ReadLine();
        }
    }
}

-1

He descubierto que SpinWait funciona bastante bien para esto.

var task = Task.Run(()=>DoSomethingAsyncronous());

if(!SpinWait.SpinUntil(()=>task.IsComplete, TimeSpan.FromSeconds(30)))
{//Task didn't complete within 30 seconds, fail...
   return false;
}

return true;

El enfoque anterior no necesita usar .Result o .Wait (). También le permite especificar un tiempo de espera para que no se quede atascado para siempre en caso de que la tarea nunca se complete.


1
El voto negativo sugiere que a alguien no le gusta este método. ¿Hay alguien que pueda comentar las desventajas de esto?
Grax32

En ausencia del votante que dice POR QUÉ se dio el voto negativo, ¿alguien puede votarlo? :-)
Curtis

1
Esto es sondeo (giro), el delegado tomará subprocesos del grupo hasta 1000 veces por segundo. Es posible que no devuelva el control inmediatamente después de finalizar la tarea ( error de hasta 10 + ms ). Si termina el tiempo de espera, la tarea continuará ejecutándose, lo que hace que el tiempo de espera sea prácticamente inútil.
Sinatr

En realidad, estoy usando esto por todas partes en mi código y cuando se cumple la condición, SpinWaitSpinUntil () sale inmediatamente. Entonces, lo que ocurra primero, 'condición cumplida' o tiempo de espera, la tarea se cierra. No continúa ejecutándose.
Curtis

-3

En wp8:

Envolverlo:

Task GetCustomersSynchronously()
{
    Task t = new Task(async () =>
    {
        myCustomers = await GetCustomers();
    }
    t.RunSynchronously();
}

Llámalo:

GetCustomersSynchronously();

3
No, esto no funcionará, porque la tarea no espera al delegado del constructor (es un delegado y no una tarea ...)
Rico Suter

-4
    private int GetSync()
    {
        try
        {
            ManualResetEvent mre = new ManualResetEvent(false);
            int result = null;

            Parallel.Invoke(async () =>
            {
                result = await SomeCalcAsync(5+5);
                mre.Set();
            });

            mre.WaitOne();
            return result;
        }
        catch (Exception)
        {
            return null;
        }
    }

-5

O simplemente podrías ir con:

customerList = Task.Run<List<Customer>>(() => { return GetCustomers(); }).Result;

Para que esto se compile, asegúrese de hacer referencia al ensamblaje de extensión:

System.Net.Http.Formatting

-9

Intenta seguir el código que me funciona:

public async void TaskSearchOnTaskList (SearchModel searchModel)
{
    try
    {
        List<EventsTasksModel> taskSearchList = await Task.Run(
            () => MakeasyncSearchRequest(searchModel),
            cancelTaskSearchToken.Token);

        if (cancelTaskSearchToken.IsCancellationRequested
                || string.IsNullOrEmpty(rid_agendaview_search_eventsbox.Text))
        {
            return;
        }

        if (taskSearchList == null || taskSearchList[0].result == Constants.ZERO)
        {
            RunOnUiThread(() => {
                textViewNoMembers.Visibility = ViewStates.Visible;                  
                taskListView.Visibility = ViewStates.Gone;
            });

            taskSearchRecureList = null;

            return;
        }
        else
        {
            taskSearchRecureList = TaskFooterServiceLayer
                                       .GetRecurringEvent(taskSearchList);

            this.SetOnAdapter(taskSearchRecureList);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine("ActivityTaskFooter -> TaskSearchOnTaskList:" + ex.Message);
    }
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.