При использовании методов с ограничением по времени я должен прервать рабочий поток или позволить ему запустить его курс?

109
11

В настоящее время я пишу веб-интерфейс, основанный на интерфейсе существующего приложения. Для этого я использую WCF LOB Adapter SDK, который позволяет создавать пользовательские привязки WCF, которые выставляют внешние данные и операции как web услуги.


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


Исследования привели меня к вопросу "Внедрить общий тайм-аут С#", который мудро советует использовать рабочий поток. Вооружившись этим знанием, я могу написать:


public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex,
int maxChildNodes, TimeSpan timeout)
{
Func<MetadataRetrievalNode[]> work = () => {
// Return computed metadata...
};

IAsyncResult result = work.BeginInvoke(null, null);
if (result.AsyncWaitHandle.WaitOne(timeout)) {
return work.EndInvoke(result);
} else {
throw new TimeoutException();
}
}


Однако консенсус неясно, что делать с рабочим потоком, если он истечет. Можно просто забыть об этом, как и в приведенном выше коде, или можно его прервать:


public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex,
int maxChildNodes, TimeSpan timeout)
{
Thread workerThread = null;
Func<MetadataRetrievalNode[]> work = () => {
workerThread = Thread.CurrentThread;
// Return computed metadata...
};

IAsyncResult result = work.BeginInvoke(null, null);
if (result.AsyncWaitHandle.WaitOne(timeout)) {
return work.EndInvoke(result);
} else {
workerThread.Abort();
throw new TimeoutException();
}
}


Теперь прерывание потока широко считается неправильным. Это разрушает работу в процессе, утечки ресурсов, беспорядок с блокировкой и даже не гарантирует, что поток фактически прекратит работу. Тем не менее, HttpResponse.Redirect() прерывает поток каждый раз, когда он вызывается, и IIS, похоже, совершенно этому согласен. Может быть, он приготовился как-то справиться с этим. Мое внешнее приложение, вероятно, не является.


С другой стороны, если я позволяю рабочему потоку запускать свой курс, кроме увеличения количества ресурсов (меньше доступных потоков в пуле), не будет ли утечка памяти в любом случае, потому что work.EndInvoke() никогда не будет вызван? Более конкретно, не будет ли массив MetadataRetrievalNode[], возвращаемый work навсегда?


Это только вопрос выбора меньшего из двух зол, или есть способ не прерывать рабочий поток и все еще восстанавливать память, используемую BeginInvoke()?

спросил(а) 2021-01-19T20:07:40+03:00 6 месяцев, 1 неделя назад
1
Решение
118

Хорошо, сначала Thread.Abort не так плохо, как раньше. В версии 2.0 CLR было внесено несколько улучшений, которые устраняли некоторые из основных проблем с прерыванием потоков. Это все еще плохо, заметьте, так что избегайте этого, это лучший способ действий. Если вы должны прибегнуть к прерыванию потоков, то, по крайней мере, вам следует рассмотреть возможность срывания домена приложения, откуда возникла ошибка. Это будет невероятно инвазивным в большинстве сценариев и не разрешит возможное повреждение неуправляемых ресурсов.


Кроме того, прерывание в этом случае будет иметь другие последствия. Самое главное, что вы пытаетесь прервать поток ThreadPool. Я действительно не знаю, каков конечный результат этого, и он может быть другим, в зависимости от того, какая версия фреймворка находится в игре.


Лучший способ действий состоит в том, чтобы ваш делегат Func<MetadataRetrievalNode[]> опросил переменную в безопасных точках, чтобы убедиться, что она должна прекратить выполнение самостоятельно.

public MetadataRetrievalNode[] Browse(string nodeId, int childStartIndex, int maxChildNodes, TimeSpan timeout)
{
bool terminate = false;

Func<MetadataRetrievalNode[]> work =
() =>
{
// Do some work.

Thread.MemoryBarrier(); // Ensure a fresh read of the terminate variable.
if (terminate) throw new InvalidOperationException();

// Do some work.

Thread.MemoryBarrier(); // Ensure a fresh read of the terminate variable.
if (terminate) throw new InvalidOperationException();

// Return computed metadata...
};

IAsyncResult result = work.BeginInvoke(null, null);
terminate = !result.AsyncWaitHandle.WaitOne(timeout);
return work.EndInvoke(result); // This blocks until the delegate completes.
}


Сложная часть заключается в том, как бороться с блокировкой вызовов внутри вашего делегата. Очевидно, вы не можете проверить флаг terminate, если делегат находится в середине блокирующего вызова. Но, предполагая, что блокирующий вызов инициируется из одного из законченных логических ожиданий BCL (WaitHandle.WaitOne, Monitor.Wait и т.д.), Вы можете использовать Thread.Interrupt для "выталкивания" его, и это должно немедленно разблокировать его.

ответил(а) 2021-01-19T20:07:40+03:00 6 месяцев, 1 неделя назад
63

Ответ зависит от типа работы, выполняемой вашим рабочим потоком. Я предполагаю, что он работает с внешними ресурсами, такими как подключение к данным. Thread.Abort() действительно является злом в любом случае потоков, работающих с крючками для неуправляемых ресурсов, независимо от того, насколько хорошо обернуто.


В принципе, вы хотите, чтобы ваш сервис отказался, если он истечет. На данный момент, теоретически, вызывающий больше не заботится о том, сколько времени займет нить; он только заботится о том, чтобы он "слишком долго", и должен двигаться дальше. Запрет на ошибку в методе работы рабочего потока, это закончится в конце концов; вызывающий абонент больше не заботится, потому что он больше не ждет.

Теперь, если причина, по которой нить затухала, связана с тем, что она попала в бесконечный цикл или ей велено вечно ждать какой-либо другой операции, такой как вызов службы, тогда у вас есть проблема, которую вы должны исправить, но это исправление не убивать нить. Это было бы похоже на отправку вашего ребенка в продуктовый магазин, чтобы купить хлеб, пока вы ждете в машине. Если ваш малыш продолжает тратить 15 минут в магазине, когда вы думаете, что ему нужно 5, вы, в конце концов, любопытны, зайдете и узнаете, что они делают. Если это не то, что, по вашему мнению, они должны делать, например, они все время смотрели на горшки и сковородки, вы "исправляете" их поведение для будущих случаев. Если вы зайдете и увидите своего ребенка, стоящего в длинной контрольной линии, тогда вы просто начинаете ждать дольше. В любом из этих случаев вы не должны нажимать кнопку, которая взрывает взрывчатый жилет, который они носят; это просто создает большой беспорядок, который, вероятно, будет мешать следующей способности ребенка выполнить тот же самый запрос позже.

ответил(а) 2021-01-19T20:07:40+03:00 6 месяцев, 1 неделя назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема