Recabamos en todo el mundo online y de esta manera brindarte la solución a tu inquietud, en caso de preguntas puedes dejar tu duda y te responderemos porque estamos para ayudarte.
Solución:
Task.Run
mayo publicar la operación para que se procese en un subproceso diferente. Esa es la única diferencia.
Esto puede ser de utilidad, por ejemplo, si LongProcess
no es realmente asincrónico, hará que la persona que llama regrese más rápido. Pero para un método verdaderamente asincrónico, no tiene sentido usar Task.Run
, y puede resultar en un desperdicio innecesario.
Sin embargo, tenga cuidado porque el comportamiento de Task.Run
cambiará según la resolución de sobrecarga. En su ejemplo, el Func
Se elegirá una sobrecarga, que esperará (correctamente) LongProcess
para terminar. Sin embargo, si se utilizó un delegado que no devuelve tareas, Task.Run
solo esperará la ejecución hasta el primer await
(tenga en cuenta que así es como TaskFactory.StartNew
voluntad siempre comportarse, así que no use eso).
Muy a menudo, la gente piensa que async-await se realiza mediante varios subprocesos. De hecho, todo está hecho por un hilo.
Vea la adición a continuación sobre esta declaración de un hilo
Lo que me ayudó mucho a entender async-await es esta entrevista con Eric Lippert sobre async-await. En algún lugar intermedio, compara la espera asíncrona con un cocinero que tiene que esperar a que hierva un poco de agua. En lugar de no hacer nada, mira a su alrededor para ver si todavía hay algo más que hacer, como cortar las cebollas. Si ha terminado, y el agua todavía no hierve, comprueba si hay algo más que hacer, y así sucesivamente hasta que no tiene nada que hacer más que esperar. En ese caso vuelve a lo primero que esperaba.
Si su procedimiento llama a una función en espera, estamos seguros de que en algún lugar de esta función en espera hay una llamada a una función en espera; de lo contrario, la función no estaría en espera. De hecho, su compilador le advertirá si olvida esperar en algún lugar de su función de espera.
Si su función en espera llama a la otra función en espera, entonces el hilo ingresa a esta otra función y comienza a hacer las cosas en esta función y profundiza en otras funciones hasta que encuentra una espera.
En lugar de esperar los resultados, el hilo sube en su pila de llamadas para ver si hay otras piezas de código que pueda procesar hasta que vea una espera. Vuelve a subir en la pila de llamadas, procesa hasta esperar, etc. Una vez que todos están esperando, el hilo busca el final de espera y continúa una vez que haya terminado.
Esto tiene la ventaja de que si la persona que llama a su función en espera no necesita el resultado de su función, pero puede hacer otras cosas antes de que se necesite el resultado, el hilo puede hacer estas otras cosas en lugar de esperar dentro de su función.
Una llamada sin esperar inmediatamente el resultado se vería así:
private async Task MyFunction()
TasktaskA = SomeFunctionAsync(...)
// I don't need the result yet, I can do something else
DoSomethingElse();
// now I need the result of SomeFunctionAsync, await for it:
ReturnType result = await TaskA;
// now you can use object result
Tenga en cuenta que en este escenario todo se realiza mediante un hilo. Mientras tu hilo tenga algo que hacer, estará ocupado.
Adición. No lo es true que solo hay un hilo involucrado. Cualquier hilo que no tenga nada que hacer podría continuar procesando su código después de una espera. Si verifica la identificación del hilo, puede ver que esta identificación se puede cambiar después de la espera. El hilo continuo tiene el mismo
context
como el hilo original, por lo que puede actuar como si fuera el hilo original. No es necesario verificarInvokeRequired
, no es necesario utilizar mutex o secciones críticas. Para su código, esto es como si hubiera un hilo involucrado.
El enlace al artículo al final de esta respuesta explica un poco más sobre el contexto del hilo.
Verá funciones en espera principalmente donde algún otro proceso tiene que hacer cosas, mientras que su hilo solo tiene que esperar sin hacer nada hasta que termine la otra cosa. Algunos ejemplos son enviar datos a través de Internet, guardar un archivo, comunicarse con una base de datos, etc.
Sin embargo, a veces se deben hacer algunos cálculos pesados y desea que su hilo esté libre para hacer otra cosa, como responder a la entrada del usuario. En ese caso, puede iniciar una acción en espera como si llamara a una función asíncrona.
Task LetSomeoneDoHeavyCalculations(...)
DoSomePreparations()
// start a different thread that does the heavy calculations:
var myTask = Task.Run( () => DoHeavyCalculations(...))
// now you are free to do other things
DoSomethingElse();
// once you need the result of the HeavyCalculations await for it
var myResult = await myTask;
// use myResult
...
Ahora un hilo diferente está haciendo los cálculos pesados mientras que su hilo está libre para hacer otras cosas. Una vez que comienza a esperar, la persona que llama puede hacer cosas hasta que comience a esperar. Efectivamente, su hilo será bastante libre de reaccionar a la entrada del usuario. Sin embargo, este solo será el caso si todos están esperando. Mientras su hilo está ocupado haciendo cosas, su hilo no puede reaccionar a la entrada del usuario. Por lo tanto, asegúrese siempre de que si cree que su subproceso de la interfaz de usuario tiene que realizar un procesamiento ocupado que lleva algún tiempo, use Task.Run y deje que otro subproceso lo haga
Otro artículo que me ayudó: Async-Await del brillante explicador Stephen Cleary
Esta respuesta trata con el caso específico de esperar un método asíncrono en el controlador de eventos de una aplicación GUI. En este caso, el primer enfoque tiene una ventaja significativa sobre el segundo. Antes de explicar por qué, reescribamos los dos enfoques de una manera que refleje claramente el contexto de esta respuesta. Lo que sigue solo es relevante para los controladores de eventos de las aplicaciones GUI.
private async void Button1_Click(object sender, EventArgs args)
await Task.Run(async () => await LongProcessAsync());
vs
private async void Button1_Click(object sender, EventArgs args)
await LongProcessAsync();
Agregué el sufijo Async
en el nombre del método, para cumplir con las pautas. También hice async como el delegado anónimo, solo por razones de legibilidad. La sobrecarga de crear una máquina de estados es minúscula y queda eclipsada por el valor de comunicar claramente que esta Task.Run
devuelve un estilo de promesa Task
, no un delegado de la vieja escuela Task
destinado al procesamiento en segundo plano de cargas de trabajo vinculadas a la CPU.
La ventaja del primer enfoque es que garantiza que la interfaz de usuario seguirá respondiendo. El segundo enfoque no ofrece tal garantía. Siempre que utilice las API asíncronas integradas de la plataforma .NET, la probabilidad de que la interfaz de usuario sea bloqueada por el segundo enfoque es bastante pequeña. Después de todo, estas API son implementadas por expertos¹. En el momento en que empiezas a esperar tu propio métodos asíncronos, todas las garantías están desactivadas. A menos que, por supuesto, su nombre de pila sea Stephen y su apellido sea Toub o Cleary. Si ese no es el caso, es muy posible que tarde o temprano escriba un código como este:
public static async Task LongProcessAsync()
TeenyWeenyInitialization(); // Synchronous
await SomeBuildInAsyncMethod().ConfigureAwait(false); // Asynchronous
CalculateAndSave(); // Synchronous
El problema obviamente es con el método TeenyWeenyInitialization()
. Este método es sincrónico y viene antes del primer await
dentro del cuerpo del método asincrónico, por lo que no se esperará. Se ejecutará sincrónicamente cada vez que llame al LongProcessAsync()
. Entonces, si sigue el segundo enfoque (sin Task.Run
), los TeenyWeenyInitialization()
se ejecutará en el hilo de la interfaz de usuario.
¿Qué tan malo puede ser esto? ¡La inicialización es pequeñita después de todo! Solo un viaje rápido a la base de datos para obtener un valor, leer la primera línea de un pequeño archivo de texto, obtener un valor del registro. Todo termina en un par de milisegundos. En el momento en que escribiste el programa. En tu PC. Antes de mover la carpeta de datos a una unidad compartida. Antes de que la cantidad de datos en la base de datos se volviera enorme.
Pero puede tener suerte y el TeenyWeenyInitialization()
permanece rápido para siempre, ¿qué pasa con el segundo método síncrono, el CalculateAndSave()
? Éste viene después de un await
que está configurado para no capturar el contexto, por lo que se ejecuta en un subproceso de grupo de subprocesos. Nunca debería ejecutarse en el hilo de la interfaz de usuario, ¿verdad? Incorrecto. Depende de la Task
devuelto por SomeBuildInAsyncMethod()
. Si el Task
se completa, no se producirá un cambio de hilo y el CalculateAndSave()
se ejecutará en el mismo hilo que llamó al método. Si sigue el segundo enfoque, este será el hilo de la interfaz de usuario. Es posible que nunca experimente un caso en el que SomeBuildInAsyncMethod()
devolvió un completo Task
en su entorno de desarrollo, pero el entorno de producción puede ser diferente en formas difíciles de predecir.
Tener una aplicación que funciona mal es desagradable. Tener una aplicación que funciona mal y congela la interfaz de usuario es aún peor. ¿De verdad quieres arriesgarte? Si no lo hace, utilice siempre Task.Run(async
dentro de sus controladores de eventos. ¡Especialmente cuando espera métodos que usted mismo ha codificado!
¹ Descargo de responsabilidad, algunas API asíncronas integradas no se implementan correctamente.
Importante: los Task.Run
ejecuta el delegado asincrónico proporcionado en un ThreadPool
hilo, por lo que se requiere que el LongProcessAsync
no tiene afinidad con el subproceso de la interfaz de usuario. Si implica la interacción con los controles de la IU, entonces el Task.Run
no es una opinión. Gracias a @Zmaster por señalar esta importante sutileza en los comentarios.
Valoraciones y comentarios
Si sostienes algún obstáculo o disposición de aumentar nuestro enunciado te sugerimos añadir una anotación y con gusto lo analizaremos.