Вопрос о чистом завершении потока в .NET - PullRequest
58 голосов
/ 03 сентября 2010

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

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

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

Спасибо, банда.

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

Ответы [ 8 ]

101 голосов
/ 03 сентября 2010

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

12 голосов
/ 03 сентября 2010

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

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

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

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

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

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

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

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

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

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

Существует два причины, по которым следует опасаться такого подхода:

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

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

2 голосов
/ 03 сентября 2010

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

2 голосов
/ 03 сентября 2010

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

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

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

  • Если вы выполняете последовательность шагов, вы можете использовать трюк IEnumerable для создания конечного автомата. Пример:

<</p>

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.

2 голосов
/ 03 сентября 2010

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

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

например:

public void DoWork() {

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

2 голосов
/ 03 сентября 2010

Возможно, проблема в том, что у вас такой длинный метод / цикл. Независимо от того, возникают ли у вас проблемы с потоками, вы должны разбить их на более мелкие этапы обработки. Давайте предположим, что это шаги 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;
        };
    }

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

1 голос
/ 03 сентября 2010

Все эти ответы SO предполагают, что рабочий поток зациклится.Мне это неудобно

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...