How to reset the CancellationTokenSource and debug the multithread with VS2010?

20,510

Solution 1

Question 1> How to reset the CancellationTokenSource after the first time usage?

If you cancel it, then it's cancelled and can't be restored. You need a new CancellationTokenSource. A CancellationTokenSource isn't some kind of factory. It's just the owner of a single token. IMO it should have been called CancellationTokenOwner.

Question 2> How to debug the multithread in VS2010? If I run the application in debug mode, I can see the following exception for the statement

That has nothing to do with debugging. You can't access a gui control from another thread. You need to use Invoke for that. I guess you see the problem only in debug mode because some checks are disabled in release mode. But the bug is still there.

Parallel.ForEach(files, parOpts, (currentFile) =>
{
  ...  
  this.Text =  ...;// <- this assignment is illegal
  ...
});

Solution 2

Under Debug > windows in visual studio there you'll want to look at the threads window, the callstack window and the paralell tasks window.

When the debugger breaks for the exception you're getting, you can look at the callstack window to see what thread is making the call and from where that thread is coming from.

-edit based on posted screenshot-

you can right click in the call stack and select 'show external code' to see exactly what is going on in the stack, but 'external code' means 'somewhere in the framework' so it may or may not be useful (i usually find it interesting though :) )

From your screenshot we can also see that the call is beeing made from a thread pool thread. If you look at the threads window, you'll see one of them has a yellow arrow. Thats the thread we're currently executing on and where the exception is beeing thrown. The name of this thread is 'Worker Thread' and that means its coming from the thread pool.

As has already been noted you must make any updates to your ui from the user thread. You can for example use the ´Invoke´ on the control for this, see @CodeInChaos awnser.

-edit2-

I read through your comments on @CodeInChaos awnser and here is one way to do it in a more TPL like way: First of all you need to get hold of an instance of a TaskScheduler that will run tasks on the UI thread. you can do this by declaring a TaskScheduler in you ui-class named for example uiScheduler and in the constructor setting it to TaskScheduler.FromCurrentSynchronizationContext();

Now that you have it, you can make a new task that updates the ui:

 Task.Factory.StartNew( ()=> String.Format("Processing {0} on thread {1}", filename,Thread.CurrentThread.ManagedThreadId),
 CancellationToken.None,
 TaskCreationOptions.None,
 uiScheduler ); //passing in our uiScheduler here will cause this task to run on the ui thread

Note that we pass the task scheduler to the task when we start it.

There is also a second way to do this, that uses the TaskContinuation apis. However we cant use Paralell.Foreach anymore, but we'll use a regular foreach and tasks. the key is that a task allows you to schedule another task that will run once the first task is done. But the second task does not have to run on the same scheduler and that is very useful for us right now since we want to do some work in the background and then update the ui:

  foreach( var currectFile in files ) {
    Task.Factory.StartNew( cf => { 
      string filename = Path.GetFileName( cf ); //make suse you use cf here, otherwise you'll get a race condition
      using( Bitmap bitmap = new Bitmap( cf ) ) {// again use cf, not currentFile
        bitmap.RotateFlip( RotateFlipType.Rotate180FlipNone );
        bitmap.Save( Path.Combine( newDir, filename ) );
        return string.Format( "Processed {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadId );
      }
    }, currectFile, cancelToken.Token ) //we pass in currentFile to the task we're starting so that each task has their own 'currentFile' value
    .ContinueWith( t => this.Text = t.Result, //here we update the ui, now on the ui thread..
                   cancelToken.Token, 
                   TaskContinuationOptions.None, 
                   uiScheduler ); //..because we use the uiScheduler here
  }

What we're doing here is making a new task each loop that will do the work and generate the message, then we're hooking on another task that will actually update the ui.

You can read more about ContinueWith and continuations here

Solution 3

For debugging I definitely recommend using the Parallel Stacks window in conjunction with the Threads window. Using the parallel stacks window you can see the callstacks of all threads on one combined display. You can easily jump between threads and points in the call stack. The parallel stacks and threads window are found in Debug > Windows.

Also another thing that can really help in debugging is to turn on throwing of CLR exceptions both when they are thrown AND user-unhandled. To do this go to Debug > Exceptions, and enable both options -

Exceptions Window

Share:
20,510
q0987
Author by

q0987

Updated on August 04, 2022

Comments

  • q0987
    q0987 almost 2 years

    I have used CancellationTokenSource to provide a function so that the user can cancel the lengthy action. However, after the user applies the first cancellation, the later further action doesn't work anymore. My guess is that the status of CancellationTokenSource has been set to Cancel and I want to know how to reset it back.

    • Question 1: How to reset the CancellationTokenSource after the first time usage?

    • Question 2: How to debug the multithread in VS2010? If I run the application in debug mode, I can see the following exception for the statement

      this.Text = string.Format("Processing {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadId);
      

    InvalidOperaationException was unhandled by user code Cross-thread operation not valid: Control 'MainForm' accessed from a thread other than the thread it was created on.

    Thank you.

    private CancellationTokenSource cancelToken = new CancellationTokenSource();
    
    private void button1_Click(object sender, EventArgs e)
    {
        Task.Factory.StartNew( () =>
        {
            ProcessFilesThree();
        });
    }
    
    private void ProcessFilesThree()
    {
        ParallelOptions parOpts = new ParallelOptions();
        parOpts.CancellationToken = cancelToken.Token;
        parOpts.MaxDegreeOfParallelism = System.Environment.ProcessorCount;
    
        string[] files = Directory.GetFiles(@"C:\temp\In", "*.jpg", SearchOption.AllDirectories);
        string newDir = @"C:\temp\Out\";
        Directory.CreateDirectory(newDir);
    
        try
        {
            Parallel.ForEach(files, parOpts, (currentFile) =>
            {
                parOpts.CancellationToken.ThrowIfCancellationRequested();
    
                string filename = Path.GetFileName(currentFile);
    
                using (Bitmap bitmap = new Bitmap(currentFile))
                {
                    bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone);
                    bitmap.Save(Path.Combine(newDir, filename));
                    this.Text =  tring.Format("Processing {0} on thread {1}",  filename, Thread.CurrentThread.ManagedThreadId);
                }
            });
    
            this.Text = "All done!";
        }
        catch (OperationCanceledException ex)
        {
            this.Text = ex.Message;                             
        }
    }
    
    private void button2_Click(object sender, EventArgs e)
    {
        cancelToken.Cancel();
    }