2008-12-30 21 views
18

Digamos que tengo una clase que implementa la interfaz IDisposable. Algo como esto:¿Cómo deshacerse de forma asincrónica?

http://www.flickr.com/photos/garthof/3149605015/

MiClase utiliza algunos recursos no administrados, de ahí la dispose() método de IDisposable comunicados de esos recursos. MiClase deben utilizarse como esto:

using (MyClass myClass = new MyClass()) { 
    myClass.DoSomething(); 
} 

Ahora, yo quiero poner en práctica un método que llama HacerAlgo() de forma asíncrona. Añado un nuevo método para MiClase:

http://www.flickr.com/photos/garthof/3149605005/

Ahora, desde el lado del cliente, MiClase deben utilizarse como esto:

using (MyClass myClass = new MyClass()) { 
    myClass.AsyncDoSomething(); 
} 

Sin embargo, si no lo hago cualquier otra cosa, esto podría fallar ya que el objeto myClass podría eliminarse antes de DoSomething() se llama (y arrojar un inesperado ObjectDisposedException). Por lo tanto, la llamada al método Dispose() (ya sea implícita o explícita) debe retrasarse hasta que se realice la llamada asincrónica a DoSomething().

creo que el código en el método Dispose() debe ser ejecutado de manera asíncrona, y sólo una vez todas las llamadas asíncronas se resuelven. Me gustaría saber cuál podría ser la mejor manera de lograr esto.

Gracias.

NOTA: En aras de la simplicidad, no he ingresado los detalles de cómo se implementa el método Dispose(). En la vida real suelo seguir el Dispose pattern.


ACTUALIZACIÓN: Muchas gracias por sus respuestas. Le agradezco su esfuerzo. Como chakrithas commented, necesito que se realicen múltiples llamadas a la función asincrónica DoSomething. Lo ideal es que algo como esto debería funcionar bien:

using (MyClass myClass = new MyClass()) { 

    myClass.AsyncDoSomething(); 
    myClass.AsyncDoSomething(); 

} 

Voy a estudiar el semáforo de cuenta, parece que lo que estoy buscando. También podría ser un problema de diseño. Si lo encuentro conveniente, compartiré algunos fragmentos del caso real y lo que realmente hace MyClass.

Respuesta

2

Por lo tanto, mi idea es mantener cuántos AsyncDoSomething() están pendientes de completar, y solo eliminan cuando este conteo llegue a cero. Mi planteamiento inicial es: se pueden producir

public class MyClass : IDisposable { 

    private delegate void AsyncDoSomethingCaller(); 
    private delegate void AsyncDoDisposeCaller(); 

    private int pendingTasks = 0; 

    public DoSomething() { 
     // Do whatever. 
    } 

    public AsyncDoSomething() { 
     pendingTasks++; 
     AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller(); 
     caller.BeginInvoke(new AsyncCallback(EndDoSomethingCallback), caller); 
    } 

    public Dispose() { 
     AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller(); 
     caller.BeginInvoke(new AsyncCallback(EndDoDisposeCallback), caller); 
    } 

    private DoDispose() { 
     WaitForPendingTasks(); 

     // Finally, dispose whatever managed and unmanaged resources. 
    } 

    private void WaitForPendingTasks() { 
     while (true) { 
      // Check if there is a pending task. 
      if (pendingTasks == 0) { 
       return; 
      } 

      // Allow other threads to execute. 
      Thread.Sleep(0); 
     } 
    } 

    private void EndDoSomethingCallback(IAsyncResult ar) { 
     AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState; 
     caller.EndInvoke(ar); 
     pendingTasks--; 
    } 

    private void EndDoDisposeCallback(IAsyncResult ar) { 
     AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState; 
     caller.EndInvoke(ar); 
    } 
} 

Algunos problemas si dos o más hilos tratan de leer/escribir los pendingTasks variables al mismo tiempo, por lo que la palabra clave bloqueo se debe utilizar para evitar las condiciones de carrera:

public class MyClass : IDisposable { 

    private delegate void AsyncDoSomethingCaller(); 
    private delegate void AsyncDoDisposeCaller(); 

    private int pendingTasks = 0; 
    private readonly object lockObj = new object(); 

    public DoSomething() { 
     // Do whatever. 
    } 

    public AsyncDoSomething() { 
     lock (lockObj) { 
      pendingTasks++; 
      AsyncDoSomethingCaller caller = new AsyncDoSomethingCaller(); 
      caller.BeginInvoke(new AsyncCallback(EndDoSomethingCallback), caller); 
     } 
    } 

    public Dispose() { 
     AsyncDoDisposeCaller caller = new AsyncDoDisposeCaller(); 
     caller.BeginInvoke(new AsyncCallback(EndDoDisposeCallback), caller); 
    } 

    private DoDispose() { 
     WaitForPendingTasks(); 

     // Finally, dispose whatever managed and unmanaged resources. 
    } 

    private void WaitForPendingTasks() { 
     while (true) { 
      // Check if there is a pending task. 
      lock (lockObj) { 
       if (pendingTasks == 0) { 
        return; 
       } 
      } 

      // Allow other threads to execute. 
      Thread.Sleep(0); 
     } 
    } 

    private void EndDoSomethingCallback(IAsyncResult ar) { 
     lock (lockObj) { 
      AsyncDoSomethingCaller caller = (AsyncDoSomethingCaller) ar.AsyncState; 
      caller.EndInvoke(ar); 
      pendingTasks--; 
     } 
    } 

    private void EndDoDisposeCallback(IAsyncResult ar) { 
     AsyncDoDisposeCaller caller = (AsyncDoDisposeCaller) ar.AsyncState; 
     caller.EndInvoke(ar); 
    } 
} 

Veo un problema con este enfoque.Como la liberación de los recursos se realiza de forma asíncrona, algo como esto podría funcionar:

MyClass myClass; 

using (myClass = new MyClass()) { 
    myClass.AsyncDoSomething(); 
} 

myClass.DoSomething(); 

Cuando el comportamiento esperado debería ser el lanzamiento de un ObjectDisposedException cuando HacerAlgo() se llama fuera del usando cláusula. Pero no creo que esto sea tan malo como para replantear esta solución.

+1

En el uso (MyClass myclass = new ....) ejemplo, creo que el objeto myclass queda sin anclaje al final del bloque de uso y está disponible para ser Finalizado. Estoy bastante seguro de que es seguro que necesita llamar y bloquear en un método de espera antes del final del bloque de uso. –

5

Los métodos asíncronos generalmente tienen una devolución de llamada que le permite hacer alguna acción al completarse.Si este es tu caso, sería algo como esto:

// The async method taks an on-completed callback delegate 
myClass.AsyncDoSomething(delegate { myClass.Dispose(); }); 

Otra forma de evitar esto es una envoltura asíncrono:

ThreadPool.QueueUserWorkItem(delegate 
{ 
    using(myClass) 
    { 
     // The class doesn't know about async operations, a helper method does that 
     myClass.DoSomething(); 
    } 
}); 
2

yo no alterar el código de alguna manera para permitir dispone asincrónicos. En cambio, me aseguraría de que cuando se realice la llamada a AsyncDoSomething, tendrá una copia de todos los datos que necesita para ejecutar. Ese método debería ser responsable de limpiar todo si sus recursos.

10

Parece que está usando el patrón asincrónico basado en eventos (see here for more info about .NET async patterns) así que lo que normalmente tendría que es un evento de la clase que se activa cuando se completa la operación asincrónica llamado DoSomethingCompleted (tenga en cuenta que debe ser realmente AsyncDoSomething llamado DoSomethingAsync para seguir el patrón correctamente). Con este evento expuesto podría escribir:

var myClass = new MyClass(); 
myClass.DoSomethingCompleted += (sender, e) => myClass.Dispose(); 
myClass.DoSomethingAsync(); 

La otra alternativa es usar el patrón IAsyncResult, donde se puede pasar un delegado que llama al método dispose a la AsyncCallback parámetro (más información sobre este patrón se encuentra en la página arriba también). En este caso usted tendría BeginDoSomething y EndDoSomething métodos en lugar de DoSomethingAsync, y lo llamaría algo así como ...

var myClass = new MyClass(); 
myClass.BeginDoSomething(
    asyncResult => { 
         using (myClass) 
         { 
          myClass.EndDoSomething(asyncResult); 
         } 
        }, 
    null);   

Pero de cualquier manera que lo hace, usted necesita una manera para que la persona que llama para ser notificado de que la la operación asincrónica se ha completado para que pueda deshacerse del objeto en el momento correcto.

+0

Una de las principales preocupaciones que tengo al deshacerme de un objeto en una devolución de llamada asíncrona es que no conozco ninguna forma generalmente practicable de saber cuándo es seguro hacerlo. Incluso si nadie más está utilizando el objeto sobre el que se llama a 'Dispose', alguien más puede estar utilizando un objeto que afectaría el método' Dispose'. – supercat

1

Puede agregar un mecanismo de devolución de llamada y pasar una función de limpieza como una devolución de llamada.

var x = new MyClass(); 

Action cleanup =() => x.Dispose(); 

x.DoSomethingAsync(/*and then*/cleanup); 

pero esto plantearía un problema si desea ejecutar varias llamadas asincrónicas fuera de la misma instancia de objeto.

Una forma sería implementar un simple counting semaphore con el Semaphore class para contar el número de trabajos asincrónicos en ejecución.

Agregue el contador a MyClass y en cada AsyncWhatever las llamadas incrementan el contador, al salir lo degradan. Cuando el semáforo es 0, entonces la clase está lista para ser eliminada.

var x = new MyClass(); 

x.DoSomethingAsync(); 
x.DoSomethingAsync2(); 

while (x.RunningJobsCount > 0) 
    Thread.CurrentThread.Sleep(500); 

x.Dispose(); 

Pero dudo que sea la manera ideal. Huelo un problema de diseño. Tal vez una nueva idea de los diseños de MyClass podría evitar esto?

¿Podría compartir algo de la implementación de MyClass? ¿Qué se supone que debe hacer?

+0

Dormir en un bucle se considera una mala práctica. Deberías usar un EventWaitHandle para disparar un evento al finalizar. –

+0

Desafortunadamente, estoy limitado a .NET CF 1.0, y la clase Semáforo solo es compatible desde .NET 2.0. – Auron

+0

Es solo un "ejemplo" ... cuidado. – chakrit

1

considero que es desafortunado que Microsoft no requiere como parte del contrato IDisposable que las implementaciones deben permitir Dispose a llamarse desde cualquier contexto rosca, ya que no hay manera sensata la creación de un objeto puede obligar a la existencia continuada de la contexto de enhebrado en el que fue creado. Es posible diseñar código para que el hilo que crea un objeto de alguna manera observe si el objeto se vuelve obsoleto y puede Dispose según su conveniencia, y para que cuando el hilo ya no se necesite para nada se quede hasta que todos los objetos apropiados tengan sido Dispose d, pero no creo que haya un mecanismo estándar que no requiera un comportamiento especial por parte del hilo que crea el Dispose.

Su mejor opción es tener todos los objetos de interés creados dentro de un hilo común (tal vez el hilo de UI), intentar garantizar que el hilo permanezca durante la vida de los objetos de interés, y usar algo como Control.BeginInvoke para solicitar la eliminación de los objetos. Suponiendo que ni la creación de objeto ni la limpieza se bloquearán durante un período de tiempo, puede ser una buena opción, pero si cualquiera de las dos operaciones pudiera bloquear un enfoque diferente puede ser necesario [tal vez abrir una forma ficticia oculta con su propio hilo, para que uno pueda use Control.BeginInvoke allí].

Alternativamente, si tiene control sobre las implementaciones IDisposable, diseñelas para que puedan dispararse de manera asincrónica. En muchos casos, eso "solo funcionará", siempre que nadie intente usar el artículo cuando se desecha, pero eso es casi imposible. En particular, con muchos tipos de IDisposable, existe un peligro real de que varias instancias de objetos puedan manipular un recurso externo común [p. un objeto puede contener un List<> de instancias creadas, agregar instancias a esa lista cuando se construyan y eliminar instancias en Dispose; si las operaciones de la lista no están sincronizadas, un Dispose asíncrono podría dañar la lista aunque el objeto que se está eliminando no esté en uso.

BTW, un patrón útil es para que los objetos permitan la eliminación asincrónica mientras están en uso, con la expectativa de que dicha eliminación hará que cualquier operación en curso arroje una excepción en la primera oportunidad conveniente. Cosas como enchufes funcionan de esta manera. Puede que no sea posible que una operación de lectura salga temprano sin dejar su socket en un estado inútil, pero si el socket nunca va a usarse de todos modos, no tiene sentido que la lectura siga esperando datos si otro hilo ha determinado que debería darse por vencido. En mi humilde opinión, así es como todos los objetos IDisposable deben esforzarse en comportarse, pero no conozco ningún documento que llame para un patrón general.

Cuestiones relacionadas