如果线程花费的时间太长,如何停止线程

How to stop a thread if thread takes too long

我有一种情况,我将数据导出到一个文件,我被要求做的是提供一个取消按钮,如果导出花费的时间太长,单击该按钮将停止导出。

我开始在线程中导出到文件。我尝试在单击按钮时中止线程。但它不起作用。

我在 Google 上搜索,发现不推荐使用 abort()。但是我还应该选择什么来实现呢?

我当前的代码是:

private void ExportButtonClick(object param)
{
    IList<Ur1R2_Time_Points> data = ct.T_UR.ToList();
    DataTable dtData = ExportHelper.ToDataTable(data);
    thread = new Thread(new ThreadStart(()=>ExportHelper.DataTableToCsv(dtData, "ExportFile.csv")));
    thread.SetApartmentState(ApartmentState.STA);
    thread.IsBackground = true;
    thread.Name = "PDF";
    thread.Start();
}

private void StopButtonClick(object param)
{
    if (thread.Name == "PDF")
    {
        thread.Interrupt();
        thread.Abort();
    }
}

您可以使用布尔标志。为此使用易失性布尔值。

在帮助程序中执行如下操作:

 this.aborted = false;
 while(!finished && !aborted) {
      //process one row
 }

每当你想取消操作时,调用一个方法将 aborted 设置为 true:

 public void Abort() {
     this.aborted = true;
 }

阅读此处:https://msdn.microsoft.com/en-us/library/system.threading.threadabortexception(v=vs.110).aspx

When a call is made to the Abort method to destroy a thread, the common language runtime throws a ThreadAbortException. ThreadAbortException is a special exception that can be caught, but it will automatically be raised again at the end of the catch block. When this exception is raised, the runtime executes all the finally blocks before ending the thread. Because the thread can do an unbounded computation in the finally blocks or call Thread.ResetAbort to cancel the abort, there is no guarantee that the thread will ever end. If you want to wait until the aborted thread has ended, you can call the Thread.Join method. Join is a blocking call that does not return until the thread actually stops executing.

由于 Thread.Abort() 由另一个线程执行,它可能随时发生,当它发生时,目标线程会抛出 ThreadAbortException。

里面ExportHelper.DataTableToCsv:

catch(ThreadAbortException e) {
    Thread.ResetAbort();
}

StopButtonClick

if (thread.Name == "PDF")
{
    thread.Interrupt();
    thread.Join();
}

要停止一个线程,您有 Thread.Abort 一个选项。但是,因为此方法在目标线程上被另一个线程执行时会抛出 ThreadAbortException。 这是不推荐的。 停止线程的第二个选项是使用目标线程和调用线程都可以访问的共享变量。 请参阅示例 ::

public static class Program
{
    public static void ThreadMethod(object o)
    {
        for (int i = 0; i < (int)o; i++)
        {
            Console.WriteLine("ThreadProc: { 0}", i);
            Thread.Sleep(0);
        }
    }
    public static void Main()
    {
        bool stopped = false;
        Thread t = new Thread(new ThreadStart(() =>
        {
            while (!stopped)
            {
                Console.WriteLine("Running...");
                Thread.Sleep(1000);
            }
        }));
        t.Start();
        Console.WriteLine("Press any key to exit");
        Console.ReadKey();
        stopped = true;
        t.Join();
    }
}

//来源 :: Book --> Programming in c#

中止线程不是一个好主意,尤其是在处理文件时。您将没有机会清理 half-written 个文件或 clean-up 个不一致的状态。

它不会损害 .NET 运行时 但它可能会损害您自己的应用程序,例如,如果 worker 方法使全局状态、文件或数据库记录处于不一致的状态。

使用合作 取消总是更可取 - 线程定期检查协调构造,如 ManualResetEvent or CancellationToken。您不能使用像布尔标志这样的简单变量,因为这会导致竞争条件,例如,如果两个或多个线程试图同时设置它。

您可以在 MSDN 的 Cancellation in Managed Threads 部分阅读有关在 .NET 中取消的信息。

.NET 4 中添加了 CancellationToken/CancellationTokenSource classes 以使取消比传递事件更容易。

对于您的情况,您应该修改 DataTableToCsv 以接受 CancellationToken. That token is generated by a CancellationTokenSource class。

当你调用 CancellationTokenSource.Cancel the token's IsCancellationRequested 时 属性 变为真。您的 DataTableToCsv 方法应定期检查此标志。如果已设置,它应该退出任何循环,删除任何不一致的文件等。

CancelAfter 直接支持超时。本质上,CancelAfter 启动一个计时器,该计时器在到期时会触发 Cancel

您的代码可能如下所示:

CancellationTokenSource _exportCts = null;

private void ExportButtonClick(object param)
{
    IList<Ur1R2_Time_Points> data = ct.T_UR.ToList();
    DataTable dtData = ExportHelper.ToDataTable(data);

    _exportCts=new CancellationTokenSource();
    var token=_exportCts.Token;

    thread = new Thread(new ThreadStart(()=>
            ExportHelper.DataTableToCsv(dtData, "ExportFile.csv",token)));
    thread.SetApartmentState(ApartmentState.STA);
    thread.IsBackground = true;
    thread.Name = "PDF";

    _exportCts.CancelAfter(10000);
    thread.Start();

}


private void StopButtonClick(object param)
{
    if (_exportCts!=null)
    {
        _exportCts.Cancel();
    }
}

DataTableToCsv 应该包含类似这样的代码:

foreach(var row in myTable)
{
    if (token.IsCancellationRequested)
    {
        break;
    }
    //else continue with processing
    var line=String.Join(",", row.ItemArray);
    writer.WriteLine(line);

}

您可以通过使用任务而不是原始线程来清理您的代码:

private async void ExportButtonClick(object param)
{
    IList<Ur1R2_Time_Points> data = ct.T_UR.ToList();
    DataTable dtData = ExportHelper.ToDataTable(data);

    _exportCts=new CancellationTokenSource();
    var token=_exportCts.Token;

    _exportCts.CancelAfter(10000);
    await Task.Run(()=> ExportHelper.DataTableToCsv(dtData, "ExportFile.csv",token)));
    MessageBox.Show("Finished");
}

您还可以通过使用异步操作来加快速度,例如从数据库读取数据或写入文本文件而不阻塞或使用线程。 Windows IO(文件和网络)在驱动程序级别是异步的。 File.WriteLineAsync 之类的方法不使用线程来写入文件。

您的导出按钮处理程序可能会变成:

private void ExportButtonClick(object param)
{
    IList<Ur1R2_Time_Points> data = ct.T_UR.ToList();
    DataTable dtData = ExportHelper.ToDataTable(data);

    _exportCts=new CancellationTokenSource();
    var token=_exportCts.Token;

    _exportCts.CancelAfter(10000);
    await Task.Run(async ()=> ExportHelper.DataTableToCsv(dtData, "ExportFile.csv",token)));
    MessageBox.Show("Finished");
}

DataTableToCsv

public async Task DataTableToCsv(DataTable table, string file,CancellationToken token)
{
...
    foreach(var row in myTable)
    {
        if (token.IsCancellationRequested)
        {
            break;
        }
        //else continue with processing
        var line=String.Join(",", row.ItemArray);
        await writer.WriteLineAsync(line);
    }