28

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();
}
abatishchev
  • 98,240
  • 88
  • 296
  • 433
q0987
  • 34,938
  • 69
  • 242
  • 387
  • 8
    If you cancel it, then it's cancelled and can't be restored. You need a new CancellationTokenSource. – CodesInChaos May 29 '11 at 15:12
  • 1
    I found an article here http://blogs.msdn.com/b/pfxteam/archive/2009/05/22/9635790.aspx that indicates we just cannot reset it. The solution is to create a new CancellationTokenSource every time. That answers my first question. However, I still need helps for my second question. --- thx – q0987 May 29 '11 at 15:16
  • The first one is answered/solved, the 2nd question is a 100-times duplicate. – H H May 29 '11 at 15:43
  • 1
    @Henk, I just don't want to post my source code twice here. -thx – q0987 May 29 '11 at 16:31
  • 1
    Not a very good reason, esp since you didn't need most of that code anyway. – H H May 29 '11 at 17:14

5 Answers5

52

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
  ...
});
CodesInChaos
  • 106,488
  • 23
  • 218
  • 262
  • do you mean the original code contain errors? Please specify if possible. The code is copied from Prof C#2010 and .NET 4.0 page 766. -- thx – q0987 May 29 '11 at 16:33
  • @q0987 Yes, that code is broken. And that's exactly what the exception tried to tell you. Accessing WinForms controls from another thread is a typically beginner mistake. – CodesInChaos May 29 '11 at 16:39
  • May you show me how to fix? This wrong method is introduced in the book and I need to know the correct way to do it. --thx – q0987 May 29 '11 at 16:44
  • using `Control.Invoke` is the traditional answer. – CodesInChaos May 29 '11 at 16:45
  • As mentioned in the book pp761 "With the release of .NET 4.0, you are provided with a brand new parallel programming library. Using the types of System.Threading.Tasks, you can build fine-grained, scalable parallel code without having to work directly with threads or the thread pool." I would like to know how to fix the problem with the solution based on TPL. --thx – q0987 May 29 '11 at 16:47
  • Wow. I spent two hours on this thinking it's a factory. – Dan Abramov Nov 09 '12 at 00:44
  • @DanAbramov Shows, once more, why using good names for your types and classes is very important. – CodesInChaos Nov 09 '12 at 13:22
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

aL3891
  • 6,205
  • 3
  • 33
  • 37
1

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

cchamberlain
  • 17,444
  • 7
  • 59
  • 72
0

Thank you for all your help with threading above here. It did help me in my research. I spent a lot of time trying to figure this one out and it was not easy. Talking to a friend helped a lot as well.

When you start and stop a thread you have to be sure to do it in a thread safe manner. You also have to be able to restart the thread after you stop it. In this example I used VS 2010 in a web application. Anyway here is the html first. Below that is the code behind first in vb.net and then in C#. Keep in mind that the C# version is a translation.

First the html:

<%@ Page Language="vb" AutoEventWireup="false" CodeBehind="Directory4.aspx.vb" Inherits="Thread_System.Directory4" %>


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head id="Head1" runat="server">
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
    <asp:ScriptManager ID="ScriptManager1" runat="server"></asp:ScriptManager>

    <div>

        <asp:Button ID="btn_Start" runat="server" Text="Start" />&nbsp;&nbsp;
        <asp:Button ID="btn_Stop" runat="server" Text="Stop" />
        <br />
        <asp:Label ID="lblMessages" runat="server"></asp:Label>
        <asp:Timer ID="Timer1" runat="server" Enabled="False" Interval="3000">
        </asp:Timer>
        <br />
    </div>


    </form>
</body>
</html>

Next is the vb.net:

Imports System
Imports System.Web
Imports System.Threading.Tasks
Imports System.Threading

Public Class Directory4
    Inherits System.Web.UI.Page

    Private Shared cts As CancellationTokenSource = Nothing
    Private Shared LockObj As New Object
    Private Shared SillyValue As Integer = 0
    Private Shared bInterrupted As Boolean = False
    Private Shared bAllDone As Boolean = False

    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    End Sub


    Protected Sub DoStatusMessage(ByVal Msg As String)

        Me.lblMessages.Text = Msg
        Debug.Print(Msg)
    End Sub

    Protected Sub btn_Start_Click(sender As Object, e As EventArgs) Handles btn_Start.Click

        If Not IsNothing(CTS) Then
            If Not cts.IsCancellationRequested Then
                DoStatusMessage("Please cancel the running process first.")
                Exit Sub
            End If
            cts.Dispose()
            cts = Nothing
            DoStatusMessage("Plase cancel the running process or wait for it to complete.")
        End If
        bInterrupted = False
        bAllDone = False
        Dim ncts As New CancellationTokenSource
        cts = ncts

        ' Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(New WaitCallback(AddressOf DoSomeWork), cts.Token)
        DoStatusMessage("This Task has now started.")

        Timer1.Interval = 1000
        Timer1.Enabled = True
    End Sub

    Protected Sub StopThread()
        If IsNothing(cts) Then Exit Sub
        SyncLock (LockObj)
            cts.Cancel()
            System.Threading.Thread.SpinWait(1)
            cts.Dispose()
            cts = Nothing
            bAllDone = True
        End SyncLock


    End Sub

    Protected Sub btn_Stop_Click(sender As Object, e As EventArgs) Handles btn_Stop.Click
        If bAllDone Then
            DoStatusMessage("Nothing running. Start the task if you like.")
            Exit Sub
        End If
        bInterrupted = True
        btn_Start.Enabled = True

        StopThread()

        DoStatusMessage("This Canceled Task has now been gently terminated.")
    End Sub


    Sub Refresh_Parent_Webpage_and_Exit()
        '***** This refreshes the parent page.
        Dim csname1 As [String] = "Exit_from_Dir4"
        Dim cstype As Type = [GetType]()

        ' Get a ClientScriptManager reference from the Page class.
        Dim cs As ClientScriptManager = Page.ClientScript

        ' Check to see if the startup script is already registered.
        If Not cs.IsStartupScriptRegistered(cstype, csname1) Then
            Dim cstext1 As New StringBuilder()
            cstext1.Append("<script language=javascript>window.close();</script>")
            cs.RegisterStartupScript(cstype, csname1, cstext1.ToString())
        End If
    End Sub


    'Thread 2: The worker
    Shared Sub DoSomeWork(ByVal token As CancellationToken)
        Dim i As Integer

        If IsNothing(token) Then
            Debug.Print("Empty cancellation token passed.")
            Exit Sub
        End If

        SyncLock (LockObj)
            SillyValue = 0

        End SyncLock


        'Dim token As CancellationToken = CType(obj, CancellationToken)
        For i = 0 To 10

            ' Simulating work.
            System.Threading.Thread.Yield()

            Thread.Sleep(1000)
            SyncLock (LockObj)
                SillyValue += 1
            End SyncLock
            If token.IsCancellationRequested Then
                SyncLock (LockObj)
                    bAllDone = True
                End SyncLock
                Exit For
            End If
        Next
        SyncLock (LockObj)
            bAllDone = True
        End SyncLock
    End Sub

    Protected Sub Timer1_Tick(sender As Object, e As System.EventArgs) Handles Timer1.Tick
        '    '***** This is for ending the task normally.


        If bAllDone Then
            If bInterrupted Then
                DoStatusMessage("Processing terminated by user")
            Else

                DoStatusMessage("This Task has has completed normally.")
            End If

            'Timer1.Change(System.Threading.Timeout.Infinite, 0)
            Timer1.Enabled = False
            StopThread()

            Exit Sub
        End If
        DoStatusMessage("Working:" & CStr(SillyValue))

    End Sub
End Class

Now the C#:

using Microsoft.VisualBasic;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Web;
using System.Threading.Tasks;
using System.Threading;

public class Directory4 : System.Web.UI.Page
{

    private static CancellationTokenSource cts = null;
    private static object LockObj = new object();
    private static int SillyValue = 0;
    private static bool bInterrupted = false;

    private static bool bAllDone = false;

    protected void Page_Load(object sender, System.EventArgs e)
    {
    }



    protected void DoStatusMessage(string Msg)
    {
        this.lblMessages.Text = Msg;
        Debug.Print(Msg);
    }


    protected void btn_Start_Click(object sender, EventArgs e)
    {
        if ((cts != null)) {
            if (!cts.IsCancellationRequested) {
                DoStatusMessage("Please cancel the running process first.");
                return;
            }
            cts.Dispose();
            cts = null;
            DoStatusMessage("Plase cancel the running process or wait for it to complete.");
        }
        bInterrupted = false;
        bAllDone = false;
        CancellationTokenSource ncts = new CancellationTokenSource();
        cts = ncts;

        // Pass the token to the cancelable operation.
        ThreadPool.QueueUserWorkItem(new WaitCallback(DoSomeWork), cts.Token);
        DoStatusMessage("This Task has now started.");

        Timer1.Interval = 1000;
        Timer1.Enabled = true;
    }

    protected void StopThread()
    {
        if ((cts == null))
            return;
        lock ((LockObj)) {
            cts.Cancel();
            System.Threading.Thread.SpinWait(1);
            cts.Dispose();
            cts = null;
            bAllDone = true;
        }


    }

    protected void btn_Stop_Click(object sender, EventArgs e)
    {
        if (bAllDone) {
            DoStatusMessage("Nothing running. Start the task if you like.");
            return;
        }
        bInterrupted = true;
        btn_Start.Enabled = true;

        StopThread();

        DoStatusMessage("This Canceled Task has now been gently terminated.");
    }


    public void Refresh_Parent_Webpage_and_Exit()
    {
        //***** This refreshes the parent page.
        String csname1 = "Exit_from_Dir4";
        Type cstype = GetType();

        // Get a ClientScriptManager reference from the Page class.
        ClientScriptManager cs = Page.ClientScript;

        // Check to see if the startup script is already registered.
        if (!cs.IsStartupScriptRegistered(cstype, csname1)) {
            StringBuilder cstext1 = new StringBuilder();
            cstext1.Append("<script language=javascript>window.close();</script>");
            cs.RegisterStartupScript(cstype, csname1, cstext1.ToString());
        }
    }


    //Thread 2: The worker
    public static void DoSomeWork(CancellationToken token)
    {
        int i = 0;

        if ((token == null)) {
            Debug.Print("Empty cancellation token passed.");
            return;
        }

        lock ((LockObj)) {
            SillyValue = 0;

        }


        //Dim token As CancellationToken = CType(obj, CancellationToken)

        for (i = 0; i <= 10; i++) {
            // Simulating work.
            System.Threading.Thread.Yield();

            Thread.Sleep(1000);
            lock ((LockObj)) {
                SillyValue += 1;
            }
            if (token.IsCancellationRequested) {
                lock ((LockObj)) {
                    bAllDone = true;
                }
                break; // TODO: might not be correct. Was : Exit For
            }
        }
        lock ((LockObj)) {
            bAllDone = true;
        }
    }

    protected void Timer1_Tick(object sender, System.EventArgs e)
    {
        //    '***** This is for ending the task normally.


        if (bAllDone) {
            if (bInterrupted) {
                DoStatusMessage("Processing terminated by user");

            } else {
                DoStatusMessage("This Task has has completed normally.");
            }

            //Timer1.Change(System.Threading.Timeout.Infinite, 0)
            Timer1.Enabled = false;
            StopThread();

            return;
        }
        DoStatusMessage("Working:" + Convert.ToString(SillyValue));

    }
    public Directory4()
    {
        Load += Page_Load;
    }
}

Enjoy the code!

Donald
  • 11
  • 2
0

I am using a class where I trick a CancellationTokenSource an ugly way:

//.ctor
{
    ...
    registerCancellationToken();
}

public CancellationTokenSource MyCancellationTokenSource
{
    get;
    private set;
}

void registerCancellationToken() {
    MyCancellationTokenSource= new CancellationTokenSource();
    MyCancellationTokenSource.Token.Register(() => {
        MyCancellationTokenSource.Dispose();
        registerCancellationToken();
    });
}

// Use it this way:

MyCancellationTokenSource.Cancel();

It is ugly has hell, but it works. I must eventually find a better solution.

Larry
  • 17,605
  • 9
  • 77
  • 106