如何中止类似中止线程的任务(Thread.Abort 方法) ?

我们可以像这样终止 Thread:

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

但是,我可以中止一个 Task(在.Net 4.0) ,以同样的方式,而不是取消机制

121281 次浏览
  1. 您不应该使用 Thread
  2. 任务可以取消,但不能中止。

线程,终止()方法已被(严重)弃用。

线程和任务在停止时都应该协作,否则将冒险使系统处于不稳定/未定义状态。

如果确实需要运行一个 Process 并从外部终止它,那么唯一安全的选择是在单独的 AppDomain 中运行它。


这个答案是关于.net 3.5和更早版本的。

此后,通过更改 finally 块工作的方式,改进了线程中止处理。

但是 Thread.Abort 仍然是一个可疑的解决方案,您应该始终尽量避免使用它。


在.net Core (. net 5 +) Thread.Abort ()中,现在将抛出一个 PlatformNotSupportedException

有点强调了“不推荐”的观点。

虽然中止一个线程是可能的,但在实践中,这样做几乎总是一个非常糟糕的主意。终止一个线程意味着线程没有机会在自己之后进行清理,留下未删除的资源和处于未知状态的内容。

实际上,如果中止一个线程,那么只能在终止进程的同时中止。遗憾的是,太多人认为 ThreadAbort 是停止某些操作并继续执行的可行方法,但事实并非如此。

由于 Tasks 是作为线程运行的,因此可以对它们调用 ThreadAbort,但是与通用线程一样,除非万不得已,否则几乎不想这样做。

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

例子;

您有一个简单的 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


});

在本例中,阻塞调用需要2分钟才能完成。现在我将 MaxDegreeOfParallelism 设置为 ProcessorCount。IListOfItems 中有1000个要处理的项。

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

然而,由于某些原因,用户需要关闭应用程序,比如说他们关闭了表单。 这20个线程将继续执行,直到处理完所有1000个项目。这在这个场景中并不理想,因为应用程序不会按照用户的期望退出,而是继续在幕后运行,从任务管理器中可以看出这一点。

假设用户再次尝试重新构建应用程序(VS 2010) ,它报告 exe 被锁定,然后他们将不得不进入任务管理器来关闭它,或者只是等待,直到所有1000个项目都被处理。

我不会责怪你说,但当然!我应该使用 取消令牌来源对象取消这些线程并调用 Cancel... ... 但是这样做有一些问题。净值4.0。首先,这仍然不会导致线程终止,这将提供一个中止异常,然后线程终止,所以应用程序域将需要等待线程正常完成,这意味着等待最后一个阻塞调用,这将是最后一个运行的迭代(线程) ,最终得到调用 po.CancellationToken.ThrowIfCancellationRequested。 在这个例子中,这意味着应用程序域仍然可以存活最多2分钟,即使表单已经关闭并取消调用。

请注意,在 CcellationTokenSource 上调用 Cancel 不会在处理线程上引发异常,这将实际中断阻塞调用,类似于线程中止并停止执行。当所有其他线程(并发迭代)最终完成并返回时,将缓存一个异常,以便将该异常引发到初始线程(其中声明了循环)中。

我选择了 没有,以便在 CcellationTokenSource 对象上使用 取消选项。这是一种浪费,并且可以说违反了众所周知的通过异常控制代码流的反模式。

相反,可以说实现一个简单的线程安全属性(即 Bool stop Execting)“更好”。然后在循环中,检查 stop Execting 的值,如果该值被外部影响设置为 true,我们可以采用另一条路径来优雅地关闭。因为我们不应该调用取消,这排除了检查 取消令牌来源,否则将是另一个选项。

如下所示,if 条件在循环中是适当的;

{ loop State. Stop () ; return; }

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

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

我们无法改变这样一个事实,即应用程序域将始终保持活动状态,并且只有在所有前台线程完成后才会被释放。这意味着在等待循环中发出的任何阻塞调用完成时都会有一个延迟。

只有真正的线程中止才能中断阻塞调用,而且您必须在中止线程的异常处理程序中尽可能减轻使系统处于不稳定/未定义状态的影响,这是毫无疑问的。这是否合适是由程序员根据他们选择维护的资源句柄以及在线程的 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
}
}


});

但这仍然会导致声明线程中出现异常。

考虑到所有的因素,使用 allel.loop 构造的中断阻塞调用可能是一个关于选项的方法,从而避免使用库中比较模糊的部分。但是,为什么在声明方法中没有取消和避免抛出异常的选项,我认为这可能是一个疏忽。

但是我可以用同样的方法中止一个任务(在.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();
}


}

免责声明 : 不要这样做。

下面是一个不要做的例子:

 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

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

这是一个简单的代码片段,用于中止 取消令牌来源中永无止境的任务。

每个人都知道(希望)终止线程是不好的。问题在于,当您没有一段正在调用的代码时。如果这段代码在 do/while 无限循环中运行,它本身调用一些本机函数,等等,那么你基本上就卡住了。当这种情况发生在您自己的代码终止、停止或处理调用中时,可以开始向坏人开枪(这样您自己就不会变成坏人)。

因此,不管怎样,我已经编写了这两个阻塞函数,它们使用自己的本机线程,而不是来自池的线程或由 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);

可以通过在自己控制的线程上运行任务并中止该线程来“中止”任务。这会导致任务以 ThreadAbortException的错误状态完成。可以使用自定义任务调度程序控制线程创建,如 这个答案所述。请注意,关于中止线程的警告适用于。

(如果不确保任务是在自己的线程上创建的,则中止任务将中止线程池线程或初始化任务的线程,这两种情况通常都不希望发生。)

我在使用 Excel 的 Application.Workbooks时遇到了类似的问题。

如果应用程序繁忙,则该方法将永久挂起。我的方法很简单,就是尝试在任务中获取它,然后等待,如果花费的时间太长,我就让任务运行,然后离开(这没有什么坏处,当用户完成任何忙碌的事情时,Excel 就会解冻)。

在这种情况下,不可能使用取消令牌。优点是我不需要过多的代码,不需要中止线程等等。

public static List<Workbook> GetAllOpenWorkbooks()
{
//gets all open Excel applications
List<Application> applications = GetAllOpenApplications();


//this is what we want to get from the third party library that may freeze
List<Workbook> books = null;


//as Excel may freeze here due to being busy, we try to get the workbooks asynchronously
Task task = Task.Run(() =>
{
try
{
books = applications
.SelectMany(app => app.Workbooks.OfType<Workbook>()).ToList();
}
catch { }
});
    

//wait for task completion
task.Wait(5000);
return books; //handle outside if books is null
}

这是我对@Simon-Mourier 提出的一个想法的实现,使用的是 dotnet 线程,简短的代码:

    public static bool RunWithAbort(this Action action, int milliseconds)
{
if (action == null) throw new ArgumentNullException(nameof(action));


var success = false;
var thread = new Thread(() =>
{
action();
success = true;
});
thread.IsBackground = true;
thread.Start();
thread.Join(milliseconds);
thread.Abort();


return success;
}