c# - ¿Cómo convierto esto a una tarea asíncrona?
.net .net-4.5 (2)
Hay dos tipos de tareas: las que ejecutan código (por ejemplo, Task.Run
y friends) y las que responden a algún evento externo (por ejemplo, TaskCompletionSource<T>
y sus amigos).
Lo que estás buscando es TaskCompletionSource<T>
. Existen varias formas de "taquigrafía" para situaciones comunes, por lo que no siempre debe usar TaskCompletionSource<T>
directamente. Por ejemplo, Task.FromResult
o TaskFactory.FromAsync
. FromAsync
se FromAsync
más comúnmente si tiene una implementación *Begin
/ *End
de su E / S; de lo contrario, puede usar TaskCompletionSource<T>
directamente.
Para obtener más información, consulte la sección "Tareas vinculadas a E / S" de Implementación del patrón asincrónico basado en tareas .
El constructor de Task
es (desafortunadamente) un remanente del paralelismo basado en tareas, y no debe usarse en el código asincrónico. Solo se puede usar para crear una tarea basada en código, no una tarea de evento externo.
Entonces, dada la restricción de que no puede llamar a ningún método asincrónico existente y debe completar tanto el Thread.Sleep como el Console.WriteLine en una tarea asíncrona, ¿cómo lo hace de una manera que es tan eficiente como el código original?
TaskCompletionSource<T>
un temporizador de algún tipo y TaskCompletionSource<T>
que completara un TaskCompletionSource<T>
cuando el temporizador se dispara. Estoy casi seguro de que eso es lo que la implementación real de Task.Delay
hace de todos modos.
Dado el siguiente código ...
static void DoSomething(int id) {
Thread.Sleep(50);
Console.WriteLine(@"DidSomething({0})", id);
}
Sé que puedo convertir esto en una tarea asíncrona de la siguiente manera ...
static async Task DoSomethingAsync(int id) {
await Task.Delay(50);
Console.WriteLine(@"DidSomethingAsync({0})", id);
}
Y eso al hacerlo si estoy llamando varias veces (Tarea.cuando) todo será más rápido y más eficiente que tal vez usando Parallel.Foreach o incluso llamando desde dentro de un bucle.
Pero por un minuto, imaginemos que Task.Delay () no existe y de hecho tengo que usar Thread.Sleep (); Sé que en realidad este no es el caso, pero este es un código conceptual y donde el Retardo / Sueño normalmente sería una operación IO donde no hay una opción asíncrona (como una EF temprana).
He probado lo siguiente...
static async Task DoSomethingAsync2(int id) {
await Task.Run(() => {
Thread.Sleep(50);
Console.WriteLine(@"DidSomethingAsync({0})", id);
});
}
Pero, aunque funciona sin errores, de acuerdo con Lucien Wischik, de hecho, esta es una mala práctica, ya que es simplemente un hilado de subprocesos del grupo para completar cada tarea (también es más lento con la siguiente aplicación de consola, si se cambia entre DoSomethingAsync y DoSomethingAsync2 llamada puede ver una diferencia significativa en el tiempo que lleva completar) ...
static void Main(string[] args) {
MainAsync(args).Wait();
}
static async Task MainAsync(String[] args) {
List<Task> tasks = new List<Task>();
for (int i = 1; i <= 1000; i++)
tasks.Add(DoSomethingAsync2(i)); // Can replace with any version
await Task.WhenAll(tasks);
}
Luego intenté lo siguiente ...
static async Task DoSomethingAsync3(int id) {
await new Task(() => {
Thread.Sleep(50);
Console.WriteLine(@"DidSomethingAsync({0})", id);
});
}
Al trasplantar esto en lugar del DoSomethingAsync original, ¡la prueba nunca se completa y no se muestra nada en la pantalla!
¡También probé otras muchas variaciones que no se compilan o no se completan!
Entonces, dada la restricción de que no puede llamar a ningún método asincrónico existente y debe completar tanto el Thread.Sleep como el Console.WriteLine en una tarea asíncrona, ¿cómo lo hace de una manera que es tan eficiente como el código original?
El objetivo aquí para aquellos de ustedes que están interesados es darme una mejor comprensión de cómo crear mis propios métodos asíncronos donde no estoy llamando a nadie más. A pesar de muchas búsquedas, esta parece ser la única área donde faltan ejemplos, mientras que hay muchos miles de ejemplos de métodos asincrónicos de llamadas que llaman a otros métodos asíncronos. No puedo encontrar ninguno que convierta un método vacío existente en una tarea asíncrona donde no hay llamadas a otra tarea asíncrona que no sean las que usan el método Task.Run (() => {}).
Entonces, dada la restricción de que no puede llamar a ningún método asincrónico existente y debe completar tanto el Thread.Sleep como el Console.WriteLine en una tarea asíncrona, ¿cómo lo hace de una manera que es tan eficiente como el código original?
OMI, esta es una restricción muy sintética que realmente necesita para quedarse con Thread.Sleep
. Bajo esta restricción, aún puede mejorar ligeramente su código basado en Thread.Sleep
. En lugar de esto:
static async Task DoSomethingAsync2(int id) {
await Task.Run(() => {
Thread.Sleep(50);
Console.WriteLine(@"DidSomethingAsync({0})", id);
});
}
Podrías hacer esto:
static Task DoSomethingAsync2(int id) {
return Task.Run(() => {
Thread.Sleep(50);
Console.WriteLine(@"DidSomethingAsync({0})", id);
});
}
De esta forma, evitaría una sobrecarga de la clase de máquina de estados generada por el compilador. Hay una sutil diferencia entre estos dos fragmentos de código, en cómo se propagan las excepciones .
De todos modos, aquí no es donde está el cuello de botella de la ralentización.
(También es más lento con la siguiente aplicación de consola: si cambia entre DoSomethingAsync y DoSomethingAsync2, puede ver una diferencia significativa en el tiempo que lleva completar)
Veamos una vez más en su código de bucle principal:
static async Task MainAsync(String[] args) {
List<Task> tasks = new List<Task>();
for (int i = 1; i <= 1000; i++)
tasks.Add(DoSomethingAsync2(i)); // Can replace with any version
await Task.WhenAll(tasks);
}
Técnicamente, solicita que 1000 tareas se ejecuten en paralelo, cada una supuestamente para ejecutarse en su propio hilo. En un universo ideal, esperarías ejecutar Thread.Sleep(50)
1000 veces en paralelo y completar todo en aproximadamente Thread.Sleep(50)
ms.
Sin embargo, esta solicitud nunca es satisfecha por el programador de tareas predeterminado de TPL, por una buena razón: el hilo es un recurso valioso y costoso. Además, el número real de operaciones simultáneas está limitado a la cantidad de CPU / núcleos. Entonces, en realidad, con el tamaño predeterminado de ThreadPool
, estoy obteniendo 21 hilos de grupo (en el pico) que sirven esta operación en paralelo. Es por eso que DoSomethingAsync2
/ Thread.Sleep
tarda mucho más tiempo que DoSomethingAsync
/ Task.Delay
. DoSomethingAsync
no bloquea un hilo del grupo, solo solicita uno al finalizar el tiempo de espera. Por lo tanto, más tareas DoSomethingAsync
pueden ejecutarse en paralelo, que DoSomethingAsync2
.
La prueba (una aplicación de consola):
// https://.com/q/21800450/1768303
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Console_21800450
{
public class Program
{
static async Task DoSomethingAsync(int id)
{
await Task.Delay(50);
UpdateMaxThreads();
Console.WriteLine(@"DidSomethingAsync({0})", id);
}
static async Task DoSomethingAsync2(int id)
{
await Task.Run(() =>
{
Thread.Sleep(50);
UpdateMaxThreads();
Console.WriteLine(@"DidSomethingAsync2({0})", id);
});
}
static async Task MainAsync(Func<int, Task> tester)
{
List<Task> tasks = new List<Task>();
for (int i = 1; i <= 1000; i++)
tasks.Add(tester(i)); // Can replace with any version
await Task.WhenAll(tasks);
}
volatile static int s_maxThreads = 0;
static void UpdateMaxThreads()
{
var threads = Process.GetCurrentProcess().Threads.Count;
// not using locks for simplicity
if (s_maxThreads < threads)
s_maxThreads = threads;
}
static void TestAsync(Func<int, Task> tester)
{
s_maxThreads = 0;
var stopwatch = new Stopwatch();
stopwatch.Start();
MainAsync(tester).Wait();
Console.WriteLine(
"time, ms: " + stopwatch.ElapsedMilliseconds +
", threads at peak: " + s_maxThreads);
}
static void Main()
{
Console.WriteLine("Press enter to test with Task.Delay ...");
Console.ReadLine();
TestAsync(DoSomethingAsync);
Console.ReadLine();
Console.WriteLine("Press enter to test with Thread.Sleep ...");
Console.ReadLine();
TestAsync(DoSomethingAsync2);
Console.ReadLine();
}
}
}
Salida:
Press enter to test with Task.Delay ... ... time, ms: 1077, threads at peak: 13 Press enter to test with Thread.Sleep ... ... time, ms: 8684, threads at peak: 21
¿Es posible mejorar la cifra de tiempo para el DoSomethingAsync2
basado en Thread.Sleep
? La única forma en que puedo pensar es usar TaskCreationOptions.LongRunning
con Task.Factory.StartNew
:
Deberías pensar dos veces antes de hacer esto en cualquier aplicación de la vida real :
static async Task DoSomethingAsync2(int id)
{
await Task.Factory.StartNew(() =>
{
Thread.Sleep(50);
UpdateMaxThreads();
Console.WriteLine(@"DidSomethingAsync2({0})", id);
}, TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
}
// ...
static void Main()
{
Console.WriteLine("Press enter to test with Task.Delay ...");
Console.ReadLine();
TestAsync(DoSomethingAsync);
Console.ReadLine();
Console.WriteLine("Press enter to test with Thread.Sleep ...");
Console.ReadLine();
TestAsync(DoSomethingAsync2);
Console.ReadLine();
}
Salida:
Press enter to test with Thread.Sleep ... ... time, ms: 3600, threads at peak: 163
El tiempo mejora, pero el precio de esto es alto. Este código le pide al planificador de tareas que cree un nuevo hilo para cada tarea nueva. No espere que este hilo provenga del grupo:
Task.Factory.StartNew(() =>
{
Thread.Sleep(1000);
Console.WriteLine("Thread pool: " +
Thread.CurrentThread.IsThreadPoolThread); // false!
}, TaskCreationOptions.LongRunning).Wait();