J'ai une interface graphique WPF, où je veux appuyer sur un bouton pour démarrer une tâche longue sans geler la fenêtre pour la durée de la tâche. Pendant que la tâche est en cours, je souhaite obtenir des rapports sur les progrès et incorporer un autre bouton qui arrêtera la tâche à tout moment de mon choix.
Je ne peux pas comprendre comment utiliser async/wait/task. Je ne peux pas inclure tout ce que j'ai essayé, mais c'est ce que j'ai pour le moment.
Une classe de fenêtre WPF:
public partial class MainWindow : Window
{
readonly otherClass _burnBabyBurn = new OtherClass();
internal bool StopWorking = false;
//A button method to start the long running method
private async void Button_Click_3(object sender, RoutedEventArgs e)
{
Task burnTheBaby = _burnBabyBurn.ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3);
await burnTheBaby;
}
//A button Method to interrupt and stop the long running method
private void StopButton_Click(object sender, RoutedEventArgs e)
{
StopWorking = true;
}
//A method to allow the worker method to call back and update the gui
internal void UpdateWindow(string message)
{
TextBox1.Text = message;
}
}
Et une classe pour la méthode worker:
class OtherClass
{
internal Task ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
var tcs = new TaskCompletionSource<int>();
//Start doing work
gui.UpdateWindow("Work Started");
While(stillWorking)
{
//Mid procedure progress report
gui.UpdateWindow("Bath water n% thrown out");
if (gui.StopTraining) return tcs.Task;
}
//Exit message
gui.UpdateWindow("Done and Done");
return tcs.Task;
}
}
Cela s'exécute, mais la fenêtre de la fonction WPF est toujours bloquée une fois la méthode de travail démarrée.
J'ai besoin de savoir comment arranger les déclarations async/wait/task pour permettre
A) la méthode de travail pour ne pas bloquer la fenêtre graphique
B) laisser la méthode worker mettre à jour la fenêtre de l'interface graphique
C) permettre à la fenêtre d'interface graphique d'arrêter l'interruption et d'arrêter la méthode de travail
Toute aide ou pointeur est très apprécié.
Longue histoire courte:
private async void ButtonClick(object sender, RoutedEventArgs e)
{
txt.Text = "started";// done in UI thread
// wait for the task to finish, but don't block the UI thread
await Task.Run(()=> HeavyMethod(txt));
// The task is now completed.
txt.Text = "done";// done in UI thread
}
// Running the Task causes this method to be executed in Thread Pool
internal void HeavyMethod(TextBox /*or any Control or Window*/ txt)
{
while (stillWorking)
{
txt/*or a control or a window*/.Dispatcher.Invoke(() =>
{
// UI operations go inside of Invoke
txt.Text += ".";
});
// CPU-bound or I/O-bound operations go outside of Invoke
System.Threading.Thread.Sleep(51);
}
}
Result:
txt.Text == "started....................done"
Vous ne pouvez que await
dans une méthode async
.
Vous ne pouvez que await
un objet awaitable
(c'est-à-dire Task
ou Task<T>
)
Task.Run
généralement place un Task
dans le pool de threads (c’est-à-dire qu’il utilise un thread existant du pool de threads ou crée un nouveau thread dans le pool de threads pour exécuter la tâche. C’est vrai si l’opération asynchrone n’est pas une opération pure , sinon il y aura pas de thread, juste une opération async pure gérée par le système d'exploitation et les pilotes de périphérique)
L'exécution attend à await
que la tâche se termine et renvoie ses résultats, sans bloquer le thread principal à cause du mot clé async
capacité magique:
Le mot clé magique de async
signifie qu'il ne crée pas un autre thread. Cela permet seulement au compilateur d'abandonner et de reprendre le contrôle sur cette méthode. ( ne confondez pas la méthode avec le mot clé async
avec la méthode intégrée à un Task
)
Alors
Votre thread principal appelle la méthode async
(MyButton_Click
) Comme une méthode normale et aucun thread jusqu'à présent ... Maintenant, vous pouvez exécuter une tâche dans le MyButton_Click
Comme ceci:
private async void MyButton_Click(object sender, RoutedEventArgs e)
{
//queue a task to run on threadpool
Task task = Task.Run(()=>
ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
//wait for it to end without blocking the main thread
await task;
}
ou simplement
private async void MyButton_Click(object sender, RoutedEventArgs e)
{
await Task.Run(()=>
ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
}
ou si ExecuteLongProcedure
a une valeur de retour de type string
private async void MyButton_Click(object sender, RoutedEventArgs e)
{
Task<string> task = Task.Run(()=>
ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
string returnValue = await task;
}
ou simplement
private async void MyButton_Click(object sender, RoutedEventArgs e)
{
string returnValue = await Task.Run(()=>
ExecuteLongProcedure(this, intParam1, intParam2, intParam3));
//or in cases where you already have a "Task returning" method:
// var httpResponseInfo = await httpRequestInfo.GetResponseAsync();
}
La méthode à l'intérieur de la tâche (ou ExecuteLongProcedure
) s'exécute de manière asynchrone et se présente comme suit:
//change the value for the following flag to terminate the loop
bool stillWorking = true;
//calling this method blocks the calling thread
//you must run a task for it
internal void ExecuteLongProcedure(MainWindow gui, int param1, int param2, int param3)
{
//Start doing work
gui.UpdateWindow("Work Started");
while (stillWorking)
{
//put a dot in the window showing the progress
gui.UpdateWindow(".");
//the following line will block main thread unless
//ExecuteLongProcedure is called with await keyword
System.Threading.Thread.Sleep(51);
}
gui.UpdateWindow("Done and Done");
}
Si task
est de type Task<T>
, La valeur renvoyée par l'instruction await task
Est une valeur de type T
. Si task
est de type Task
alors await task
Ne renvoie rien (ou renvoie void
). À ce stade, vous pouvez indiquer au compilateur de await
la tâche à terminer ou simplement de passer à la ligne suivante.
Par conséquent, si votre méthode async
ne renvoie rien, vous pouvez écrire async void MyMethod()
ou async Task MyMethod()
. Et si votre méthode async
renvoie quelque chose (par exemple, un entier), vous pouvez écrire async Task<int> MyMethod
. Dans ce cas, votre code peut ressembler à ceci:
private async Task<int> MyMethod()
{
int number = await Task.Run(todo);
return number;
}
Ceci est évident car si vous ne voulez pas attendre les résultats , vous n’avez probablement pas besoin de Task
comme type de retour de la méthode asynchrone. Mais si vous voulez attendre un résultat , alors vous devez attendre le résultat de l'async méthode de la même manière que dans cette méthode. par exemple. var asyncResult = await MyMethod()
Encore confus? Lire les types de retour async sur on MSDN .
Task.Run
Est une version plus récente (.NetFX4.5) et plus simple de Task.Factory.StartNew
await
est pas Task.Wait()
Les opérations liées à la CPU ou à l'IO telles que Sleep
vont bloquer le thread principal même si elles sont appelées dans une méthode avec async
mot clé. ( encore une fois, ne confondez pas la méthode async
avec la méthode dans un Task
. Évidemment, cela n’est pas vrai si la méthode async elle-même est exécutée en tant que tâche: await MyAsyncMethod
)
await
empêche une tâche de bloquer le thread principal car le compilateur abandonnera son contrôle sur la méthode async
.
private async void Button_Click(object sender, RoutedEventArgs e)
{
Thread.Sleep(1000);//blocks
await Task.Run(() => Thread.Sleep(1000));//does not block
}
Si vous devez accéder à l'interface graphique de manière asynchrone (à l'intérieur de la méthode ExecuteLongProcedure
), invoquez toute opération impliquant la modification d'un objet non thread-safe. . Par exemple, tout objet d'interface graphique WPF doit être appelé à l'aide d'un objet Dispatcher
associé au thread d'interface graphique:
void UpdateWindow(string text)
{
//safe call
Dispatcher.Invoke(() =>
{
txt.Text += text;
});
}
Cependant, si une tâche est démarrée à la suite d'une modification de rappel de propriété à partir du ViewModel, il n'est pas nécessaire d'utiliser Dispatcher.Invoke
Car le rappel est en réalité exécuté à partir du thread d'interface utilisateur.
Accès aux collections sur des threads non-UI
WPF vous permet d'accéder aux collections de données sur des threads autres que celui qui a créé la collection et de les modifier. Cela vous permet d'utiliser un thread d'arrière-plan pour recevoir des données d'une source externe, telle qu'une base de données, et d'afficher les données sur le thread d'interface utilisateur. En utilisant un autre thread pour modifier la collection, votre interface utilisateur reste sensible aux interactions de l'utilisateur.
Comment activer l'accès cross-thread
Rappelez-vous, la méthode async
s'exécute elle-même sur le thread principal. Donc ceci est valide:
private async void MyButton_Click(object sender, RoutedEventArgs e)
{
txt.Text = "starting"; // UI Thread
await Task.Run(()=> ExecuteLongProcedure1());
txt.Text = "waiting"; // UI Thread
await Task.Run(()=> ExecuteLongProcedure2());
txt.Text = "finished"; // UI Thread
}
Il suffit de postfixer le nom de la méthode avec le type de retour Task
ou Task<T>
Avec Async
. par exemple:
Task WriteToFileAsync(string fileName)
{
return Task.Run(()=>WriteToFile(fileName));
}
async void DoJob()
{
await WriteToFileAsync("a.txt");
}
void Main()
{
DoJob();
}
N'utilisez pas Async
postfix pour une méthode qui sera transmise à Task.Run()
.
Personnellement, je pense que le Async
postfixe ne devrait pas être utilisé pour une méthode qui ne renvoie pas un Task
ou Task<T>
. Mais la plupart des gens utilisent ce préfixe sur n’importe quelle méthode async
.
Il y a encore beaucoup à apprendre sur async
, son contexte et sa suite .
Pas nécessairement. Lire cette réponse pour en savoir plus sur le vrai visage de async
.
Stephen Cleary a parfaitement expliqué async-await
. Il explique également dans son autre article de blog quand aucun fil n'est impliqué.
comment-appeler-méthode-asynchrone-depuis-méthode-synchrone
async await
- Dans les coulisses
Assurez-vous de connaître la différence entre Asynchrone, Parallèle et Concurrent.
Vous pouvez également lire n simple rédacteur de fichier asynchrone pour savoir où vous devez être simultané.
Enquêter espace de noms simultané
En fin de compte, lisez ce livre électronique: Patterns_of_Parallel_Programming_CSharp
Votre utilisation de TaskCompletionSource<T>
est incorrect. TaskCompletionSource<T>
est un moyen de créer wrappers compatibles avec TAP pour les opérations asynchrones. Dans votre méthode ExecuteLongProcedureAsync
, l’exemple de code est lié à la CPU (c’est-à-dire intrinsèquement synchrone et non asynchrone).
Donc, il est beaucoup plus naturel d'écrire ExecuteLongProcedure
en tant que méthode synchrone. C'est aussi une bonne idée d'utiliser des types standard pour les comportements standard, en particulier en utilisant IProgress<T>
pour les mises à jour progressives et CancellationToken
pour les annulations :
internal void ExecuteLongProcedure(int param1, int param2, int param3,
CancellationToken cancellationToken, IProgress<string> progress)
{
//Start doing work
if (progress != null)
progress.Report("Work Started");
while (true)
{
//Mid procedure progress report
if (progress != null)
progress.Report("Bath water n% thrown out");
cancellationToken.ThrowIfCancellationRequested();
}
//Exit message
if (progress != null)
progress.Report("Done and Done");
}
Vous avez maintenant un type plus réutilisable (pas de dépendance d'interface graphique) qui utilise les conventions appropriées. Il peut être utilisé comme tel:
public partial class MainWindow : Window
{
readonly otherClass _burnBabyBurn = new OtherClass();
CancellationTokenSource _stopWorkingCts = new CancellationTokenSource();
//A button method to start the long running method
private async void Button_Click_3(object sender, RoutedEventArgs e)
{
var progress = new Progress<string>(data => UpdateWindow(data));
try
{
await Task.Run(() => _burnBabyBurn.ExecuteLongProcedure(intParam1, intParam2, intParam3,
_stopWorkingCts.Token, progress));
}
catch (OperationCanceledException)
{
// TODO: update the GUI to indicate the method was canceled.
}
}
//A button Method to interrupt and stop the long running method
private void StopButton_Click(object sender, RoutedEventArgs e)
{
_stopWorkingCts.Cancel();
}
//A method to allow the worker method to call back and update the gui
void UpdateWindow(string message)
{
TextBox1.Text = message;
}
}
Ceci est une version simplifiée de la réponse la plus populaire ici par Bijan. J'ai simplifié la réponse de Bijan pour m'aider à résoudre le problème en utilisant le formatage Nice fourni par Stack Overflow.
En lisant et en révisant attentivement le message de Bijan, j'ai finalement compris: Comment attendre que la méthode asynchrone se termine?
Dans mon cas, la réponse choisie pour cet autre poste est ce qui m'a finalement amené à résoudre mon problème:
"Éviter async void
. Demandez à vos méthodes de retourner Task
au lieu de void
. Ensuite, vous pouvez await
les ".
Voici ma version simplifiée de (excellente) réponse de Bijan:
1) Cela démarre une tâche en async et attend:
private async void Button_Click_3(object sender, RoutedEventArgs e)
{
// if ExecuteLongProcedureAsync has a return value
var returnValue = await Task.Run(()=>
ExecuteLongProcedureAsync(this, intParam1, intParam2, intParam3));
}
2) Voici la méthode à exécuter de manière asynchrone:
bool stillWorking = true;
internal void ExecuteLongProcedureAsync(MainWindow gui, int param1, int param2, int param3)
{
//Start doing work
gui.UpdateWindow("Work Started");
while (stillWorking)
{
//put a dot in the window showing the progress
gui.UpdateWindow(".");
//the following line blocks main thread unless
//ExecuteLongProcedureAsync is called with await keyword
System.Threading.Thread.Sleep(50);
}
gui.UpdateWindow("Done and Done");
}
3) Invoquer l'opération qui implique une propriété de gui:
void UpdateWindow(string text)
{
//safe call
Dispatcher.Invoke(() =>
{
txt.Text += text;
});
}
Ou,
void UpdateWindow(string text)
{
//simply
txt.Text += text;
}
Commentaires de clôture) Dans la plupart des cas, vous avez deux méthodes.
Première méthode (Button_Click_3
) appelle la deuxième méthode et a le modificateur async
qui indique au compilateur d'activer le threading pour cette méthode.
Thread.Sleep
dans une méthode async
bloque le thread principal. mais attendre une tâche ne le fait pas.await
jusqu'à la fin de la tâche.await
en dehors d'une méthode async
La deuxième méthode (ExecuteLongProcedureAsync
) est encapsulée dans une tâche et retourne un Task<original return type>
objet qui peut être chargé de façon asynchrone en ajoutant await
avant.
Liero a soulevé un problème important. Lorsque vous liez un élément à une propriété ViewModel, la propriété modifiée callback est exécutée dans le thread d'interface utilisateur. Donc, il n'est pas nécessaire d'utiliser Dispatcher.Invoke
. Les changements de valeur déclenchés par INotifyPropertyChanged sont automatiquement redirigés vers le répartiteur.
Voici un exemple utilisant async/await
, IProgress<T>
et CancellationTokenSource
. Ce sont les fonctionnalités modernes du langage C # et .Net Framework que vous devriez utiliser. Les autres solutions me font un peu saigner les yeux.
<Window x:Class="ProgressExample.MainWindow"
xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.Microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" SizeToContent="WidthAndHeight" Height="93.258" Width="316.945">
<StackPanel>
<Button x:Name="Button_Start" Click="Button_Click">Start</Button>
<ProgressBar x:Name="ProgressBar_Progress" Height="20" Maximum="100"/>
<Button x:Name="Button_Cancel" IsEnabled="False" Click="Button_Cancel_Click">Cancel</Button>
</StackPanel>
</Window>
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private CancellationTokenSource currentCancellationSource;
public MainWindow()
{
InitializeComponent();
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
// Enable/disabled buttons so that only one counting task runs at a time.
this.Button_Start.IsEnabled = false;
this.Button_Cancel.IsEnabled = true;
try
{
// Set up the progress event handler - this instance automatically invokes to the UI for UI updates
// this.ProgressBar_Progress is the progress bar control
IProgress<int> progress = new Progress<int>(count => this.ProgressBar_Progress.Value = count);
currentCancellationSource = new CancellationTokenSource();
await CountToOneHundredAsync(progress, this.currentCancellationSource.Token);
// Operation was successful. Let the user know!
MessageBox.Show("Done counting!");
}
catch (OperationCanceledException)
{
// Operation was cancelled. Let the user know!
MessageBox.Show("Operation cancelled.");
}
finally
{
// Reset controls in a finally block so that they ALWAYS go
// back to the correct state once the counting ends,
// regardless of any exceptions
this.Button_Start.IsEnabled = true;
this.Button_Cancel.IsEnabled = false;
this.ProgressBar_Progress.Value = 0;
// Dispose of the cancellation source as it is no longer needed
this.currentCancellationSource.Dispose();
this.currentCancellationSource = null;
}
}
private async Task CountToOneHundredAsync(IProgress<int> progress, CancellationToken cancellationToken)
{
for (int i = 1; i <= 100; i++)
{
// This is where the 'work' is performed.
// Feel free to swap out Task.Delay for your own Task-returning code!
// You can even await many tasks here
// ConfigureAwait(false) tells the task that we dont need to come back to the UI after awaiting
// This is a good read on the subject - https://blog.stephencleary.com/2012/07/dont-block-on-async-code.html
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
// If cancelled, an exception will be thrown by the call the task.Delay
// and will bubble up to the calling method because we used await!
// Report progress with the current number
progress.Report(i);
}
}
private void Button_Cancel_Click(object sender, RoutedEventArgs e)
{
// Cancel the cancellation token
this.currentCancellationSource.Cancel();
}
}