24

我使用 CancellationTokenSource 提供了一个功能,以便用户可以取消冗长的操作。但是,在用户应用第一次取消后,后面的进一步操作不再起作用。我的猜测是 CancellationTokenSource 的状态已设置为 Cancel,我想知道如何将其重置。

  • 问题一:第一次使用后如何重置CancellationTokenSource?

  • 问题2:如何调试VS2010中的多线程?如果我在调试模式下运行应用程序,我可以看到语句的以下异常

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

用户代码未处理 InvalidOperaationException 跨线程操作无效:控件“MainForm”从创建它的线程以外的线程访问。

谢谢你。

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

5 回答 5

46

问题1>第一次使用后如何重置CancellationTokenSource?

如果您取消它,那么它将被取消并且无法恢复。你需要一个新的CancellationTokenSource. ACancellationTokenSource不是某种工厂。它只是一个令牌的所有者。IMO 它应该被称为CancellationTokenOwner.

问题2> 如何调试VS2010中的多线程?如果我在调试模式下运行应用程序,我可以看到语句的以下异常

这与调试无关。您不能从另一个线程访问 gui 控件。你需要使用Invoke它。我猜你只在调试模式下看到问题,因为在发布模式下禁用了一些检查。但是错误仍然存​​在。

Parallel.ForEach(files, parOpts, (currentFile) =>
{
  ...  
  this.Text =  ...;// <- this assignment is illegal
  ...
});
于 2011-05-29T15:21:29.520 回答
2

在 Visual Studio 中的 Debug > windows 下,您需要查看线程窗口、调用堆栈窗口和并行任务窗口。

当调试器因您遇到的异常而中断时,您可以查看调用堆栈窗口以查看正在进行调用的线程以及该线程来自何处。

-根据发布的屏幕截图进行编辑-

您可以右键单击调用堆栈并选择“显示外部代码”以查看堆栈中发生的确切情况,但“外部代码”表示“框架中的某处”,因此它可能有用也可能没有用(我通常会发现虽然很有趣:))

从您的屏幕截图中,我们还可以看到调用是从线程池线程发出的。如果您查看线程窗口,您会看到其中一个带有黄色箭头。那就是我们当前正在执行的线程,也是抛出异常的地方。该线程的名称是“Worker Thread”,这意味着它来自线程池。

如前所述,您必须从用户线程对您的 ui 进行任何更新。例如,您可以为此使用控件上的“Invoke”,请参阅@CodeInChaos awnser。

-edit2-

我通读了您对@CodeInChaos awnser 的评论,这是一种更类似于 TPL 的方法:首先,您需要获取一个TaskScheduler将在 UI 线程上运行任务的实例。您可以通过TaskScheduler在您的 ui-class 中声明一个例如uiScheduler并在构造函数中将其设置为TaskScheduler.FromCurrentSynchronizationContext();

现在你有了它,你可以创建一个更新 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

请注意,我们在启动任务时将任务调度程序传递给它。

还有第二种方法可以做到这一点,即使用 TaskContinuation api。但是我们不能再使用 Paralell.Foreach,但我们将使用常规的 foreach 和任务。关键是任务允许您安排另一个任务,该任务将在第一个任务完成后运行。但是第二个任务不必在同一个调度程序上运行,这对我们现在非常有用,因为我们想在后台做一些工作,然后更新 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
  }

我们在这里所做的是在每个循环中创建一个新任务,该任务将完成工作并生成消息,然后我们将挂钩另一个将实际更新 ui 的任务。

您可以在此处阅读有关 ContinueWith 和延续的更多信息

于 2011-05-29T15:29:24.167 回答
1

对于调试,我绝对建议将 Parallel Stacks 窗口与 Threads 窗口结合使用。使用并行堆栈窗口,您可以在一个组合显示器上查看所有线程的调用堆栈。您可以轻松地在调用堆栈中的线程和点之间跳转。并行堆栈和线程窗口位于 Debug > Windows。

另一件真正有助于调试的事情是在抛出和用户未处理时都打开 CLR 异常的抛出。为此,请转到 Debug > Exceptions,并启用这两个选项 -

例外窗口

于 2011-05-29T15:39:30.657 回答
0

感谢您对上述线程的所有帮助。它确实帮助了我的研究。我花了很多时间试图弄清楚这一点,但这并不容易。与朋友交谈也有很大帮助。

当您启动和停止线程时,您必须确保以线程安全的方式进行。您还必须能够在停止线程后重新启动它。在此示例中,我在 Web 应用程序中使用了 VS 2010。无论如何,这里首先是html。下面是 vb.net 中的代码,然后是 C# 中的代码。请记住,C# 版本是翻译版本。

首先是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>

接下来是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

现在是 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;
    }
}

享受代码!

于 2015-08-14T05:14:44.363 回答
0

我正在使用一个类,我以一种丑陋的方式欺骗​​ CancellationTokenSource:

//.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();

这是丑陋的地狱,但它的工作原理。我最终必须找到更好的解决方案。

于 2016-03-07T19:42:39.440 回答