可以像中止一个Thread(Thread.Abort方法)一样中止一个Task吗?

Ami*_*imi 57 c# parallel-processing .net-4.0

我们可以中止这样的线程:

Thread thread = new Thread(SomeMethod);
.
.
.
thread.Abort();
Run Code Online (Sandbox Code Playgroud)

但是我可以以相同的方式中止任务(在.Net 4.0中)而不是取消机制.我想立即杀死任务.

Mic*_*per 41

不使用线程中止的指导是有争议的.我认为仍有一席之地,但在特殊情况下.但是,您应该始终尝试围绕它进行设计并将其视为最后的手段.

例;

您有一个简单的Windows窗体应用程序连接到阻塞同步Web服务.在其中,它在并行循环内的Web服务上执行函数.

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

});
Run Code Online (Sandbox Code Playgroud)

比如在这个例子中,阻塞调用需要2分钟才能完成.现在我将MaxDegreeOfParallelism设置为ProcessorCount.iListOfItems中有1000个要处理的项目.

用户单击进程按钮并开始循环,我们在iListOfItems集合中对1000个项目执行"最多"20个线程.每次迭代都在自己的线程上执行.每个线程在Parallel.ForEach创建时都将使用前台线程.这意味着无论主应用程序关闭,app域都将保持活动状态,直到所有线程都完成.

但是,用户需要关闭应用程序,因为他们关闭了表单.这20个线程将继续执行,直到处理完所有1000个项目.在这种情况下,这并不理想,因为应用程序不会像用户期望的那样退出,并将继续在幕后运行,这可以通过查看任务管理器来看出.

假设用户再次尝试重建应用程序(VS 2010),它报告exe已被锁定,那么他们将不得不进入任务管理器来杀死它或者等到所有1000个项目都被处理完毕.

我不会责怪你说,但当然!我应该使用CancellationTokenSource对象取消这些线程并调用Cancel ...但是从.net 4.0开始存在一些问题.首先,这仍然不会导致线程中止,这将提供中止异常,然后是线程终止,因此app域将需要等待线程正常完成,这意味着等待最后一次阻塞调用,这将是最终调用的最后一次运行迭代(线程)po.CancellationToken.ThrowIfCancellationRequested.在示例中,这意味着app域仍然可以保持活动长达2分钟,即使表单已关闭并取消调用.

注意,CancellationTokenSource上的Calling Cancel不会在处理线程上抛出异常,这确实会中断阻塞调用,类似于线程中止并停止执行.当所有其他线程(并发迭代)最终完成并返回时,一个异常被缓存,在启动线程(声明循环的地方)中抛出异常.

我选择不在 CancellationTokenSource对象上使用Cancel选项.这是浪费的,并且可以说是违反了众所周知的控制异常代码流的反模式.

相反,实现一个简单的线程安全属性,即Bool stopExecuting可以说是"更好".然后在循环中检查stopExecuting的值,如果外部影响将值设置为true,我们可以采用备用路径优雅地关闭.因为我们不应该调用cancel,所以这排除了检查CancellationTokenSource.IsCancellationRequested,否则这将是另一个选项.

如果条件在循环中适当,则类似以下内容;

if(loopState.ShouldExitCurrentIteration || loopState.IsExceptional || stopExecuting){loopState.Stop(); 返回;}

迭代现在将以"受控"的方式退出,并终止进一步的迭代,但正如我所说,这对我们必须等待长时间运行和阻塞每次迭代中的调用的问题几乎没有作用(并行循环线程),因为这些必须在每个线程可以选择检查它是否应该停止之前完成.

总之,当用户关闭表单时,将通过stopExecuting通知20个线程停止,但它们只会在完成执行长时间运行的函数调用后停止.

我们无法做任何关于应用程序域将始终保持活动并且仅在所有前台线程完成时释放的事实.这意味着等待在循环内完成的任何阻塞调用都会有延迟.

只有真正的线程中止才能中断阻塞调用,并且您必须尽可能地避免系统处于不稳定/未定义状态,最好能够在中止线程的异常处理程序中运行,这是毫无疑问的.根据他们选择维护的资源句柄以及在线程的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
        }
    }

});
Run Code Online (Sandbox Code Playgroud)

但这仍然会在声明线程中导致异常.

考虑到所有事情,使用parallel.loop构造的中断阻塞调用可能是选项的一种方法,避免使用更复杂的库部分.但是为什么没有选择取消并避免在声明方法中抛出异常会让我觉得可能是疏忽.

  • 嗨,Terry,我尝试了您使用线程中止的示例,但该示例无法正常工作。如果考虑一下,长时间运行的操作(在本例中为thread.sleep)正在使工作线程挂起,因此取消回调无法在该线程上运行,因此无法在线程上调用Thread.CurrentThread.Abort。挂起的线程。您可以通过将回调转换为方法组并在调试器中使用线程ID来观察此情况。就我而言,中止在令牌源上调用Cancel的线程上被调用。 (2认同)
  • 在 NET 5.0 和 Core 出现之前,这是一个很好的解决方案。您将无法使用任何类型的 Thread.Abort,因为它是 PlatformNotSupportedException。 (2认同)

jlt*_*rem 38

但是我可以以相同的方式中止任务(在.Net 4.0中)而不是取消机制.我想立即杀死任务.

其他的回答者告诉你不要这样做.但是,是的,你可以做到.您可以提供Thread.Abort()由Task的取消机制调用的委托.以下是您可以配置的方法:

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();
  }

}
Run Code Online (Sandbox Code Playgroud)

免责声明:不要这样做.

以下是不应该做的事情的示例:

 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();
Run Code Online (Sandbox Code Playgroud)

示例代码的输出

  • 正如我所理解的那样,你不应该混合任务和线程功能.一个线程可以承载许多任务,你中止整个线程(带有所有负面影响)只是为了中止一个任务.我真的不得不对此投票. (7认同)
  • 这是一个很好的解决方案,但不幸的是,.NET.Core 不支持 Thread.CurrentThread.Abort。有人有解决方案吗? (3认同)
  • .NET 5.0 + 或 Core 不支持。也不支持应用程序域容器技巧。我同意..在某些情况下,中间层将无限期地阻塞并且无法重写较低层。 (2认同)

Hen*_*man 30

  1. 你不应该使用Thread.Abort()
  2. 任务可以取消但不会中止.

Thread.Abort的()方法是(严重)弃用.

线程和任务在停止时应该合作,否则您将面临使系统处于不稳定/未定义状态的风险.

如果您确实需要运行进程并从外部终止它,唯一安全的选择是在单独的AppDomain中运行它.

  • 如果您调用无限期阻塞的第三方组件(当然,它不应该这样做,但我们在这里遇到),您不能包含在该组件内轮询取消令牌所需的代码.而且您不必为任何可能无法从阻塞调用返回(或不及时返回)的代码或组件创建单独的可执行文件和AppDomain! (50认同)
  • "......(严重地)弃用了." [引证需要] (33认同)
  • @amkh - Task库包含一个名为CancelationToken的取消机制.你为什么不用它? (2认同)
  • @amkh:AppDomain看起来很适合您。您不希望所有这些不可预测的代码在主内存空间中运行。 (2认同)

Sim*_*ier 6

每个人都(希望)知道终止线程是不好的。问题是当您不拥有要调用的代码时。如果此代码在某些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);
Run Code Online (Sandbox Code Playgroud)

  • 我喜欢这个答案!正如一位聪明人所说“软件中的每个问题都可以通过添加一定程度的间接来解决” (2认同)