Aquí hay un dilema interesante para la bibliotecaria. 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 a 'Publicar' en un nuevo hilo. Quiero ser capaz de detectar cuándo sale el hilo del cliente para poder limpiar los recursos (canales).¿Cómo puedo detectar cuándo sale un subproceso de cliente?
La única forma de hacer esto que he ideado es crear un nuevo hilo 'observador' que simplemente bloquea en una llamada Join al hilo del cliente. Aquí una demostración simple:
Primero mi 'biblioteca'. Se agarra el hilo del cliente y luego crea un nuevo hilo cual se detiene en ‘Join’:
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();
}
}
Aquí es un cliente que utiliza mi biblioteca. Se crea un nuevo hilo y luego llama al método StartSomething de mi biblioteca:
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();
}
}
Cuando ejecuta el cliente de la siguiente manera:
var client = new Client(new Library());
client.DoWorkInAThread();
// give the client thread time to complete
Thread.Sleep(100);
consigo esta salida:
Library says: StartSomething called
Client thread says: I'm done
Libaray says: Client thread existed
Así funciona , pero es feo Realmente no me gusta la idea de que todos estos hilos de vigilantes bloqueados estén rondando. ¿Hay una mejor manera de hacer esto?
Primera alternativa.
Proporcione un método que devuelva un trabajador que implementa IDisposable y deje en claro en la documentación que no debe compartir trabajadores entre subprocesos. Aquí está la biblioteca modificada:
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");
}
}
El cliente es ahora un poco más complicado:
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();
}
}
El problema principal de este cambio es que se trata de un cambio importante para la API. Los clientes existentes deberán ser reescritos. Ahora que eso no es tan malo, significaría volver a visitarlos y asegurarse de que estén limpiando correctamente.
Segunda alternativa sin interrupción. La API proporciona una forma para que el cliente declare 'alcance de trabajo'. Una vez que se completa el alcance, la biblioteca puede limpiar. La biblioteca ofrece un alcance de trabajo que implementa IDisposable, pero a diferencia de la primera alternativa anterior, el método StartSomething permanece en la biblioteca de clases:
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");
}
}
El cliente simplemente coloca la llamada StartSomething en un alcance de trabajo ...
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();
}
}
Me gusta menos que la primera alternativa porque no obliga al usuario de la biblioteca a pensar en el alcance.
'Asigno hilos de recursos locales' - no es un buen comienzo para esto :( –
Desafortunadamente es una restricción de la biblioteca AMQP de bajo nivel, no tiene permitido compartir canales entre hilos. Supongo que un diseño de API alternativo sería para la biblioteca proporcionar un 'editor' no seguro para subprocesos que el cliente debe crear. –
No estoy seguro de que correlacione correctamente el caso de uso y el código de muestra. ¿Se requeriría que el código del cliente de EasyNetQ inicie un nuevo ¿Enlazar para usar la biblioteca? ¿Puedes poner un código de cliente más real? Si no, entonces algo en las líneas de: var ctx = library.StartExecutionContext(); ... ctx.Complete(); luego dentro de Library ctx.Complete se limpiará, tal vez mediante el uso de un ManualResetEvent para señalizar el hilo para completar –