Можно ли прервать задачу, например, прерывание потока (метод Thread.Abort)? - PullRequest
54 голосов
/ 05 декабря 2010

Мы можем прервать поток следующим образом:

Thread thread = new Thread(SomeMethod);
.
.
.
thread.Abort();

Но можно ли прервать задачу (в .Net 4.0) таким же образом, не используя механизм отмены. Я хочу немедленно убить задание.

Ответы [ 7 ]

40 голосов
/ 14 января 2011

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

Пример;

У вас есть простое приложение Windows Form, которое подключается к блокирующему синхронному веб-сервису. В рамках которого он выполняет функцию веб-службы в параллельном цикле.

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    Thread.Sleep(120000); // pretend web service call

});

Скажем, в этом примере блокирующий вызов занимает 2 минуты. Теперь я установил свой MaxDegreeOfParallelism, чтобы сказать ProcessorCount. В iListOfItems есть 1000 элементов для обработки.

Пользователь нажимает кнопку процесса, и цикл начинается, у нас есть «до» 20 потоков, выполняющих 1000 элементов в коллекции iListOfItems. Каждая итерация выполняется в своем собственном потоке. Каждый поток будет использовать поток переднего плана при создании Parallel.ForEach. Это означает, что независимо от закрытия основного приложения, домен приложения будет оставаться активным до тех пор, пока не завершатся все потоки.

Однако пользователю по какой-то причине необходимо закрыть приложение, например, закрыть форму. Эти 20 потоков будут выполняться до тех пор, пока не будут обработаны все 1000 элементов. Это не идеальный вариант в этом сценарии, поскольку приложение не будет закрываться, как того ожидает пользователь, и будет продолжать работать за кулисами, что можно увидеть, заглянув в диспетчер задач.

Скажем, пользователь пытается перестроить приложение еще раз (VS 2010), он сообщает, что исполняемый файл заблокирован, затем ему нужно будет зайти в диспетчер задач, чтобы убить его, или просто подождать, пока все 1000 элементов будут обработаны.

Я бы не стал винить вас за это, но, конечно же! Я должен был отменить эти потоки, используя объект CancellationTokenSource и вызывая Cancel ..., но с .net 4.0 есть некоторые проблемы с этим. Во-первых, это по-прежнему никогда не приведет к прерыванию потока, которое вызовет исключение прерывания с последующим завершением потока, поэтому домену приложения вместо этого придется ждать нормального завершения потоков, а это означает ожидание последнего вызова блокировки, это будет самая последняя запущенная итерация (поток), которая в конечном итоге вызовет po.CancellationToken.ThrowIfCancellationRequested. В этом примере это будет означать, что домен приложения может оставаться активным до 2 минут, даже если форма была закрыта и вызвана отмена.

Обратите внимание, что вызов Cancel в CancellationTokenSource не генерирует исключение в потоке (ах) обработки, которое действительно будет действовать, чтобы прервать блокирующий вызов, аналогичный прерыванию потока, и остановить выполнение. Исключение кэшируется, готово к тому моменту, когда все другие потоки (параллельные итерации) в конце концов завершаются и возвращаются, исключение выдается в инициирующем потоке (где объявлен цикл).

Я выбрал , а не , чтобы использовать Cancel для объекта CancellationTokenSource. Это расточительно и, возможно, нарушает хорошо известный антипаттен управления потоком кода с помощью исключений.

Вместо этого, возможно, «лучше» реализовать простое поточно-ориентированное свойство, то есть Bool stopExecuting. Затем в цикле проверьте значение stopExecuting и, если внешнее воздействие установит значение true, мы можем выбрать альтернативный путь, чтобы изящно закрыться. Поскольку мы не должны вызывать отмену, это исключает проверку CancellationTokenSource.IsCancellationRequested , которая в противном случае была бы другой опцией.

Что-то похожее на следующее, если условие будет подходящим в цикле;

if (loopState.ShouldExitCurrentIteration || loopState.IsExceptional || stopExecuting) {loopState.Stop (); возвращение;}

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

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

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

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

CancellationTokenSource cts = new CancellationTokenSource();
ParallelOptions po = new ParallelOptions();
po.CancellationToken = cts.Token;
po.MaxDegreeOfParallelism = System.Environment.ProcessorCount;

Parallel.ForEach(iListOfItems, po, (item, loopState) =>
{

    using (cts.Token.Register(Thread.CurrentThread.Abort))
    {
        Try
        {
           Thread.Sleep(120000); // pretend web service call          
        }
        Catch(ThreadAbortException ex)
        {
           // log etc.
        }
        Finally
        {
          // clean up here
        }
    }

});

, но это все равно приведет к исключению в объявленном потоке.

Учитывая все вышесказанное, вызовы блокировки прерываний с использованием конструкций parallel.loop могли бы быть методом опций, избегая использования более неясных частей библиотеки.Но почему нет возможности отменить и избежать выбрасывания исключения в методе объявления, мне кажется возможным упущением.

38 голосов
/ 12 февраля 2014

Но можно ли отменить задачу (в .Net 4.0) таким же образом, не используя механизм отмены. Я хочу немедленно убить задание .

Другие ответчики сказали вам не делать этого.Но да, вы можете сделать это.Вы можете указать Thread.Abort() в качестве делегата, который будет вызываться механизмом отмены задания.Вот как вы можете настроить это:

class HardAborter
{
  public bool WasAborted { get; private set; }
  private CancellationTokenSource Canceller { get; set; }
  private Task<object> Worker { get; set; }

  public void Start(Func<object> DoFunc)
  {
    WasAborted = false;

    // start a task with a means to do a hard abort (unsafe!)
    Canceller = new CancellationTokenSource();

    Worker = Task.Factory.StartNew(() => 
      {
        try
        {
          // specify this thread's Abort() as the cancel delegate
          using (Canceller.Token.Register(Thread.CurrentThread.Abort))
          {
            return DoFunc();
          }
        }
        catch (ThreadAbortException)
        {
          WasAborted = true;
          return false;
        }
      }, Canceller.Token);
  }

  public void Abort()
  {
    Canceller.Cancel();
  }

}

отказ от ответственности : не делайте этого.

Вот пример того, чего не следует делать:

 var doNotDoThis = new HardAborter();

 // start a thread writing to the console
 doNotDoThis.Start(() =>
    {
       while (true)
       {
          Thread.Sleep(100);
          Console.Write(".");
       }
       return null;
    });


 // wait a second to see some output and show the WasAborted value as false
 Thread.Sleep(1000);
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted);

 // wait another second, abort, and print the time
 Thread.Sleep(1000);
 doNotDoThis.Abort();
 Console.WriteLine("Abort triggered at " + DateTime.Now);

 // wait until the abort finishes and print the time
 while (!doNotDoThis.WasAborted) { Thread.CurrentThread.Join(0); }
 Console.WriteLine("WasAborted: " + doNotDoThis.WasAborted + " at " + DateTime.Now);

 Console.ReadKey();

output from sample code

28 голосов
/ 05 декабря 2010
  1. Вы не должны использовать Thread.Abort ()
  2. Задачи могут быть отменены, но не отменены.

* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 100 '* * * * * * * * * * * * * * * * * * "100" * ".оставив систему в нестабильном / неопределенном состоянии.

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

2 голосов
/ 05 декабря 2010

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

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

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

1 голос
/ 16 апреля 2019

Все знают (надеюсь), что это плохо - завершать тему. Проблема в том, что у вас нет части кода, который вы вызываете. Если этот код выполняется в некотором бесконечном цикле do / while, который сам вызывает некоторые нативные функции и т. Д., Вы в основном застряли. Когда это происходит при завершении вашего кода, остановке или вызове Dispose, все в порядке, если вы начинаете отстреливать плохих парней (чтобы вы сами не стали плохим парнем).

Итак, что бы это ни стоило, я написал те две блокирующие функции, которые используют свой собственный собственный поток, а не поток из пула или какой-то поток, созданный CLR. Они остановят поток, если истечет время ожидания:

// returns true if the call went to completion successfully, false otherwise
public static bool RunWithAbort(this Action action, int milliseconds) => RunWithAbort(action, new TimeSpan(0, 0, 0, 0, milliseconds));
public static bool RunWithAbort(this Action action, TimeSpan delay)
{
    if (action == null)
        throw new ArgumentNullException(nameof(action));

    var source = new CancellationTokenSource(delay);
    var success = false;
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            action();
            success = true;
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return success;
}

// returns what's the function should return if the call went to completion successfully, default(T) otherwise
public static T RunWithAbort<T>(this Func<T> func, int milliseconds) => RunWithAbort(func, new TimeSpan(0, 0, 0, 0, milliseconds));
public static T RunWithAbort<T>(this Func<T> func, TimeSpan delay)
{
    if (func == null)
        throw new ArgumentNullException(nameof(func));

    var source = new CancellationTokenSource(delay);
    var item = default(T);
    var handle = IntPtr.Zero;
    var fn = new Action(() =>
    {
        using (source.Token.Register(() => TerminateThread(handle, 0)))
        {
            item = func();
        }
    });

    handle = CreateThread(IntPtr.Zero, IntPtr.Zero, fn, IntPtr.Zero, 0, out var id);
    WaitForSingleObject(handle, 100 + (int)delay.TotalMilliseconds);
    CloseHandle(handle);
    return item;
}

[DllImport("kernel32")]
private static extern bool TerminateThread(IntPtr hThread, int dwExitCode);

[DllImport("kernel32")]
private static extern IntPtr CreateThread(IntPtr lpThreadAttributes, IntPtr dwStackSize, Delegate lpStartAddress, IntPtr lpParameter, int dwCreationFlags, out int lpThreadId);

[DllImport("kernel32")]
private static extern bool CloseHandle(IntPtr hObject);

[DllImport("kernel32")]
private static extern int WaitForSingleObject(IntPtr hHandle, int dwMilliseconds);
0 голосов
/ 12 января 2018

Если у вас есть конструктор задач, мы можем извлечь Thread из задачи и вызвать thread.abort.

Thread th = null;

Task.Factory.StartNew(() =>
{
    th = Thread.CurrentThread;

    while (true)
    {
        Console.WriteLine(DateTime.UtcNow);
    }
});

Thread.Sleep(2000);
th.Abort();
Console.ReadKey();
0 голосов
/ 04 февраля 2017
using System;
using System.Threading;
using System.Threading.Tasks;

...

var cts = new CancellationTokenSource();
var task = Task.Run(() => { while (true) { } });
Parallel.Invoke(() =>
{
    task.Wait(cts.Token);
}, () =>
{
    Thread.Sleep(1000);
    cts.Cancel();
});

Это простой фрагмент для прерывания бесконечной задачи с CancellationTokenSource .

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