Почему код в блоке finally не выполняется?

Кажется, что блок finally не выполняется, если он выполняет код, отличный от основного потока. Можно ли принудительно выполнить в этом случае?

Среда: VS 2010, .Net Framework 4.0.3

class Program
{
    static void Main(string[] args)
    {
        var h = new AutoResetEvent(false);

        ThreadPool.QueueUserWorkItem(
            obj => TestProc(h));

        h.WaitOne();
    }

    private static void TestProc(EventWaitHandle h)
    {
        try
        {
            Trace.WriteLine("Try");
            h.Set();
        }
        catch(Exception)
        {
            Trace.WriteLine("Catch");
        }
        finally
        {
            Thread.Sleep(2000);
            Trace.WriteLine("Finally");
        }
    }
}

Обновление:

Я нашел упоминания и объяснения по этому поводу в MSDN:

Класс ThreadAbortException http://msdn.microsoft.com/en-us/library/system.threading.threadabortexception.aspx

Когда вызывается метод Abort для уничтожения потока, среда CLR генерирует исключение ThreadAbortException. ThreadAbortException - это специальное исключение, которое может быть перехвачено, но оно будет автоматически вызвано снова в конце блока перехвата. Когда возникает это исключение, среда выполнения выполняет все блоки finally перед завершением потока. Поскольку поток может выполнять неограниченные вычисления в блоках finally или вызывать Thread.ResetAbort для отмены прерывания, нет никакой гарантии, что поток когда-либо завершится. Если вы хотите дождаться завершения прерванного потока, вы можете вызвать метод Thread.Join. Присоединение - это блокирующий вызов, который не возвращается, пока поток не прекратит выполнение.

Примечание:

Когда среда CLR останавливает фоновые потоки после завершения всех потоков переднего плана в управляемом исполняемом файле, Thread.Abort не используется. Следовательно, вы не можете использовать ThreadAbortException, чтобы определить, когда фоновые потоки завершаются CLR.


Передний план и фоновые потоки http://msdn.microsoft.com/en-us/library/h339syd0.aspx

Когда среда выполнения останавливает фоновый поток, потому что процесс завершается, в потоке не возникает никаких исключений. Однако, когда потоки останавливаются из-за того, что метод AppDomain.Unload выгружает домен приложения, возникает исключение ThreadAbortException как в потоках переднего плана, так и в фоновых потоках.


Так почему же в конце приложения CLR не использует метод AppDomain.Unload для выгрузки домена приложения перед завершением (уничтожением) основного процесса? Поскольку http://msdn.microsoft.com/en-us/library/system.appdomain.unload.aspx:

Когда поток вызывает Unload, целевой домен помечается для выгрузки. Выделенный поток пытается выгрузить домен, и все потоки в домене прерываются. Если поток не прерывается, например, потому что он выполняет неуправляемый код или потому что он выполняет блок finally, то по прошествии определенного периода времени в потоке, который изначально вызывал Unload, создается исключение CannotUnloadAppDomainException. Если поток, который не удалось прервать, в конечном итоге завершается, целевой домен не выгружается. Таким образом, в домене .NET Framework версии 2.0 не гарантируется выгрузка, поскольку может быть невозможно завершить выполнение потоков.

Заключение: в некоторых случаях мне нужно подумать, будет ли мой код выполняться в фоновом или переднем потоке? Возможно ли, что мой код не будет завершен до того, как основной поток приложения завершит всю работу?


person Andir    schedule 13.03.2012    source источник
comment
Ваша программа завершается до завершения сна. Он работает ... вы просто его отключаете.   -  person Andrew Barber    schedule 13.03.2012


Ответы (3)


Ваш код работает в фоновом потоке. Когда вы устанавливаете AutoResetEvent, ваш единственный поток переднего плана завершается (когда вы достигаете конца метода Main), и процесс прекращается «немедленно».

На самом деле, я думаю, что ваш finally блок начинает выполняться, но поскольку первое, что вы делаете, это засыпаете на две секунды, процесс завершается до того, как дойдет до вашего WriteLine вызова.

Если бы ваш Main метод все еще работал или какой-либо другой поток переднего плана поддерживал процесс, вы бы увидели, что ваш finally блок завершен как обычно. На самом деле это не вопрос «наконец-то на других потоках» - это вопрос «процесс остается живым, только пока есть потоки переднего плана».

person Jon Skeet    schedule 13.03.2012
comment
Я думаю, что понял это, чем я писал этот код, но, на мой взгляд, это нормальная операция и, наконец, блокировка гарантирована для выполнения в этих обстоятельствах. Это нормальный поток выполнения, но в итоге он был прерван. Для меня это удивительное поведение. - person Andir; 13.03.2012
comment
@Andir: Почему это удивительно? Вы попросили запустить код в фоновом потоке, и он выполнил. Если вы этого не хотите, не делайте этого! Вместо этого используйте поток переднего плана. Откровенно говоря, я думаю, что было бы намного более удивительным, если бы вы запускали код в фоновом потоке и это предотвратило выход из вашего приложения. Предполагается, что фон означает не это. - person Mark Byers; 13.03.2012
comment
Поток не может запускать какой-либо код, наконец, блокируется или нет, если вы его завершите. Если вы хотите дождаться его завершения перед завершением потока, вы должны это закодировать. - person David Schwartz; 13.03.2012
comment
Сон был добавлен только для примера, в реальном коде блок finally тоже не выполняется. - person Andir; 13.03.2012
comment
@Andir: Ну, как я уже сказал, я подозреваю, что он начинает выполняться ... но далеко не уходит. (Я пробовал это в консольном приложении с Console.WriteLine в начале блока finally перед сном, и он показал это. Если это все неожиданное поведение для вас, вероятно, это просто потому, что вы не знали о процессе политика завершения. Что вы ожидаете от фонового потока в бесконечном цикле? Поддерживать процесс в рабочем состоянии вечно? Если бы это было так, то какой была бы разница между потоком переднего плана и фоновым потоком? - person Jon Skeet; 13.03.2012
comment
Итак, насколько я понимаю, я не могу гарантировать, что ресурсы, которые я использую в фоновом потоке, будут правильно освобождены, если основной поток был завершен (ожидаемый или неожиданный). Например, если у меня есть фоновый поток, который иногда (по таймеру) очищает кеши файлов или базы данных, я не могу гарантировать правильность его работы. Это очень грустно. - person Andir; 13.03.2012
comment
@Andir: Вам всегда придется справляться с возможностью внезапного прекращения работы процесса. Обратите внимание, что есть альтернативные хуки завершения работы, которые вы можете добавить, если вам нужно. - person Jon Skeet; 13.03.2012

Вы можете предотвратить выход из основного метода до тех пор, пока он не будет выполнен. Есть много возможных подходов.

  • Для этого можно использовать синхронизацию. Например, с помощью ResetEvent, аналогичного тому, что вы уже делаете, или явного создания потока и присоединения к нему.

  • Вы можете просто просто засыпать или читать строку в конце метода Main:

    h.WaitOne();
    Console.ReadLine();
    

Затем пользователь может контролировать, когда программа завершается.

  • Вы можете использовать нефоновый поток вместо потока из пула потоков. Тогда программа не выйдет, пока поток также не завершится. Это, вероятно, лучший и самый простой вариант, если вы хотите, чтобы ваша программа не завершалась до завершения потока.
person Mark Byers    schedule 13.03.2012

У меня была такая же проблема в течение некоторого времени, и после нескольких разных попыток сработало следующее: см. Ссылку https://docs.microsoft.com/en-us/visualstudio/code-quality/ca2124?view=vs-2019

По сути, это внутри вашего Main (). Я использую его, и он очищает меня, как и ожидалось.

try { try {} finally {} } catch {}

Таким образом, ваш код окончательной очистки запускается и по-прежнему улавливает любые исключения.

person Goku    schedule 28.07.2020