Вопрос о прекращении потока чисто in.NET


Я понимаю нить.Abort () - это зло из множества статей, которые я прочитал по этой теме, поэтому в настоящее время я разрываю свой аборт, чтобы заменить его на более чистый способ; и после сравнения пользовательских стратегий от людей здесь на stackoverflow, а затем после чтения "как создать и завершить потоки (руководство по программированию на C#)" из MSDN оба, которые заявляют подход очень похожий - который должен использовать volatile bool подход проверка стратегии, что приятно, но у меня еще есть несколько вопросов....

сразу же, что выделяется для меня здесь, это то, что если у вас нет простого рабочего процесса, который просто выполняет цикл хруста кода? Например, для меня мой процесс является фоновым процессом загрузки файлов, я действительно просматриваю каждый файл, так что это что-то, и, конечно же, я мог бы добавить свой while (!_shouldStop) в верхней части, которая охватывает меня каждую итерацию цикла, но у меня есть еще много бизнес-процессов, которые происходят раньше он попадает в следующую итерацию цикла, я хочу, чтобы эта процедура отмены была быстрой; не говорите мне, что мне нужно разбрызгивать эти циклы каждые 4-5 строк по всей моей рабочей функции?!

Я действительно надеюсь, что есть лучший способ, может кто-нибудь, пожалуйста, посоветуйте мне, если это на самом деле, правильный [и только?] подход к этому, или стратегии, которые они использовали в прошлом, чтобы достичь того, что я после.

спасибо банды.

дальнейшее чтение: все эти ответы SO предположим, рабочий поток будет цикл. Это не очень удобно для меня. Что делать, если это линейная, но своевременная фоновая операция?

8 54

8 ответов:

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

опрос остановочный флаг

вы уже упоминали этот метод. Это довольно распространенное явление. Сделайте периодические проверки флага в безопасных точках вашего алгоритма и выручите, когда он получит сигнал. Стандартный подход заключается в том, чтобы отметить переменную volatile. Если это невозможно или неудобно, то вы можете использовать lock. Помните, что вы не можете пометить локальную переменную как volatile Так что если лямбда-выражение захватывает его через закрытие, например, тогда вам придется прибегнуть к другому методу для создания барьера памяти, который требуется. Существует не так много еще, что нужно сказать для этого метода.

используйте новые механизмы отмены в TPL

это похоже на опрос флага остановки, за исключением того, что он использует новые структуры данных отмены в TPL. Он по-прежнему основан на кооперативных схемах отмены. Вам нужно получить CancellationToken и периодически проверять IsCancellationRequested. Чтобы запросить отмену вы бы позвонили Cancel на CancellationTokenSource который изначально предоставил токен. Существует много вы можете сделать с новыми механизмами отмене. Вы можете прочитать больше о здесь.

используйте дескрипторы ожидания

этот метод может быть полезен, если ваш рабочий поток требует ожидания на определенном интервале или для сигнала во время его нормальной работы. Вы можете Set a 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 Я все еще не одобряю его использование, потому что не очевидно, какие вызовы отвечают на него, и многие разработчики будут незнакомы с его нюансами.

ну, к сожалению, в многопоточности вам часто приходится идти на компромисс "snappiness" для чистоты... вы можете выйти из потока немедленно, если вы 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);
    }
}

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

предположим, что ваш поток делает одну из двух вещей во все времена.

  1. что-то связанное с процессором
  2. ожидание ядра

Если вы привязаны к процессору в рассматриваемом потоке, вы наверное есть хорошее место для вставки чек на спасение. Если вы вызываете чужой код для выполнения какой-либо длительной задачи, связанной с ЦП, вам может потребоваться исправить внешний код, переместить его из процесса (прерывание потоков-это зло, но прерывание процессов четко определено и безопасно) и т. д.

Если вы ждете ядра, то, вероятно, есть дескриптор (или fd, или порт mach,...) участвует в ожидании. Обычно, если вы уничтожите соответствующий дескриптор, ядро немедленно вернется с некоторым кодом сбоя. Если ты внутри .сеть/Ява/и др. вы, вероятно, в конечном итоге с исключением. В C любой код, который у вас уже есть для обработки сбоев системного вызова, распространит ошибку до значимой части вашего приложения. В любом случае, вы выходите из низкоуровневого места довольно чисто и очень своевременно, не нуждаясь в новом коде, разбрызганном повсюду.

тактика, которую я часто использую с такого рода кодом, заключается в отслеживании списка дескрипторов, которые необходимо закрыть, а затем установить мою функцию abort a "отменен" флаг, а затем закрыть их. Когда функция терпит неудачу, она может проверить флаг и сообщить об ошибке из-за отмены, а не из-за какого-либо конкретного исключения/errno.

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

  1. Это позволяет избежать класса условий гонки, когда старые потоки bgwork возвращаются к жизни после неожиданных задержек.

  2. Это позволяет избежать потенциальных скрытых потоков / утечек памяти, вызванных висячими фоновыми процессами, позволяя скрыть эффекты висячего фонового потока.

есть две причины бояться такого подхода:

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

  2. вы не доверяете коду, который вы вызываете, потому что это вне вашего контроля. Если вы действительно не доверяете ему, подумайте о том, чтобы вывести его из процесса. Вы получаете гораздо лучшую изоляцию от многих видов рисков, включая этот, Таким образом.

возможно, часть проблемы заключается в том, что у вас такая длинная метод / цикл while. Независимо от того, есть ли у вас проблемы с потоками, вы должны разбить его на более мелкие этапы обработки. Предположим, что эти шаги-Альфа(), Браво (), Чарли () и Дельта ().

тогда вы могли бы сделать что-то вроде этого:

    public void MyBigBackgroundTask()
    {
        Action[] tasks = new Action[] { Alpha, Bravo, Charlie, Delta };
        int workStepSize = 0;
        while (!_shouldStop)
        {
            tasks[workStepSize++]();
            workStepSize %= tasks.Length;
        };
    }

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

вы не должны посыпать в то время как петли везде. Внешний цикл while просто проверяет, было ли ему сказано остановиться, и если да, то не делает еще одну итерацию...

Если у вас есть прямой поток "go do something and close out" (без петель в нем), то вы просто проверяете логическое значение _shouldStop либо до, либо после каждого основного места внутри потока. Таким образом, вы знаете, следует ли продолжать или выручить.

например:

public void DoWork() {

  RunSomeBigMethod();
  if (_shouldStop){ return; }
  RunSomeOtherBigMethod();
  if (_shouldStop){ return; }
  //....
}

лучший ответ во многом зависит от того, что ты делаешь в треде.

  • Как вы сказали, большинство ответов вращаются вокруг опроса общего логического значения каждые пару строк. Даже если вам это может не понравиться, это часто самая простая схема. Если вы хотите сделать свою жизнь проще, вы можете написать такой метод, как ThrowIfCancelled (), который выдает какое-то исключение, если вы закончили. Пуристы скажут, что это (вздох), используя исключения для потока управления, но затем снова cacelling является исключительным ИМО.

  • Если вы выполняете операции ввода-вывода (например, сетевые вещи), вы можете рассмотреть возможность выполнения всех операций с использованием асинхронных операций.

  • Если вы делаете последовательность шагов, вы можете использовать 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, а может и нет. Слишком сложно? Ну, это зависит от. Еще один бонус этого подхода заключается в том, что он позволяет вам (с небольшими изменениями) перемещать вашу операцию в Таймер.отметьте обратный вызов и пустую резьбу вообще, если это имеет смысл.

и делать все, что говорит блюц тоже.

вместо добавления цикла while, где цикл иначе не принадлежит, добавьте что-то вроде if (_shouldStop) CleanupAndExit(); везде, где это имеет смысл сделать. Нет необходимости проверять после каждой операции, или посыпать кода вместе с ними. Вместо этого подумайте о каждой проверке как о шансе выйти из потока в этот момент и добавить их стратегически с учетом этого.

все эти ответы SO предполагают, что рабочий поток будет циклическим. Это не сидит комфортно со мной

существует не так много способов заставить код занять много времени. Зацикливание-довольно существенная программная конструкция. Создание кода занимает много времени без цикла занимает огромный количество заявлений. Сотни тысяч.

или вызов какого-то другого кода, который делает цикл для вас. Да, трудно заставить этот код остановиться требовать. Это просто не работает.