Как сбросить CancellationTokenSource и отладить многопоточность с VS2010? - PullRequest
21 голосов
/ 29 мая 2011

Я использовал CancellationTokenSource для предоставления функции, позволяющей пользователю отменить длительное действие.Однако после того, как пользователь применяет первую отмену, дальнейшее дальнейшее действие больше не работает.Я предполагаю, что статус CancellationTokenSource был установлен на Cancel, и я хочу знать, как сбросить его обратно.

  • Вопрос 1: Как сбросить CancellationTokenSource после первого использования?

  • Вопрос 2: Как отладить многопоточность в VS2010?Если я запускаю приложение в режиме отладки, я вижу следующее исключение для оператора

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

InvalidOperaationException не обрабатывается кодом пользователя. Недопустимая операция в потоке: ControlДоступ к «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();
}

Ответы [ 5 ]

34 голосов
/ 29 мая 2011

Вопрос 1> Как сбросить CancellationTokenSource после первого использования?

Если вы отмените его, то он будет отменен и не может быть восстановлен.Вам нужен новый CancellationTokenSource.A CancellationTokenSource не какая-то фабрика.Это просто владелец одного токена.ИМО должен был называться CancellationTokenOwner.

Вопрос 2> Как отладить многопоточность в VS2010?Если я запускаю приложение в режиме отладки, я вижу следующее исключение для оператора

, который не имеет ничего общего с отладкой.Вы не можете получить доступ к графическому элементу управления из другого потока.Для этого вам нужно использовать Invoke.Я думаю, вы видите проблему только в режиме отладки, потому что некоторые проверки отключены в режиме выпуска.Но ошибка все еще там.

Parallel.ForEach(files, parOpts, (currentFile) =>
{
  ...  
  this.Text =  ...;// <- this assignment is illegal
  ...
});
2 голосов
/ 29 мая 2011

Под окнами Debug> в Visual Studio вы можете посмотреть на окно потоков, окно стека вызовов и окно задач paralell.

Когда отладчик останавливается для получаемого исключения, вы можете посмотреть в окне callstack, чтобы увидеть, какой поток выполняет вызов и откуда этот поток приходит.

-editна основе опубликованного скриншота -

вы можете щелкнуть правой кнопкой мыши в стеке вызовов и выбрать «показать внешний код», чтобы точно увидеть, что происходит в стеке, но «внешний код» означает «где-то в структуре», поэтомуэто может или не может быть полезно (хотя обычно я нахожу это интересным :))

Из вашего скриншота также видно, что вызов выполняется из потока пула потоков.Если вы посмотрите в окно потоков, то увидите, что у одного из них есть желтая стрелка.Это поток, в котором мы сейчас выполняем, и где создается исключение.Название этого потока - «Рабочий поток», и это означает, что он приходит из пула потоков.

Как уже отмечалось, вы должны вносить любые изменения в ваш пользовательский интерфейс из пользовательского потока.Например, вы можете использовать «Invoke» на элементе управления, см. @CodeInChaos awnser.

-edit2-

Я прочитал ваши комментарии к @CodeInChaos awnser, и вот один из способов сделать это более похожим на TPL способом: Прежде всего, вам нужно получить экземплярTaskScheduler, который будет запускать задачи в потоке пользовательского интерфейса.Вы можете сделать это, объявив TaskScheduler в своем пользовательском интерфейсе с именем, например, uiScheduler и в конструкторе, установив его на TaskScheduler.FromCurrentSynchronizationContext();

Теперь, когда у вас есть это, вы можете сделать новую задачу, котораяобновляет пользовательский интерфейс:

 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

Обратите внимание, что мы передаем планировщик задач в задачу при запуске.

Существует также второй способ сделать это, использующий apis TaskContinuation.Однако мы больше не можем использовать Paralell.Foreach, но мы будем использовать обычный foreach и задачи.ключ в том, что задача позволяет вам запланировать другую задачу, которая будет выполняться после выполнения первой задачи.Но вторая задача не обязательно должна выполняться на том же планировщике , и это очень полезно для нас прямо сейчас, поскольку мы хотим выполнить некоторую работу в фоновом режиме и затем обновить пользовательский интерфейс:

  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
  }

То, что мы делаем здесь, - это создание новой задачи в каждом цикле, которая будет выполнять эту работу и генерировать сообщение, затем мы подключаем другую задачу, которая фактически обновит пользовательский интерфейс.

Вы можете прочитатьподробнее о ContinueWith и продолжениях здесь

1 голос
/ 29 мая 2011

Для отладки я определенно рекомендую использовать окно Parallel Stacks вместе с окном Threads.Используя окно параллельных стеков, вы можете видеть стеки вызовов всех потоков на одном комбинированном дисплее.Вы можете легко перемещаться между потоками и точками в стеке вызовов.Окно параллельных стеков и потоков находится в Debug> Windows.

Также еще одна вещь, которая действительно может помочь в отладке, - это включить выброс исключений CLR, когда они выбрасываются И пользователь не обрабатывается.Для этого перейдите в «Отладка> Исключения» и включите обе опции -

Exceptions Window

0 голосов
/ 07 марта 2016

Я использую класс, в котором я обманываю метод 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();

У него ужасный ад, но он работает.Я должен в конечном итоге найти лучшее решение.

0 голосов
/ 14 августа 2015

Спасибо за вашу помощь с потоками выше здесь.Это помогло мне в моих исследованиях.Я потратил много времени, пытаясь понять это, и это было нелегко.Разговор с другом также очень помог.

Когда вы запускаете и останавливаете поток, вы должны быть уверены, что делаете это безопасно.Вы также должны иметь возможность перезапустить поток после его остановки.В этом примере я использовал 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;
    }
}

Наслаждайтесь кодом!

...