Как 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();
}
Ответы
Ответ 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" />
<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();
Уродливый ад, но он работает. В конечном итоге я должен найти лучшее решение.