ejecutar-codigo-bloqueante-de-terceros-con-timeout-en-c

Ejecutar código bloqueante de terceros con Timeout en C#

A través de la comunidad de Discord he recibido la pregunta de cómo ejecutar en C# código de terceros susceptible de bloquearse de forma segura empleando un Timeout para cancelar la tarea.

La concurrencia siempre es un tema complicado, en prácticamente todos los lenguajes. C# No solo no es una excepción sino que incluso es un buen ejemplo. El motivo es que a lo largo de años al incorporado distintas funcionalidades pero, lógicamente, por compatibilidad no se han retirado los anteriores.

Está hecho que .NET disponga de una gran cantidad de herramientas para gestionar las concurrencia pero, a su vez, que una gran cantidad de tutoriales códigos y ejemplos hayan quedado obsoletos por no emplear las últimas herramientas y estén desactualizados.

Como decíamos, un ejemplo habitual es tener que tratar con una tarea que es susceptible de quedar bloqueada. Para ello normalmente querremos emplear un timeout, es decir, un tiempo transcurrido el cual, cancelamos la tarea siesta no ha finalizado.

Para ello disponemos de distintos mecanismos. El más habitual será emplear un CancellationToken. Pero, esto requiere que el código de la tarea ejecutada esté diseñado para emplear este mecanismo. Sin embargo en el caso que nos toque trabajar con códigos heredados o librerías de terceros, es posible que no tengamos disposición de emplear un CancellationToken directamente.

Así que vamos a ver una de las posibles técnicas para abordar esta situación, en la que tenemos que tratar con un código susceptible de quedar bloqueado, que no podemos modificar, y que queremos tratar mediante un Timeout.

Vamos a probar con un ejemplo. En primer lugar, vamos a crear nuestro “código bloqueante”. Para ello, creamos una nueva librería, y en su interior simplemente ponemos lo siguiente,

namespace ClassLibrary1
{
    public static class Testme
    {
        public static void DoMagic()
        {
            while (true)
            {
                Thread.Sleep(1000);
            }
        }
    }
}

Es decir, únicamente hemos creado un método llamado ‘DoMagic’, que como vemos usa un while(true) y una espera para bloquear permanentemente la ejecución. Nos hemos puesto en el peor de los caos, que es que el código de terceros (en nuestro caso, simulado) no emplea async, ni Task, ni ningún mecanismo asíncrono. Es simplemente un método bloqueante, de toda la vida.

Para probarlo, creamos una nueva aplicación de consola, y reemplazamos el código por lo siguiente,

  
Console.WriteLine("Starting");

ClassLibrary1.Testme.DoMagic();

Console.WriteLine("Finally");

Ejecutamos el código y comprobamos que, como era de esperar, se muestra ‘Starting’ pero nunca llega a mostrarse ‘Finally’ porque el código queda bloqueado en nuestra librería (/(desastrosa) de terceros simulada.

Para resolver esto de forma cómoda, nos creamos una clase con métodos de extensión para Tasks,

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

namespace ConsoleApp1
{
    public static class TaskExtensions
    {
        public static async Task RunWithTimeout(this Task task, TimeSpan timeout)
        {

            using (var timeoutCancellationTokenSource = new CancellationTokenSource())
            {

                var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
                if (completedTask == task)
                {
                    timeoutCancellationTokenSource.Cancel();
                    return;
                }
                else
                {
                    throw new TimeoutException("The operation has timed out.");
                }
            }
        }

        public static async Task<TResult> RunWithTimeout<TResult>(this Task<TResult> task, TimeSpan timeout)
        {

            using (var timeoutCancellationTokenSource = new CancellationTokenSource())
            {

                var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
                if (completedTask == task)
                {
                    timeoutCancellationTokenSource.Cancel();
                    return await task;
                }
                else
                {
                    throw new TimeoutException("The operation has timed out.");
                }
            }
        }
    }
}

Con esto, podríamos ejecutar nuestra tarea con un TimeOut haciendo

using ConsoleApp1;

Console.WriteLine("Starting");

var task = Task.Factory.StartNew(() => { ClassLibrary1.Testme.DoMagic(); });

try
{
    await task.RunWithTimeout(TimeSpan.FromSeconds(3));
}
catch (TimeoutException ex)
{
    Console.WriteLine("Timeout");
}

Console.WriteLine("Finally");

Si ejecutamos el código veremos que se muestra “Starting” y, a los 3 segundos, “Timeout” y “Finally”. Es decir, que nuestro Timeout está funcionando correctamente