¿Cómo detecto cuándo sale un hilo de cliente?

Aquí hay un interesante dilema del escritor de bibliotecas. En mi biblioteca (en mi caso, EasyNetQ) estoy asignando recursos locales de subprocesos. Entonces, cuando un cliente crea un nuevo hilo y luego llama a ciertos métodos en mi biblioteca, se crean nuevos recursos. En el caso de EasyNetQ, se crea un nuevo canal para el servidor RabbitMQ cuando el cliente llama "Publicar" en un nuevo hilo. Quiero poder detectar cuándo sale el subproceso del cliente para poder limpiar los recursos (canales).

La única forma de hacer esto es crear un nuevo hilo "observador" que simplemente bloquee una llamada Unida al hilo cliente. Aquí una simple demostración:

Primero mi ‘biblioteca’. Agarra el hilo del cliente y luego crea un nuevo hilo que bloquea en 'Join':

<code>public class Library
{
    public void StartSomething()
    {
        Console.WriteLine("Library says: StartSomething called");

        var clientThread = Thread.CurrentThread;
        var exitMonitorThread = new Thread(() =>
        {
            clientThread.Join();
            Console.WriteLine("Libaray says: Client thread existed");
        });

        exitMonitorThread.Start();
    }
}
</code>

Aquí hay un cliente que usa mi biblioteca. Crea un nuevo hilo y luego llama al método StartSomething de mi biblioteca:

<code>public class Client
{
    private readonly Library library;

    public Client(Library library)
    {
        this.library = library;
    }

    public void DoWorkInAThread()
    {
        var thread = new Thread(() =>
        {
            library.StartSomething();
            Thread.Sleep(10);
            Console.WriteLine("Client thread says: I'm done");
        });
        thread.Start();
    }
}
</code>

Cuando ejecuto el cliente así:

<code>var client = new Client(new Library());

client.DoWorkInAThread();

// give the client thread time to complete
Thread.Sleep(100);
</code>

Obtengo esta salida:

<code>Library says: StartSomething called
Client thread says: I'm done
Libaray says: Client thread existed
</code>

Así funciona, pero es feo. Realmente no me gusta la idea de todos estos hilos de observador bloqueados dando vueltas. ¿Hay una mejor manera de hacer esto?

Primera alternativa.

Proporcione un método que devuelva un trabajador que implemente IDisposable y deje en claro en la documentación que no debe compartir trabajadores entre subprocesos. Aquí está la biblioteca modificada:

<code>public class Library
{
    public LibraryWorker GetLibraryWorker()
    {
        return new LibraryWorker();
    }
}

public class LibraryWorker : IDisposable
{
    public void StartSomething()
    {
        Console.WriteLine("Library says: StartSomething called");
    }

    public void Dispose()
    {
        Console.WriteLine("Library says: I can clean up");
    }
}
</code>

El cliente ahora es un poco más complicado:

<code>public class Client
{
    private readonly Library library;

    public Client(Library library)
    {
        this.library = library;
    }

    public void DoWorkInAThread()
    {
        var thread = new Thread(() =>
        {
            using(var worker = library.GetLibraryWorker())
            {
                worker.StartSomething();
                Console.WriteLine("Client thread says: I'm done");
            }
        });
        thread.Start();
    }
}
</code>

El principal problema con este cambio es que es un cambio importante para la API. Los clientes existentes deberán ser reescritos. Ahora que no es tan malo, significaría volver a visitarlos y asegurarse de que estén limpiando correctamente.

Segunda alternativa inigualable.. La API proporciona una manera para que el cliente declare el "ámbito de trabajo". Una vez que se completa el alcance, la biblioteca puede limpiar. La biblioteca proporciona un WorkScope que implementa IDisposable, pero a diferencia de la primera alternativa anterior, el método StartSomething permanece en la clase Library:

<code>public class Library
{
    public WorkScope GetWorkScope()
    {
        return new WorkScope();
    }

    public void StartSomething()
    {
        Console.WriteLine("Library says: StartSomething called");
    }
}

public class WorkScope : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Library says: I can clean up");
    }
}
</code>

El cliente simplemente pone la llamada StartSomething en un WorkScope ...

<code>public class Client
{
    private readonly Library library;

    public Client(Library library)
    {
        this.library = library;
    }

    public void DoWorkInAThread()
    {
        var thread = new Thread(() =>
        {
            using(library.GetWorkScope())
            {
                library.StartSomething();
                Console.WriteLine("Client thread says: I'm done");
            }
        });
        thread.Start();
    }
}
</code>

Me gusta esto menos que la primera alternativa porque no obliga al usuario de la biblioteca a pensar en el alcance.

Respuestas a la pregunta(5)

Su respuesta a la pregunta