2011-05-29 9 views
19

He utilizado CancellationTokenSource para proporcionar una función para que el usuario pueda cancelar la acción prolongada. Sin embargo, después de que el usuario aplique la primera cancelación, , la acción futura posterior ya no funciona. Supongo que el estado de CancellationTokenSource se ha establecido en Cancelar y quiero saber cómo restaurarlo de nuevo .¿Cómo restablecer el CancellationTokenSource y depurar el multithread con VS2010?

  • Pregunta 1: ¿Cómo restablecer la CancelaciónTokenSource después del primer uso?

  • Pregunta 2: ¿Cómo depurar el multihilo en VS2010? Si ejecutar la aplicación en modo de depuración, que se puede ver la siguiente excepción de la declaración

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

InvalidOperaationException era controlada por el código de usuario operación de la Cruz-hilo no válido: 'MainForm' Control se accede desde un hilo diferente del hilo en el que se creó.

Gracias.

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

Si lo cancela, se cancela y no se puede restaurar. Necesita un nuevo CancellationTokenSource. – CodesInChaos

+1

Encontré un artículo aquí http://blogs.msdn.com/b/pfxteam/archive/2009/05/22/9635790.aspx que indica que simplemente no podemos restablecerlo. La solución es crear un nuevo CancellationTokenSource cada vez. Eso responde mi primera pregunta. Sin embargo, todavía necesito ayuda para mi segunda pregunta. --- thx – q0987

+0

Pruebe 1 pregunta por pregunta –

Respuesta

26

Pregunta 1> Cómo restablecer la CancellationTokenSource después del primer uso del tiempo?

Si lo cancela, se cancela y no se puede restaurar. Necesita un nuevo CancellationTokenSource. Un CancellationTokenSource no es un tipo de fábrica. Es solo el propietario de un solo token. IMO debería haberse llamado CancellationTokenOwner.

Pregunta 2> ¿Cómo depurar el multihilo en VS2010? Si ejecuto la aplicación en modo de depuración, puedo ver la siguiente excepción para la instrucción

Eso no tiene nada que ver con la depuración. No puede acceder a un control gui desde otro hilo. Necesitas usar Invoke para eso. Supongo que verá el problema solo en modo de depuración porque algunas comprobaciones están deshabilitadas en el modo de lanzamiento. Pero el error todavía está allí.

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

¿quiere decir que el código original contiene errores? Por favor especifique si es posible. El código se copia de Prof C# 2010 y .NET 4.0, página 766. - thx – q0987

+0

@ q0987 Sí, ese código está roto. Y eso es exactamente lo que la excepción intentó decirte. Acceder a los controles de WinForms desde otro subproceso es un error típico de principiante. – CodesInChaos

+0

¿Me puede mostrar cómo solucionarlo? Este método equivocado se presenta en el libro y necesito saber la forma correcta de hacerlo. --thx – q0987

2

Bajo Test> ventanas en Visual Studio no se tendrá que buscar en la ventana de hilos, la ventana de pila de llamadas y la ventana de tareas Paralelo de.

Cuando el depurador se rompe por la excepción que está recibiendo, puede mirar la ventana de la ventana de llamadas para ver qué hilo está haciendo la llamada y de dónde proviene ese hilo.

-edit basado en screenshot- publicado

puede hacer clic derecho en la pila de llamadas y seleccione 'Mostrar código externo' para ver exactamente lo que está pasando en la pila, pero 'código externo' significa 'en algún lugar de el marco 'por lo que puede o no ser útil (por lo general, me resulta interesante :))

De su captura de pantalla también podemos ver que la llamada se está realizando a partir de un hilo del grupo de subprocesos.Si miras la ventana de hilos, verás que uno de ellos tiene una flecha amarilla. Ese es el hilo que estamos ejecutando actualmente y donde se lanza la excepción. El nombre de este hilo es 'Worker Thread' y eso significa que proviene del grupo de hilos.

Como ya se ha mencionado, debe realizar cualquier actualización de su interfaz de usuario desde el hilo del usuario. Puede, por ejemplo, usar la 'Invocación' en el control para esto, ver @CodeInChaos awnser.

-edit2-

leí sus comentarios sobre @CodeInChaos awnser y aquí es una manera de hacerlo de una manera más TPL como: Primero de todo lo que necesita hacerse con una instancia de un TaskScheduler que ejecutará tareas en el hilo de UI. usted puede hacer esto declarando un TaskScheduler en que clase de interfaz de usuario llamado así por ejemplo uiScheduler y en el constructor ajustarlo a TaskScheduler.FromCurrentSynchronizationContext();

Ahora que lo tienes, puede realizar una nueva tarea que actualiza la interfaz de usuario:

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 

Tenga en cuenta que pasamos el planificador de tareas a la tarea cuando lo iniciamos.

También hay una segunda forma de hacerlo, que utiliza las apis TaskContinuation. Sin embargo, ya no podemos usar Paralell.Foreach, pero usaremos un foreach y tareas regulares. la clave es que una tarea le permite programar otra tarea que se ejecutará una vez que se haya completado la primera. Pero la segunda tarea no tiene que ejecutar en el mismo programador y que es muy útil para nosotros en este momento ya que queremos hacer un trabajo en segundo plano y luego actualizar la interfaz de usuario:

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 
    } 

Lo que' Re hacer aquí es hacer una nueva tarea cada ciclo que hará el trabajo y generará el mensaje, entonces estamos enganchando en otra tarea que realmente actualizará la interfaz de usuario.

Puede leer más sobre ContinueWith y continuaciones here

+0

por favor vea la captura de pantalla http://i53.tinypic.com/24o81z8.png -thx – q0987

1

Para la depuración Sin duda, recomiendo el uso de la ventana de pilas en paralelo junto con la ventana de Temas. Usando la ventana de pilas paralelas puede ver las llamadas de todos los hilos en una pantalla combinada. Puede saltar fácilmente entre los hilos y los puntos en la pila de llamadas. La ventana de pilas y subprocesos paralelos se encuentra en Debug> Windows.

También otra cosa que realmente puede ayudar en la depuración es activar el lanzamiento de excepciones CLR tanto cuando se lanzan como si no se manejan por el usuario. Para ello, accede a Depurar> Excepciones, y permitirá a ambas opciones -

Exceptions Window

0

Gracias por toda su ayuda con rosca por encima de aquí. Me ayudó en mi investigación. Pasé mucho tiempo tratando de resolver esto y no fue fácil. Hablar con un amigo también me ayudó mucho.

Cuando inicia y detiene un hilo, debe asegurarse de hacerlo de forma segura. También debe poder reiniciar el hilo después de detenerlo. En este ejemplo, utilicé VS 2010 en una aplicación web. De todos modos, aquí está el html primero. Debajo está el código que está primero en vb.net y luego en C#. Tenga en cuenta que la versión C# es una traducción.

En primer lugar el 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> 

El siguiente es el 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 

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

Disfrute el código!

0

estoy usando una clase en la que engañar a un CancellationTokenSource una manera fea:

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

es feo cuenta con el infierno, pero funciona. Eventualmente, debo encontrar una mejor solución.

Cuestiones relacionadas