Проблема утилизации розетки / финализации дважды?
Я работаю с некоторым кодом (не моим, я спешу добавить, я вообще не очень доверяю) для класса, который открывает сокет, делает запросы и прослушивает ответы, которые выдают исключение, каким образом я могу не понимаю, когда тестируется в xunit. Я предполагаю, что то же самое исключение происходит "вживую", но на класс ссылается синглтон, поэтому он, вероятно, просто скрыт.
Проблема проявляется как "System.CannotUnloadAppDomainException: ошибка при выгрузке appdomain" в xunit, а внутреннее исключение - "System.ObjectDisposedException", которое выдается (по существу) внутри финализатора при закрытии сокета! Нет другой ссылки на сокет, который вызывает close и dispose защищен в классе Socket, поэтому я не уверен, как еще объект может быть удален.
Кроме того, если я просто ловлю и поглощаю ObjectDisposedException, xunit завершается, когда попадает в строку, чтобы закрыть поток слушателя.
Я просто не понимаю, как сокет может быть утилизирован до того, как его попросят закрыть.
Мои знания о сокетах - это только то, что я узнал с момента обнаружения этой проблемы, поэтому я не знаю, предоставил ли я все, что может понадобиться SO. ЛМК если нет!
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;
}
}
}
}
3 ответа
Если этот объект пригоден для сборки мусора и нет других ссылок на Socket, то финализатор сокета может хорошо работать до финализатора вашего объекта. Я подозреваю, что это то, что здесь произошло.
Это вообще плохая идея (IMO) делать эту большую работу в финализаторе. Я не могу вспомнить, когда я в последний раз реализовывал финализатор вообще - если вы реализуете IDisposable, у вас все будет хорошо, если у вас нет прямых ссылок на неуправляемые ресурсы, которые почти всегда имеют форму IntPtrs. Упорядоченное завершение должно быть нормой - финализатор должен обычно запускаться только в том случае, если программа закрывается или кто-то забыл утилизировать экземпляр для начала.
(Я знаю, что вы пояснили в начале, что это не ваш код - я просто подумал, что объясню, почему это проблематично. Извиняюсь, если вы уже знали некоторые / все это.)
Из-за способа работы сборщика мусора и финализаторов финализаторы должны использоваться только в том случае, если ваш класс является прямым владельцем неуправляемого ресурса, такого как дескриптор окна, объект GDI, глобальный дескриптор или любой другой тип IntPtr.
Финализатор не должен пытаться распоряжаться или даже использовать управляемый ресурс, иначе вы рискуете вызвать завершенный или удаленный объект.
Я настоятельно рекомендую вам прочитать эту очень важную статью Microsoft для более подробной информации о том, как работает сборка мусора. Кроме того, это справочник MSDN по внедрению финализации и утилизации для очистки неуправляемых ресурсов, внимательно посмотрите рекомендации внизу.
В двух словах:
- Если ваш объект содержит неуправляемый ресурс, вы должны реализовать IDisposable, и вы должны реализовать Finalizer.
- Если ваш объект содержит объект IDiposable, он должен также самостоятельно реализовать IDisposable и явно уничтожить этот объект.
- Если ваш объект содержит как неуправляемый, так и одноразовый предмет, финализатор должен вызвать две разные версии Dispose, одна из которых выпускает одноразовую и неуправляемую, а другая - только неуправляемую. Обычно это делается с помощью функции Dispose (bool), вызываемой Dipose() и Finalizer().
- Финализатор никогда не должен использовать какой-либо другой ресурс, кроме неуправляемого ресурса, который был выпущен, и самого себя. Несоблюдение этого требования может привести к ссылкам на собранные или удаленные объекты, поскольку объект временно ресурируется до завершения.
Новая информация: похоже, у меня две проблемы на самом деле, но одна нить кажется довольно токсичной.
Из ссылки MSDN выше:
"ThreadAbortException - это особое исключение, которое может быть перехвачено, но оно будет автоматически вызвано снова в конце блока catch".
По этой ссылке также есть очень интересный контент сообщества, в том числе "Тема. Аборт - признак плохо разработанной программы".
Так что, по крайней мере, у меня есть патроны, чтобы изменить это сейчас:)