Pregunta

Digamos que tengo una clase que implementa el Desechable interfaz.Algo como esto:

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

Mi clase utiliza algunos recursos no administrados, de ahí la Disponer() método de Desechable libera esos recursos. Mi clase debe usarse así:

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

Ahora quiero implementar un método que llame Hacer algo() asincrónicamente.Agrego un nuevo método a Mi clase:

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

Ahora, desde el lado del cliente, Mi clase debe usarse así:

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

Sin embargo, si no hago nada más, esto podría fallar ya que el objeto mi clase podría ser eliminado antes Hacer algo() se llama (y arroja un mensaje inesperado) ObjetoDispuestoExcepción).Entonces, el llamado a la Disponer() El método (ya sea implícito o explícito) debe retrasarse hasta que se realice la llamada asincrónica a Hacer algo() está hecho.

Creo que el código en el Disponer() El método debe ser ejecutado. de forma asincrónica, y solo una vez que se resuelvan todas las llamadas asincrónicas.Me gustaría saber cuál podría ser la mejor manera de lograr esto.

Gracias.

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


ACTUALIZAR: Muchas gracias por sus respuestas.Le agradezco su esfuerzo.Como chakrit ha comentado, Necesito eso Se pueden realizar múltiples llamadas al asíncrono DoSomething..Idealmente, algo como esto debería funcionar bien:

using ( MyClass myClass = new MyClass() ) {

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

}

Estudiaré el semáforo de conteo, parece lo que busco.También podría ser un problema de diseño.Si lo encuentro conveniente les compartiré algunos fragmentos del caso real y lo que Mi clase realmente lo hace.

¿Fue útil?

Solución 6

Entonces, mi idea es mantener cuántos AsyncDoSomething () están pendientes de completar, y solo eliminarlos cuando este recuento llegue a cero. Mi enfoque inicial es:

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 );
    }
}

Pueden ocurrir algunos problemas si dos o más hilos intentan leer / escribir la variable pendientesTasks al mismo tiempo, por lo que la palabra clave bloqueo debe usarse para evitar 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 recursos se realiza de forma asincrónica, algo como esto podría funcionar:

MyClass myClass;

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

myClass.DoSomething();

Cuando el comportamiento esperado debe ser iniciar una ObjectDisposedException cuando se llama a DoSomething () fuera de la cláusula using . Pero no encuentro esto lo suficientemente malo como para repensar esta solución.

Otros consejos

Parece que estás usando el patrón asíncrono basado en eventos (consulte aquí para obtener más información sobre los patrones asíncronos de .NET) entonces, lo que normalmente tendría es un evento en la clase que se activa cuando se completa la operación asíncrona llamada DoSomethingCompleted (tenga en cuenta que AsyncDoSomething realmente debería llamarse DoSomethingAsync seguir el patrón correctamente).Con este evento expuesto se podría escribir:

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

La otra alternativa es utilizar el IAsyncResult patrón, donde puede pasar un delegado que llama al método de disposición al AsyncCallback parámetro (más información sobre este patrón también está en la página de arriba).En este caso tendrías 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 sea cual sea la forma en que lo haga, necesita una manera de notificar a la persona que llama que la operación asíncrona se ha completado para que pueda deshacerse del objeto en el momento correcto.

Los métodos asíncronos generalmente tienen una devolución de llamada que le permite realizar alguna acción al finalizar. Si este es su 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 un contenedor asíncrono:

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

No alteraría el código de alguna manera para permitir la eliminación asincrónica. En cambio, me aseguraría de que cuando se realice la llamada a AsyncDoSomething, tenga una copia de todos los datos que necesita para ejecutarse. Ese método debería ser responsable de limpiar todos sus recursos.

Puede agregar un mecanismo de devolución de llamada y pasar una función de limpieza como 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 múltiples llamadas asíncronas desde la misma instancia de objeto.

Una forma sería implementar un simple contando el semáforo con el Clase de semáforo para contar el número de trabajos asíncronos en ejecución.

Agregue el contador a MyClass y en todas las llamadas AsyncWhaever incrementen el contador, en las salidas lo engañan. Cuando el semáforo es 0, 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 esa sea la forma ideal. Huelo un problema de diseño. ¿Quizás un replanteamiento de los diseños de MyClass podría evitar esto?

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

Considero desafortunado que Microsoft no haya requerido como parte del contrato IDisposable que las implementaciones deberían permitir que se llame a Dispose desde cualquier contexto de subprocesos, ya que no hay una forma sensata de que la creación de un objeto pueda forzar la continuación existencia del contexto de subprocesamiento en el que se creó. Es posible diseñar un código para que el hilo que crea un objeto de alguna manera observe si el objeto se vuelve obsoleto y pueda Control.BeginInvoke a su conveniencia, y de modo que cuando el hilo ya no sea necesario para cualquier otra cosa, se quede hasta que sea apropiado los objetos han sido List<> d, pero no creo que haya un mecanismo estándar que no requiera un comportamiento especial por parte del hilo que crea el <=>.

Su mejor opción es probablemente tener todos los objetos de interés creados dentro de un hilo común (tal vez el hilo de la interfaz de usuario), intente garantizar que el hilo permanezca durante toda la vida útil de los objetos de interés y use algo como < => para solicitar la disposición de los objetos. Siempre que ni la creación ni la limpieza de objetos se bloqueen por un período de tiempo prolongado, puede ser un buen enfoque, pero si alguna operación puede bloquear un enfoque diferente puede ser necesario [quizás abra una forma ficticia oculta con su propio hilo, para que uno pueda use <=> allí].

Alternativamente, si tiene control sobre las implementaciones <=>, diséñelas de modo que puedan dispararse de forma segura de forma asincrónica. En muchos casos, eso & "; Solo funcionará &"; siempre y cuando nadie esté tratando de usar el artículo cuando lo deseche, pero eso no es un hecho. En particular, con muchos tipos de <=>, existe un peligro real de que varias instancias de objetos puedan manipular un recurso externo común [p. un objeto puede contener un <=> de instancias creadas, agregar instancias a esa lista cuando se construyen y eliminar instancias en <=>; si las operaciones de la lista no están sincronizadas, un <=> asincrónico podría corromper la lista incluso si el objeto que se está desechando no está en uso.

Por cierto, un patrón útil es que los objetos permitan la eliminación asincrónica mientras están en uso, con la expectativa de que dicha eliminación provocará que cualquier operación en curso arroje una excepción en la primera oportunidad conveniente. Cosas como los enchufes funcionan de esta manera. Puede que no sea posible que una operación de lectura salga antes de tiempo sin dejar su socket en un estado inútil, pero si el socket nunca se va a usar de todos modos, no tiene sentido que la lectura siga esperando datos si otro hilo ha determinado que Debería rendirse. En mi humilde opinión, así es como todos los objetos <=> deberían esforzarse por comportarse, pero no conozco ningún documento que requiera un patrón tan general.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top