Search code examples
c#.netmultithreadingkillabort

How to stop a thread if thread takes too long


I have a situation that i export data to a file and what i have been asked to do is to provide a cancel button which on click will stop the export if it takes too much time to export.

I started exporting to the file in a thread. And i try to abort the thread on the button click. But it do not work.

I searched on Google and i found that abort() is not recommended. But what else should I choose to achieve it?

My current code is:

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

Solution

  • Aborting a thread is a bad idea, especially when dealing with files. You won't have a chance to clean up half-written files or clean-up inconsistent state.

    It won't harm the .NET Runtime bat it can hurt your own application eg if the worker method leaves global state, files or database records in an inconsistent state.

    It's always preferable to use cooperative cancellation - the thread periodically checks a coordination construct like a ManualResetEvent or CancellationToken. You can't use a simple variable like a Boolean flag, as this can lead to race conditions, eg if two or more threads try to set it at the same time.

    You can read about cancellation in .NET in the Cancellation in Managed Threads section of MSDN.

    The CancellationToken/CancellationTokenSource classes were added in .NET 4 to make cancellation easier that passing around events.

    In your case, you should modify your DataTableToCsv to accept a CancellationToken. That token is generated by a CancellationTokenSource class.

    When you call CancellationTokenSource.Cancel the token's IsCancellationRequested property becomes true. Your DataTableToCsv method should check this flag periodically. If it's set, it should exit any loops, delete any inconsistent files etc.

    Timeouts are directly supported with CancelAfter. Essentially, CancelAfter starts a timer that will fire Cancel when it expires.

    Your code could look like this:

    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 should contain code similar to this:

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

    You can clean up your code quite a bit by using tasks instead of raw threads:

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

    You could also speed it up by using asynchronous operations, eg to read data from the database or write to text files without blocking or using threads. Windows IO (both file and network) is asynchronous at the driver level. Methods like File.WriteLineAsync don't use threads to write to a file.

    Your Export button handler could become :

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

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