Pregunta

Estoy trabajando con algún código (no el mío, me apresuro a agregar, no confío tanto en esto) para una clase que abre un socket, realiza solicitudes y escucha las respuestas, lo que está generando una excepción en un Una forma que no puedo comprender cuando se prueba en xunit. Supongo que ocurre la misma excepción " en vivo " pero un singleton hace referencia a la clase, por lo que probablemente solo esté oculta.

El problema se manifiesta como " System.CannotUnloadAppDomainException: error al descargar appdomain " en xunit y la excepción interna es " System.ObjectDisposedException " ¡Tirado (esencialmente) dentro del finalizador al cerrar el zócalo! No hay otra referencia al socket que cierre y elimine llamadas está protegido en la clase Socket, por lo que no tengo claro de qué otra forma podría eliminarse el objeto.

Además, si simplemente capto y absorbo la excepción ObjectDisposedException, finaliza cuando toca la línea para cerrar el hilo del oyente.

Simplemente no entiendo cómo se puede desechar el zócalo antes de que se le solicite que cierre.

Mi conocimiento de sockets es solo lo que he aprendido desde que encontré este problema, así que no sé si he provisto todo lo que SOY pueda necesitar. ¡LMK si no!

public class Foo
{
    private Socket sock = null;
    private Thread tListenerThread = null
    private bool bInitialised;
    private Object InitLock = null;
    private Object DeInitLock = null;

    public Foo()
    {
        bInitialised = false;

        InitLock = new Object();
        DeInitLock = new Object();
    }

    public bool initialise()
    {
        if (null == InitLock)
            return false;

        lock (InitLock)
        {
            if (bInitialised)
                return false;

            sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
            sock.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 8);
            sock.Bind( /*localIpEndPoint*/);
            sock.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(mcIP));

            tListenerThread = new Thread(new ThreadStart(listener));
            tListenerThread.Start();

            bInitialised = true;
            return true;
        }
    }

    ~Foo()
    {
        if (bInitialised)
            deInitialise();
    }

    private void deInitialise()
    {
        if (null == DeInitLock)
            return;

        lock (DeInitLock)
        {
            if (bInitialised)
            {
                sock.Shutdown(SocketShutdown.Both); //throws System.ObjectDisposedException
                sock.Close();

                tListenerThread.Abort(); //terminates xunit test!
                tListenerThread = null;

                sock = null;

                bInitialised = false;
            }
        }
    }
}
¿Fue útil?

Solución

Si este objeto es elegible para la recolección de basura y no hay otras referencias al Socket, entonces el finalizador de socket puede ejecutarse antes del finalizador de su objeto. Sospecho que eso es lo que pasó aquí.

En general, es una mala idea (IMO) hacer este trabajo en un finalizador. No recuerdo la última vez que implementé un finalizador: si implementas IDisposable, deberías estar bien a menos que tengas referencias directas a recursos no administrados, que casi siempre tienen la forma de IntPtrs. El cierre ordenado debe ser la norma: un finalizador solo debe ejecutarse si el programa se está cerrando o si alguien ha olvidado desechar la instancia para comenzar.

(Sé que aclaró al principio que este no es su código, solo pensé en explicarle por qué es problemático. Disculpas si ya sabía algo de todo esto)

Otros consejos

Debido a la forma en que funcionan los recolectores de basura y los finalizadores, los finalizadores solo se deben usar si su clase es el propietario directo de un recurso no administrado, como un identificador de ventana, un objeto GDI, un identificador global o cualquier otro tipo de IntPtr.

Un finalizador no debe intentar deshacerse de un recurso administrado, ni tampoco utilizarlo. De lo contrario, se arriesgará a llamar a un objeto finalizado o eliminado.

Le recomiendo que lea este artículo muy importante de Microsoft para más detalles sobre cómo funciona la recolección de basura. Además, esta es la referencia de MSDN en Implementación de Finalize and Disose to Clean Up Unmanaged Resources , busque atentamente las recomendaciones en la parte inferior.

En pocas palabras:

  • Si su objeto tiene un recurso no administrado, debe implementar IDisposable y debe implementar Finalizer.
  • Si su objeto contiene un objeto IDiposable, también debe implementar IDisposable por sí mismo y eliminar ese objeto explícitamente.
  • Si su objeto contiene tanto un no administrado como un desechable, el finalizador debe invocar dos versiones diferentes de Dispose, una que se libera de forma desechable y no administrada, la otra solo no administrada. Esto generalmente se hace usando una función Dispose (bool) llamada por Dipose () y Finalizer ().
  • El Finalizer nunca debe usar ningún otro recurso que no sea el recurso no administrado que se está liberando y auto. De no hacerlo, se corre el riesgo de hacer referencia a los objetos recolectados o eliminados, ya que un objeto se resurge temporalmente antes de su finalización.

Nueva información: parece que tengo dos problemas en realidad, pero el hilo uno parece ser bastante tóxico.

Desde el enlace de MSDN anterior:

  

" ThreadAbortException es un especial   excepción que se puede atrapar, pero   será automáticamente levantado de nuevo en   el final del bloque catch. "

También hay un contenido de comunidad muy interesante en ese enlace que incluye " Thread.Abort es un signo de un programa mal diseñado ".

Así que al menos tengo algunas municiones para cambiar esto ahora :)

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