Llamar a métodos asíncronos desde un servicio de Windows

Tengo un servicio de Windows escrito en C # que dispara periódicamente trabajos en segundo plano. Normalmente, en un momento dado, varias docenas de Tareas fuertemente enlazadas de E / S (descarga de archivos grandes, etc.) se ejecutan en paralelo. El servicio se ejecuta en un servidor web relativamente ocupado (necesario por ahora), y creo que podría beneficiarse enormemente en términos de conservación de subprocesos el uso de API asíncronas tanto como sea posible.

La mayor parte de este trabajo está hecho. Todos los trabajos ahora son totalmente asíncronos (aprovechando HttpClient, etc.), al igual que el bucle principal de trabajo (con dosis elevadas de Task.Delay). Todo lo que queda es descubrir cómo iniciar de forma correcta y segura el bucle principal desde el inicio del servicio. Esencialmente, es el dilema de las llamadas asincrónicas desde la sincronización. A continuación es lo que tengo hasta ahora (muy simplificado).

en Program.cs:

static void Main(string[] args) { TaskScheduler.UnobservedTaskException += (sender, e) => { // log & alert! e.SetObserved(); }; ServiceBase.Run(new MyService()); } 

en MyService.cs:

 protected override void OnStart(string[] args) { _scheduler.StartLoopAsync(); // fire and forget! will this get me into trouble? } 

Es ese llamado a StartLoopAsync que me preocupa. No puedo simplemente Wait() en la tarea devuelta porque OnStart necesita regresar con relativa rapidez. (Los bucles de trabajo deben ejecutarse en un hilo separado). Un par de pensamientos vienen a la mente:

  • ¿Estoy bien cubierto en cuanto a excepciones no observadas al colocar ese controlador en Main?
  • ¿Habría algún beneficio al utilizar Task.Run, algo como Task.Run(() => _scheduler.StartLoopAsync().Wait()); ?
  • ¿Habría algún beneficio al llamar a _scheduler.StartLoopAsync().ConfigureAwait(false) aquí? (Lo estoy dudando ya que no hay await aquí).
  • ¿Habría algún beneficio al usar AsyncContextThread de Stephen Cleary en esta situación? No he visto ningún ejemplo de cómo usar esto, y como estoy iniciando un bucle infinito, no sé si la sincronización de la copia de seguridad en algún contexto es incluso relevante aquí.

UnobservedTaskException se llamará para todas las excepciones de Task no observadas, por lo que es un buen lugar para el registro de este tipo. Sin embargo, no es genial porque, dependiendo de la lógica de su progtwig, puede ver mensajes espurios; por ejemplo, si realiza Task.WhenAny y luego ignora la tarea más lenta, entonces cualquier excepción de esa tarea más lenta debe ignorarse, pero se envían a la UnobservedTaskException . Como alternativa, considere colocar un ContinueWith en su tarea de nivel superior (la que se devolvió de StartLoopAsync ).

Su llamada a StartLoopAsync parece bien, asumiendo que es correctamente asíncrono. Puede usar TaskRun (p. Ej., Task.Run(() => _scheduler.StartLoopAsync()) – no es necesario Wait )), pero el único beneficio sería si el propio StartLoopAsync pudiera generar una excepción (en lugar de fallar en su tarea devuelta) O si demoraba mucho antes de la primera await .

ConfigureAwait(false) solo es útil cuando se hace una await , como supuso.

My AsyncContextThread está diseñado para este tipo de situación, pero también fue diseñado para ser muy simple. 🙂 AsyncContextThread proporciona un subproceso independiente con un bucle principal similar a su progtwigdor, completo con TaskScheduler , TaskFactory y SynchronizationContext . Sin embargo, es simple: solo usa un solo hilo, y toda la progtwigción / contexto apunta al mismo hilo. Me gusta eso porque simplifica enormemente las preocupaciones sobre la seguridad de los subprocesos y también permite operaciones asíncronas simultáneas, pero no está haciendo un uso completo del conjunto de subprocesos, por lo que, por ejemplo, el trabajo vinculado a la CPU bloquearía el bucle principal (similar a un escenario de subprocesos de la interfaz de usuario).

En su situación, parece que AsyncContextThread puede permitirle eliminar / simplificar parte del código que ya ha escrito. Pero, por otro lado, no es multiproceso como lo es su solución.

No es una respuesta en sí misma, pero un año después de publicar esta pregunta, estamos transfiriendo este servicio a un Servicio de Azure Cloud. He encontrado que la plantilla Worker Role de Azure SDK es un muy buen ejemplo de cómo llamar correctamente a async desde la sincronización, proporcionar soporte de cancelación, lidiar con excepciones, etc. no proporciona un equivalente al método Run (debe comenzar su trabajo en OnStart y regresar inmediatamente), pero para lo que vale, aquí está:

 public class WorkerRole : RoleEntryPoint { private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly ManualResetEvent runCompleteEvent = new ManualResetEvent(false); public override void Run() { Trace.TraceInformation("WorkerRole1 is running"); try { this.RunAsync(this.cancellationTokenSource.Token).Wait(); } finally { this.runCompleteEvent.Set(); } } public override bool OnStart() { // Set the maximum number of concurrent connections ServicePointManager.DefaultConnectionLimit = 12; // For information on handling configuration changes // see the MSDN topic at http://go.microsoft.com/fwlink/?LinkId=166357. bool result = base.OnStart(); Trace.TraceInformation("WorkerRole1 has been started"); return result; } public override void OnStop() { Trace.TraceInformation("WorkerRole1 is stopping"); this.cancellationTokenSource.Cancel(); this.runCompleteEvent.WaitOne(); base.OnStop(); Trace.TraceInformation("WorkerRole1 has stopped"); } private async Task RunAsync(CancellationToken cancellationToken) { // TODO: Replace the following with your own logic. while (!cancellationToken.IsCancellationRequested) { Trace.TraceInformation("Working"); await Task.Delay(1000); } } }