Вопрос о чистом завершении потока в.NET
Я понимаю, что Thread.Abort() является злом из множества статей, которые я прочитал по этой теме, поэтому в настоящее время я нахожусь в процессе извлечения из моего аборта, чтобы заменить его более чистым способом; и после сравнения пользовательских стратегий от людей, находящихся здесь в stackru, а затем после прочтения " Как: создать и завершить потоки (Руководство по программированию в C#)" из MSDN, в которых говорится, что подход почти одинаков - то есть использовать volatile bool
Стратегия проверки подхода, что приятно, но у меня еще есть несколько вопросов....
Сразу видно, что если у вас нет простого рабочего процесса, который просто запускает цикл обработки кода? Например, для меня мой процесс является процессом загрузки фоновых файлов, я делаю цикл по каждому файлу, так что это что-то, и я уверен, что могу добавить свой while (!_shouldStop)
вверху, которая покрывает каждую итерацию цикла, но у меня есть еще много бизнес-процессов, которые происходят до того, как он достигнет следующей итерации цикла, я хочу, чтобы эта процедура отмены была быстрой; не говорите мне, что мне нужно посыпать эти циклы while каждые 4-5 строк на протяжении всей моей рабочей функции?!
Я действительно надеюсь, что есть лучший способ, может кто-нибудь посоветовать мне, если это на самом деле, правильный [и только?] Подход к этому или стратегии, которые они использовали в прошлом для достижения того, чего я добиваюсь.
Спасибо, банда.
Дальнейшее чтение: Все эти ответы SO предполагают, что рабочий поток будет зациклен. Это не устраивает меня. Что если это линейная, но своевременная фоновая операция?
8 ответов
К сожалению, не может быть лучшего варианта. Это действительно зависит от вашего конкретного сценария. Идея состоит в том, чтобы аккуратно остановить поток в безопасных точках. В этом суть причины, по которой Thread.Abort
не хорошо; потому что это не гарантируется в безопасных точках. Посыпая код механизмом остановки, вы фактически вручную определяете безопасные точки. Это называется совместное аннулирование. Есть в основном 4 широких механизма для этого. Вы можете выбрать тот, который лучше всего соответствует вашей ситуации.
Опрос остановочного флага
Вы уже упоминали этот метод. Это довольно распространенный. Периодически проверяйте флаг в безопасных точках вашего алгоритма и спасайтесь, когда он получает сигнал. Стандартный подход заключается в маркировке переменной volatile
, Если это невозможно или неудобно, вы можете использовать lock
, Помните, вы не можете пометить локальную переменную как volatile
поэтому, если лямбда-выражение захватывает его, например, через замыкание, вам придется прибегнуть к другому методу для создания необходимого барьера памяти. Существует не так много, что нужно сказать об этом методе.
Используйте новые механизмы отмены в TPL
Это похоже на опрос флага остановки, за исключением того, что он использует новые структуры данных отмены в TPL. Это все еще основано на совместных шаблонах отмены. Вам нужно получить CancellationToken
и периодически проверять IsCancellationRequested
, Для запроса отмены вы бы позвонили Cancel
на CancellationTokenSource
который изначально предоставил маркер. С новыми механизмами отмены можно многое сделать. Вы можете прочитать больше о здесь.
Используйте ручки ожидания
Этот метод может быть полезен, если ваш рабочий поток требует ожидания в течение определенного интервала или для сигнала во время своей нормальной работы. Вы можете Set
ManualResetEvent
Например, чтобы сообщить потоку, что пора остановиться. Вы можете проверить событие, используя WaitOne
функция, которая возвращает bool
указывает, было ли событие сигнализировано. WaitOne
принимает параметр, который указывает, сколько времени ждать возврата вызова, если событие не было сигнализировано за это время. Вы можете использовать эту технику вместо Thread.Sleep
и получить указание остановки одновременно. Это также полезно, если есть другие WaitHandle
случаи, когда поток может ждать. Ты можешь позвонить WaitHandle.WaitAny
ждать любого события (включая событие остановки) все в одном вызове. Использование события может быть лучше, чем вызов Thread.Interrupt
так как у вас есть больше контроля над потоком программы (Thread.Interrupt
выдает исключение, чтобы вы могли стратегически разместить try-catch
блоки для выполнения любой необходимой очистки).
Специализированные сценарии
Существует несколько одноразовых сценариев с очень специализированными механизмами остановки. Этот ответ определенно выходит за рамки перечисления их всех (не говоря уже о том, что это было бы почти невозможно). Хороший пример того, что я имею в виду, это Socket
учебный класс. Если поток заблокирован при вызове Send
или же Receive
потом звоню Close
прервет сокет при любом блокирующем вызове, который он использовал, чтобы эффективно его разблокировать. Я уверен, что есть несколько других областей в BCL, где аналогичные методы могут быть использованы для разблокировки потока.
Прервать поток черезThread.Interrupt
Преимущество здесь в том, что это просто, и вам не нужно сосредотачиваться на том, чтобы посыпать свой код чем-то действительно. Недостатком является то, что у вас мало контроля над тем, где находятся безопасные точки в вашем алгоритме. Причина в том, что Thread.Interrupt
работает, внедряя исключение в один из стандартных вызовов блокировки BCL. Они включают Thread.Sleep
, WaitHandle.WaitOne
, Thread.Join
и т. д. Так что вы должны быть мудрыми в том, где вы их размещаете. Однако в большинстве случаев алгоритм определяет, куда они идут, и в любом случае это нормально, особенно если ваш алгоритм проводит большую часть своего времени в одном из этих блокирующих вызовов. Если ваш алгоритм не использует один из блокирующих вызовов в BCL, тогда этот метод не будет работать для вас. Теория здесь заключается в том, что ThreadInterruptException
генерируется только из ожидающего вызова.NET, поэтому, скорее всего, в безопасной точке. По крайней мере, вы знаете, что поток не может находиться в неуправляемом коде или выйти из критической секции, оставляя висячий замок в полученном состоянии. Несмотря на то, что это менее агрессивно, чем Thread.Abort
Я все еще не одобряю его использование, потому что не очевидно, какие вызовы отвечают на него, и многие разработчики будут незнакомы с его нюансами.
Ну, к сожалению, в многопоточности вам часто приходится идти на компромисс с "чистотой" ради чистоты... вы можете немедленно выйти из потока, если вы Interrupt
это, но это не будет очень чисто. Так что нет, вам не нужно посыпать _shouldStop
проверяет каждые 4-5 строк, но если вы прерываете поток, вам следует обработать исключение и выйти из цикла чистым способом.
Обновить
Даже если это не зацикливающийся поток (т. Е., Возможно, это поток, выполняющий какую-то длительную асинхронную операцию или некоторый тип блока для операции ввода), вы можете Interrupt
это, но вы все равно должны поймать ThreadInterruptedException
и выйти из нити чисто. Я думаю, что примеры, которые вы читали, очень уместны.
Обновление 2.0
Да, у меня есть пример... Я просто покажу вам пример, основанный на ссылке, на которую вы ссылались:
public class InterruptExample
{
private Thread t;
private volatile boolean alive;
public InterruptExample()
{
alive = false;
t = new Thread(()=>
{
try
{
while (alive)
{
/* Do work. */
}
}
catch (ThreadInterruptedException exception)
{
/* Clean up. */
}
});
t.IsBackground = true;
}
public void Start()
{
alive = true;
t.Start();
}
public void Kill(int timeout = 0)
{
// somebody tells you to stop the thread
t.Interrupt();
// Optionally you can block the caller
// by making them wait until the thread exits.
// If they leave the default timeout,
// then they will not wait at all
t.Join(timeout);
}
}
Если отмена является требованием того, что вы создаете, то к нему следует относиться с таким же уважением, как и к остальному вашему коду - это может быть то, для чего вы должны разработать.
Предположим, что ваш поток постоянно выполняет одну из двух вещей.
- Что-то связано с процессором
- В ожидании ядра
Если вы связаны с процессором в обсуждаемом потоке, у вас, вероятно, есть подходящее место для вставки проверки отказа. Если вы вызываете чужой код для выполнения какой-то длительной задачи, связанной с процессором, вам может потребоваться исправить внешний код, вывести его из процесса (прерывание потоков - это зло, но прерывание процессов четко определено и безопасно), так далее.
Если вы ожидаете ядро, то, вероятно, в ожидании задействован дескриптор (или fd, или порт mach...). Обычно, если вы уничтожаете соответствующий дескриптор, ядро немедленно возвращает код ошибки. Если вы находитесь в.net/java/ и т.д. вы, вероятно, в конечном итоге с исключением. В C любой код, который у вас уже есть для обработки сбоев системных вызовов, распространит ошибку до значимой части вашего приложения. В любом случае, вы выходите из низкоуровневого места довольно чисто и своевременно, без необходимости разбрасывать повсюду новый код.
Тактика, которую я часто использую с этим видом кода, состоит в том, чтобы отслеживать список дескрипторов, которые должны быть закрыты, и затем моя функция прерывания устанавливает флаг "отменен", а затем закрывает их. Когда функция завершается ошибкой, она может проверять флаг и сообщать о сбое из-за отмены, а не из-за какого-либо конкретного исключения / errno.
Вы, похоже, подразумеваете, что приемлемая степень детализации для отмены находится на уровне вызова службы. Это, вероятно, не очень хорошая мысль - вам гораздо лучше отменить фоновую работу синхронно и присоединиться к старому фоновому потоку из потока переднего плана. Это намного чище, потому что:
Это позволяет избежать класса условий гонки, когда старые потоки bgwork возвращаются к жизни после неожиданных задержек.
Это позволяет избежать потенциальных скрытых потоков / утечек памяти, вызванных зависанием фоновых процессов, позволяя скрыть эффекты зависания фонового потока.
Есть две причины, чтобы бояться этого подхода:
Вы не думаете, что сможете своевременно прервать собственный код. Если отмена является требованием вашего приложения, то решение, которое вам действительно нужно принять, является решением о ресурсах / бизнесе: сделайте взлом или исправьте свою проблему чисто.
Вы не доверяете некоторому коду, который вызываете, потому что он находится вне вашего контроля. Если вы действительно не доверяете этому, рассмотрите возможность удаления его из процесса. Таким образом, вы получаете намного лучшую изоляцию от многих видов рисков, включая этот.
Лучший ответ во многом зависит от того, что вы делаете в ветке.
Как вы сказали, большинство ответов вращаются вокруг опроса общего логического значения в каждой паре строк. Даже если вам это не нравится, зачастую это самая простая схема. Если вы хотите облегчить свою жизнь, вы можете написать метод, такой как ThrowIfCancelled(), который выдает какое-то исключение, если вы закончили. Пуристы скажут, что это (задыхается) использование исключений для управления потоком, но опять же каскадирование является исключительным imo.
Если вы выполняете операции ввода-вывода (например, сетевые операции), вы можете рассмотреть возможность выполнения всего, используя асинхронные операции.
Если вы делаете последовательность шагов, вы можете использовать трюк IEnumerable, чтобы сделать конечный автомат. Пример:
<
abstract class StateMachine : IDisposable
{
public abstract IEnumerable<object> Main();
public virtual void Dispose()
{
/// ... override with free-ing code ...
}
bool wasCancelled;
public bool Cancel()
{
// ... set wasCancelled using locking scheme of choice ...
}
public Thread Run()
{
var thread = new Thread(() =>
{
try
{
if(wasCancelled) return;
foreach(var x in Main())
{
if(wasCancelled) return;
}
}
finally { Dispose(); }
});
thread.Start()
}
}
class MyStateMachine : StateMachine
{
public override IEnumerabl<object> Main()
{
DoSomething();
yield return null;
DoSomethingElse();
yield return null;
}
}
// then call new MyStateMachine().Run() to run.
>
Переустройства? Это зависит от того, сколько конечных автоматов вы используете. Если у вас есть только 1, да. Если у вас есть 100, то, возможно, нет. Слишком сложно? Смотря как. Другим преимуществом этого подхода является то, что он позволяет (с небольшими изменениями) переместить вашу операцию в обратный вызов Timer.tick и полностью исключить многопоточность, если это имеет смысл.
и делай все, что говорит blucz.
Возможно, проблема в том, что у вас такой длинный цикл метода / while. Независимо от того, возникают ли у вас проблемы с потоками, вы должны разбить их на более мелкие этапы обработки. Давайте предположим, что это шаги Alpha(), Bravo(), Charlie() и Delta().
Затем вы можете сделать что-то вроде этого:
public void MyBigBackgroundTask()
{
Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta };
int workStepSize = 0;
while (!_shouldStop)
{
tasks[workStepSize++]();
workStepSize %= tasks.Length;
};
}
Так что да, он зацикливается бесконечно, но проверяет, не пора ли остановиться между каждым шагом в бизнесе.
Вместо добавления цикла while, где цикл не принадлежит, добавьте что-то вроде if (_shouldStop) CleanupAndExit();
где бы это ни было целесообразно. Там нет необходимости проверять после каждой отдельной операции или посыпать весь код ими. Вместо этого думайте о каждой проверке как о возможности выйти из потока в этой точке и добавить их стратегически с учетом этого.
Вам не нужно посыпать, пока петли повсюду. Внешний цикл while просто проверяет, было ли сказано, чтобы он остановился, и если это так, не делает еще одну итерацию...
Если у вас есть прямой поток "сделайте что-нибудь и закройте" (в нем нет циклов), тогда вы просто проверяете логическое значение _shouldStop либо до, либо после каждого основного пятна внутри потока. Таким образом, вы знаете, следует ли продолжать или выручить.
например:
public void DoWork() {
RunSomeBigMethod();
if (_shouldStop){ return; }
RunSomeOtherBigMethod();
if (_shouldStop){ return; }
//....
}
Все эти SO-ответы предполагают, что рабочий поток будет зациклен. Это не удобно сидеть со мной
Существует не так много способов сделать код длительным. Циклы - довольно важная программная конструкция. Создание кода занимает много времени без циклов и требует огромного количества утверждений. Сотни тысяч.
Или вызывая какой-то другой код, который выполняет за вас цикл. Да, трудно заставить этот код останавливаться по требованию. Это просто не работает.