c# - net - ¿Cómo restablecer el origen CancelToken y depurar el multiproceso con VS2010?
cancellationtoken source c# (5)
Pregunta 1> ¿Cómo restablecer el CancelTokenSource después del primer uso?
Si lo cancelas, entonces se cancela y no se puede restaurar. Necesitas un nuevo CancellationTokenSource
. Una CancellationTokenSource
no es algún tipo de fábrica. Es solo el dueño de un token único. En mi opinión, debería haber sido llamado CancellationTokenOwner
.
Pregunta 2> ¿Cómo depurar el multiproceso en VS2010? Si ejecuto la aplicación en modo de depuración, puedo ver la siguiente excepción para la declaración
Eso no tiene nada que ver con la depuración. No puedes acceder a un control de GUI desde otro hilo. Necesitas usar Invoke
para eso. Supongo que usted ve el problema solo en el modo de depuración porque algunas comprobaciones están desactivadas en el modo de liberación. Pero el error sigue ahí.
Parallel.ForEach(files, parOpts, (currentFile) =>
{
...
this.Text = ...;// <- this assignment is illegal
...
});
He utilizado CancelTokenSource para proporcionar una función para que el usuario pueda cancelar la acción prolongada. Sin embargo, después de que el usuario aplica la primera cancelación, la acción posterior posterior ya no funciona. Supongo que el estado de CancelaciónTokenSource se ha establecido en Cancelar y quiero saber cómo restablecerlo.
Pregunta 1: ¿Cómo restablecer el CancelTokenSource después del primer uso?
Pregunta 2: ¿Cómo depurar el multiproceso en VS2010? Si ejecuto la aplicación en modo de depuración, puedo ver la siguiente excepción para la declaración
this.Text = string.Format("Processing {0} on thread {1}", filename, Thread.CurrentThread.ManagedThreadId);
La InvalidOperaationException no fue manejada por el código de usuario. La operación entre hilos no es válida: se puede acceder a ''MainForm'' del control desde un hilo diferente al 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();
}
Debajo de Debug> windows en visual studio, querrás mirar la ventana de hilos, la ventana de la pila de llamadas y la ventana de tareas paralelas.
Cuando el depurador se rompe por la excepción que está recibiendo, puede mirar la ventana de la pila de llamadas para ver qué hilo está haciendo la llamada y de dónde viene ese hilo.
-edit basado en screenshot publicado
puede hacer clic con el botón derecho en la pila de llamadas y seleccionar ''mostrar código externo'' para ver exactamente lo que está sucediendo en la pila, pero ''código externo'' significa ''en algún lugar del marco'', por lo que puede o no ser útil (generalmente encuentro aunque interesante :))
Desde su captura de pantalla también podemos ver que la llamada se está realizando desde un subproceso de 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 está lanzando la excepción. El nombre de este subproceso es "Subproceso de trabajo" y eso significa que proviene del grupo de subprocesos.
Como ya se ha señalado, debe realizar actualizaciones en su interfaz de usuario desde el hilo de usuario. Por ejemplo, puede usar el ''Invoke'' en el control para esto, vea @CodeInChaos awnser.
-editar2-
Leí sus comentarios en @CodeInChaos awnser y aquí hay una forma de hacerlo de una manera más TPL: en primer lugar, debe obtener una instancia de TaskScheduler
que ejecutará tareas en el subproceso de la interfaz de usuario. puede hacer esto declarando un TaskScheduler
en su clase ui llamada por ejemplo uiScheduler
y en el constructor configurándolo en TaskScheduler.FromCurrentSynchronizationContext();
Ahora que lo tiene, puede hacer una nueva tarea que actualice 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 programador de tareas a la tarea cuando lo iniciamos.
También hay una segunda forma de hacerlo, que utiliza las API de TaskContinuation. Sin embargo, ya no podemos usar Paralell.Foreach, pero usaremos una tarea y tareas regulares. la clave es que una tarea le permite programar otra tarea que se ejecutará una vez que se realiza la primera tarea. Pero la segunda tarea no tiene que ejecutarse en el mismo programador y eso es muy útil para nosotros en este momento, ya que queremos trabajar 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 estamos haciendo aquí es hacer una nueva tarea en cada bucle que hará el trabajo y generará el mensaje, luego nos engancharemos en otra tarea que realmente actualizará la interfaz de usuario.
Puedes leer más sobre ContinueWith y continuaciones here
Estoy usando una clase en la que engaño a un CancelTokenSource de 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 tiene el infierno, pero funciona. Eventualmente debo encontrar una mejor solución.
Gracias por toda su ayuda con los hilos de arriba. Me ayudó en mi investigación. Pasé mucho tiempo tratando de resolver esto y no fue fácil. Hablar con un amigo también ayudó mucho.
Cuando inicie y detenga un subproceso, 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 detrás primero en vb.net y luego en C #. Tenga en cuenta que la versión C # es una traducción.
Primero 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" />
<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;
}
}
Disfruta el código!
Para la depuración definitivamente recomiendo usar la ventana de pilas paralelas junto con la ventana de subprocesos. Usando la ventana de pilas paralelas puede ver las pilas de llamadas de todos los hilos en una pantalla combinada. Puede saltar fácilmente entre hilos y puntos en la pila de llamadas. Las ventanas de pilas y subprocesos paralelos se encuentran en Depurar> Windows.
Otra cosa que realmente puede ayudar en la depuración es activar el lanzamiento de excepciones CLR cuando se lanzan Y el usuario no las maneja. Para hacer esto, vaya a Depurar> Excepciones, y habilite ambas opciones -