Подтвердить что ты не робот

Как reset CancellationTokenSource и отладить многопоточность с VS2010?

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

  • Вопрос 1: Как reset Источник 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();
}
4b9b3361

Ответ 1

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

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

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

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

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

Ответ 2

В разделе "Отладка" > "Окна в визуальной студии" вам нужно посмотреть окно потоков, окно вызова и окно задач паралелла.

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

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

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

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

Как уже было отмечено, вы должны делать какие-либо обновления для своего ui из пользовательского потока. Вы можете, например, использовать "Invoke" для элемента управления для этого, см. @CodeInChaos awnser.

-edit2 -

Я прочитал ваши комментарии на awnser @CodeInChaos, и вот один из способов сделать это более похожим образом на TPL: Прежде всего вам нужно получить экземпляр TaskScheduler, который будет запускать задачи в потоке пользовательского интерфейса. вы можете сделать это, объявив TaskScheduler в вашем ui-классе, названном, например, 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

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

Существует и второй способ сделать это, используя Apis TaskContinuation. Однако мы не можем использовать 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 и продолжениях здесь

Ответ 3

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

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

Exceptions Window

Ответ 4

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

Когда вы начинаете и останавливаете поток, вы должны быть уверены, что делаете это в потоковом режиме. Вы также должны будете перезапустить поток после его остановки. В этом примере я использовал VS 2010 в веб-приложении. Во всяком случае, вот сначала html. Ниже это код, стоящий сначала на vb.net, а затем на С#. Имейте в виду, что версия С# - это перевод.

Сначала 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

Теперь С#:

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

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

Ответ 5

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

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