2012-05-03 19 views
14

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.

+2

'Asigno hilos de recursos locales' - no es un buen comienzo para esto :( –

+2

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. –

+0

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 –

Respuesta

1

Si un hilo del cliente realiza llamadas a su biblioteca que internamente asignan algunos recursos, el cliente debe 'abrir' su biblioteca y obtener un token para todas las demás operaciones. Este token podría ser un índice int en un vector interno de la biblioteca o un puntero de vacío en un objeto/estructura interno.Insista en que los clientes deben cerrar el token antes de terminar.

Así es como el 99% de todas esas llamadas de lib funcionan donde se debe preservar el estado en las llamadas de los clientes, por ejemplo. manijas de socket, manejadores de archivos.

0

Su solución .Join se ve muy elegante para mí. Los hilos de vigilante bloqueados no son algo tan terrible.

+1

¡Oh, sí lo son! –

+0

Bueno, todas las cosas con moderación.: p Realmente no hay otra manera que yo sepa de estar seguro de que el hilo está completo. – IngisKahn

+0

¿Qué pasa si el hilo ha completado sus operaciones de biblioteca para ese canal, quiere cerrarlo y, quizás más tarde, abrirlo de nuevo, u otro? Los hilos de llamada nunca terminan mientras dure la aplicación, pero pueden abrir y cerrar miles de veces los recursos de la biblioteca. Ninguna de estas soluciones de polling-client-thread-state es útil para una biblioteca. –

3

Dado que no está controlando directamente la creación del hilo, es difícil para usted saber cuándo el hilo ha terminado de hacer su trabajo. Un enfoque alternativo sería tal vez para forzar al cliente para que le notifique cuando estén listas:

public interface IThreadCompletedNotifier 
{ 
    event Action ThreadCompleted; 
} 

public class Library 
{ 
    public void StartSomething(IThreadCompletedNotifier notifier) 
    { 
     Console.WriteLine("Library says: StartSomething called"); 
     notifier.ThreadCompleted +=() => Console.WriteLine("Libaray says: Client thread existed"); 
     var clientThread = Thread.CurrentThread; 
     exitMonitorThread.Start(); 
    } 
} 

De esta manera, cualquier cliente que llama es forzado a pasar en algún tipo de mecanismo de notificación que le indicará cuando su hecho haciendo lo suyo:

public class Client : IThreadCompletedNotifier 
{ 
    private readonly Library library; 

    public event Action ThreadCompleted; 

    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"); 
      if(ThreadCompleted != null) 
      { 
       ThreadCompleted(); 
      } 
     }); 
     thread.Start(); 
    } 
} 
+0

Esto es mejor, el cliente debe proporcionar una notificación. La notificación debe tener un parámetro que identifique el recurso que se está liberando. Este parámetro debería ser un handle/token para el recurso asignado, es decir. nada que ver con ID de hilo o cualquier cosa similar, de lo contrario la biblioteca se vuelve inútil para hilos agrupados, hilos/fibras "verdes" o hilos que nunca terminan, vuelven y desean usar diferentes recursos en el mismo tiempo o diferentes. –

+0

Esta fue mi primera idea, proporcionar un OperationContext que implemente IDisposable, luego todas las llamadas locales de subprocesos se podrían ejecutar en una instrucción using. Supongo que quería explorar primero las alternativas de 'solo funciona', pero estoy llegando a la idea de una API específica. –

+3

'simplemente funciona' soluciones simplemente no lo haga . Whidows, Linux, cada OS que he usado tiene un 'token', 'handle' u otro objeto similar como la solución para mantener el estado en todas las llamadas. El uso de datos locales y locales de hilo implícitos para la terminación de hilos, etc. absorberá la vida útil de su biblioteca. –

1

Aparte de hacer cualquier cosas de fantasía asíncrona para evitar un hilo por completo, me gustaría tratar de combinar toda la observación en un único hilo que sondea el .ThreadState de propiedad de todas las discusiones que tienen golpea tu biblioteca, digamos, cada 100ms (no estoy seguro de la rapidez con la que debes limpiar los recursos ...)

+1

¿Qué ocurre si los hilos del cliente nunca terminan, sino que abren más canales/recursos de la biblioteca? La solución de sondeo de estado restringe a los usuarios de la biblioteca a los hilos que se crean y terminan continuamente. Los hilos de la piscina deben estar prohibidos, así como cualquier hilo que circule. Simplemente no funcionará. –

+0

@MartinJames ¿por qué los subprocesos de la agrupación tampoco se pudieron supervisar? Cada hilo que alguna vez use la biblioteca podría registrarse para sondeo. Creo que esta solución tiene sentido. – usr

+0

@Martin Entiendo lo que está diciendo, y creo que no hace falta decir que detectar el cierre de hilos no es el único método (ni siquiera el método preferido) de limpieza. Es solo una protección adicional para la biblioteca contra el error del usuario. Tiene razón: si los usuarios no cierran sus propios identificadores en los subprocesos agrupados o en el subproceso de la GUI, se cuelgan para siempre. –

5

Puede crear un monitor estático de hilo que tenga un finalizador. Cuando el hilo está vivo, mantendrá el objeto del monitor. Cuando el thead muera, dejará de contenerlo. Más tarde, cuando la GC entre, finalizará su monitor. En el finalizador puede generar un evento que informará a su marco sobre la muerte (observada) del hilo del cliente.

Un código de ejemplo se puede encontrar en este GIST: https://gist.github.com/2587063

Aquí está una copia de la misma:

public class ThreadMonitor 
{ 
    public static event Action<int> Finalized = delegate { }; 
    private readonly int m_threadId = Thread.CurrentThread.ManagedThreadId; 

    ~ThreadMonitor() 
    { 
     Finalized(ThreadId); 
    } 

    public int ThreadId 
    { 
     get { return m_threadId; } 
    } 
} 

public static class Test 
{ 
    private readonly static ThreadLocal<ThreadMonitor> s_threadMonitor = 
     new ThreadLocal<ThreadMonitor>(() => new ThreadMonitor()); 

    public static void Main() 
    { 
     ThreadMonitor.Finalized += i => Console.WriteLine("thread {0} closed", i); 
     var thread = new Thread(() => 
     { 
      var threadMonitor = s_threadMonitor.Value; 
      Console.WriteLine("start work on thread {0}", threadMonitor.ThreadId); 
      Thread.Sleep(1000); 
      Console.WriteLine("end work on thread {0}", threadMonitor.ThreadId); 
     }); 
     thread.Start(); 
     thread.Join(); 

     // wait for GC to collect and finalize everything 
     GC.GetTotalMemory(forceFullCollection: true); 

     Console.ReadLine(); 
    } 
} 

espero que ayude. Creo que es más elegante que tu hilo adicional de espera.

Cuestiones relacionadas