"Comment bloquer le flux de code jusqu'à ce qu'un événement soit déclenché?"
Votre approche est fausse. L'événementiel ne signifie pas bloquer et attendre un événement. Vous n'attendez jamais, au moins vous essayez toujours de l'éviter. Attendre, c'est gaspiller des ressources, bloquer les threads et peut-être introduire le risque d'un blocage ou d'un thread zombie (au cas où le signal de libération ne serait jamais émis).
Il doit être clair que bloquer un thread pour attendre un événement est un anti-modèle car il contredit l'idée d'un événement.
Vous avez généralement deux options (modernes): implémentez une API asynchrone ou une API pilotée par les événements. Puisque vous ne voulez pas implémenter votre API asynchrone, vous vous retrouvez avec l'API événementielle.
La clé d'une API événementielle est qu'au lieu de forcer l'appelant à attendre un résultat de manière synchrone ou à interroger un résultat, vous laissez l'appelant continuer et lui envoyez une notification, une fois le résultat prêt ou l'opération terminée. Pendant ce temps, l'appelant peut continuer à exécuter d'autres opérations.
Lorsque vous examinez le problème du point de vue du thread, l'API événementielle permet au thread appelant, par exemple, le thread d'interface utilisateur, qui exécute le gestionnaire d'événements du bouton, d'être libre de continuer à gérer, par exemple, d'autres opérations liées à l'interface utilisateur, comme le rendu d'éléments d'interface utilisateur. ou gérer les entrées de l'utilisateur comme le mouvement de la souris et les pressions sur les touches. L'API événementielle a le même effet ou le même objectif qu'une API asynchrone, bien qu'elle soit beaucoup moins pratique.
Étant donné que vous n'avez pas fourni suffisamment de détails sur ce que vous essayez vraiment de faire, ce qui Utility.PickPoint()
se passe réellement et le résultat de la tâche ou pourquoi l'utilisateur doit cliquer sur la `Grille, je ne peux pas vous offrir une meilleure solution . Je peux juste offrir un modèle général de la façon de mettre en œuvre votre exigence.
Votre flux ou votre objectif est évidemment divisé en au moins deux étapes pour en faire une séquence d'opérations:
- Exécutez l'opération 1, lorsque l'utilisateur clique sur le bouton
- Exécutez l'opération 2 (continuer / terminer l'opération 1), lorsque l'utilisateur clique sur le
Grid
avec au moins deux contraintes:
- Facultatif: la séquence doit être terminée avant que le client API ne soit autorisé à la répéter. Une séquence est terminée une fois l'opération 2 terminée.
- L'opération 1 est toujours exécutée avant l'opération 2. L'opération 1 démarre la séquence.
- L'opération 1 doit se terminer avant que le client API ne soit autorisé à exécuter l'opération 2
Cela nécessite au moins deux notifications pour que le client de l'API autorise une interaction non bloquante:
- Opération 1 terminée (ou interaction requise)
- Opération 2 (ou objectif) terminée
Vous devez laisser votre API implémenter ce comportement et ces contraintes en exposant deux méthodes publiques et deux événements publics.
API utilitaire d'implémentation / refactorisation
Utility.cs
class Utility
{
public event EventHandler InitializePickPointCompleted;
public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
private bool IsPickPointInitialized { get; set; }
private bool IsExecutingSequence { get; set; }
// The prefix 'Begin' signals the caller or client of the API,
// that he also has to end the sequence explicitly
public void BeginPickPoint(param)
{
// Implement constraint 1
if (this.IsExecutingSequence)
{
// Alternatively just return or use Try-do pattern
throw new InvalidOperationException("BeginPickPoint is already executing. Call EndPickPoint before starting another sequence.");
}
// Set the flag that a current sequence is in progress
this.IsExecutingSequence = true;
// Execute operation until caller interaction is required.
// Execute in background thread to allow API caller to proceed with execution.
Task.Run(() => StartOperationNonBlocking(param));
}
public void EndPickPoint(param)
{
// Implement constraint 2 and 3
if (!this.IsPickPointInitialized)
{
// Alternatively just return or use Try-do pattern
throw new InvalidOperationException("BeginPickPoint must have completed execution before calling EndPickPoint.");
}
// Execute operation until caller interaction is required.
// Execute in background thread to allow API caller to proceed with execution.
Task.Run(() => CompleteOperationNonBlocking(param));
}
private void StartOperationNonBlocking(param)
{
... // Do something
// Flag the completion of the first step of the sequence (to guarantee constraint 2)
this.IsPickPointInitialized = true;
// Request caller interaction to kick off EndPickPoint() execution
OnInitializePickPointCompleted();
}
private void CompleteOperationNonBlocking(param)
{
// Execute goal and get the result of the completed task
Point result = ExecuteGoal();
// Reset API sequence
this.IsExecutingSequence = false;
this.IsPickPointInitialized = false;
// Notify caller that execution has completed and the result is available
OnPickPointCompleted(result);
}
private void OnInitializePickPointCompleted()
{
// Set the result of the task
this.InitializePickPointCompleted?.Invoke(this, EventArgs.Empty);
}
private void OnPickPointCompleted(Point result)
{
// Set the result of the task
this.PickPointCompleted?.Invoke(this, new PickPointCompletedEventArgs(result));
}
}
PickPointCompletedEventArgs.cs
class PickPointCompletedEventArgs : EventArgs
{
public Point Result { get; }
public PickPointCompletedEventArgs(Point result)
{
this.Result = result;
}
}
Utilisez l'API
MainWindow.xaml.cs
partial class MainWindow : Window
{
private Utility Api { get; set; }
public MainWindow()
{
InitializeComponent();
this.Api = new Utility();
}
private void StartPickPoint_OnButtonClick(object sender, RoutedEventArgs e)
{
this.Api.InitializePickPointCompleted += RequestUserInput_OnInitializePickPointCompleted;
// Invoke API and continue to do something until the first step has completed.
// This is possible because the API will execute the operation on a background thread.
this.Api.BeginPickPoint();
}
private void RequestUserInput_OnInitializePickPointCompleted(object sender, EventArgs e)
{
// Cleanup
this.Api.InitializePickPointCompleted -= RequestUserInput_OnInitializePickPointCompleted;
// Communicate to the UI user that you are waiting for him to click on the screen
// e.g. by showing a Popup, dimming the screen or showing a dialog.
// Once the input is received the input event handler will invoke the API to complete the goal
MessageBox.Show("Please click the screen");
}
private void FinishPickPoint_OnGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.Api.PickPointCompleted += ShowPoint_OnPickPointCompleted;
// Invoke API to complete the goal
// and continue to do something until the last step has completed
this.Api.EndPickPoint();
}
private void ShowPoint_OnPickPointCompleted(object sender, PickPointCompletedEventArgs e)
{
// Cleanup
this.Api.PickPointCompleted -= ShowPoint_OnPickPointCompleted;
// Get the result from the PickPointCompletedEventArgs instance
Point point = e.Result;
// Handle the result
MessageBox.Show(point.ToString());
}
}
MainWindow.xaml
<Window>
<Grid MouseLeftButtonUp="FinishPickPoint_OnGridMouseLeftButtonUp">
<Button Click="StartPickPoint_OnButtonClick" />
</Grid>
</Window>
Remarques
Les événements déclenchés sur un thread d'arrière-plan exécuteront leurs gestionnaires sur le même thread. L'accès DispatcherObject
à un élément d'interface utilisateur similaire à partir d'un gestionnaire, qui est exécuté sur un thread d'arrière-plan, nécessite que l'opération critique soit mise en file d'attente à l' Dispatcher
aide de l'un Dispatcher.Invoke
ou l' autre ou Dispatcher.InvokeAsync
pour éviter les exceptions inter-thread.
Lisez les remarques sur DispatcherObject
pour en savoir plus sur ce phénomène appelé affinité de répartiteur ou affinité de thread.
Quelques réflexions - répondez à vos commentaires
Parce que vous vous approchiez de moi pour trouver une "meilleure" solution de blocage, me donnant l'exemple des applications console, j'ai eu l'impression de vous convaincre, que votre perception ou votre point de vue est totalement faux.
"Envisagez une application console contenant ces deux lignes de code.
var str = Console.ReadLine();
Console.WriteLine(str);
Que se passe-t-il lorsque vous exécutez l'application en mode débogage. Il s'arrêtera à la première ligne de code et vous forcera à entrer une valeur dans l'interface utilisateur de la console, puis après avoir entré quelque chose et appuyé sur Entrée, il exécutera la ligne suivante et imprimera réellement ce que vous avez entré. Je pensais exactement au même comportement mais dans l'application WPF. "
Une application console est quelque chose de totalement différent. Le concept de filetage est un peu différent. Les applications de console n'ont pas d'interface graphique. Juste flux d'entrée / sortie / erreur. Vous ne pouvez pas comparer l'architecture d'une application console à une application GUI riche. Ça ne marchera pas. Vous devez vraiment comprendre et accepter cela.
Ne vous laissez pas tromper par les regards . Savez-vous ce qui se passe à l' intérieur Console.ReadLine
? Comment est-il mis en œuvre ? Est-ce qu'il bloque le thread principal et en parallèle, il lit l'entrée? Ou s'agit-il simplement d'un sondage?
Voici l'implémentation originale de Console.ReadLine
:
public virtual String ReadLine()
{
StringBuilder sb = new StringBuilder();
while (true)
{
int ch = Read();
if (ch == -1)
break;
if (ch == '\r' || ch == '\n')
{
if (ch == '\r' && Peek() == '\n')
Read();
return sb.ToString();
}
sb.Append((char)ch);
}
if (sb.Length > 0)
return sb.ToString();
return null;
}
Comme vous pouvez le voir, c'est une opération synchrone simple . Il interroge l'entrée utilisateur dans une boucle "infinie". Pas de bloc magique et continuez.
WPF est construit autour d'un thread de rendu et d'un thread d'interface utilisateur. Ces threads tournent toujours afin de communiquer avec le système d'exploitation comme la gestion des entrées utilisateur - en gardant l'application réactive . Vous ne voulez jamais suspendre / bloquer ce thread car cela empêchera le framework de faire un travail d'arrière-plan essentiel, comme répondre aux événements de la souris - vous ne voulez pas que la souris se fige:
attente = blocage des threads = absence de réponse = mauvaise expérience utilisateur = utilisateurs / clients ennuyés = problèmes au bureau.
Parfois, le flux d'application nécessite d'attendre l'entrée ou l'exécution d'une routine. Mais nous ne voulons pas bloquer le thread principal.
C'est pourquoi les gens ont inventé des modèles de programmation asynchrones complexes, pour permettre d'attendre sans bloquer le thread principal et sans forcer le développeur à écrire du code multithreading compliqué et erroné.
Chaque framework d'application moderne propose des opérations asynchrones ou un modèle de programmation asynchrone, pour permettre le développement de code simple et efficace.
Le fait que vous vous efforcez de résister au modèle de programmation asynchrone, me montre un certain manque de compréhension. Chaque développeur moderne préfère une API asynchrone à une API synchrone. Aucun développeur sérieux ne se soucie d'utiliser le await
mot - clé ou de déclarer sa méthode async
. Personne. Vous êtes le premier que je rencontre qui se plaint des API asynchrones et qui les trouve peu pratiques à utiliser.
Si je vérifiais votre infrastructure, qui vise à résoudre les problèmes liés à l'interface utilisateur ou à faciliter les tâches liées à l'interface utilisateur, je m'attendrais à ce qu'elle soit asynchrone - tout le long.
L'API liée à l'interface utilisateur qui n'est pas asynchrone est un gaspillage, car cela compliquera mon style de programmation, donc mon code qui devient donc plus sujet aux erreurs et difficile à maintenir.
Une perspective différente: lorsque vous reconnaissez que l'attente bloque le thread d'interface utilisateur, crée une expérience utilisateur très mauvaise et indésirable car l'interface utilisateur se figera jusqu'à la fin de l'attente, maintenant que vous vous rendez compte, pourquoi offririez-vous un modèle d'API ou de plugin qui encourage un développeur à faire exactement cela - implémenter l'attente?
Vous ne savez pas ce que fera le plugin tiers et combien de temps une routine prendra jusqu'à ce qu'elle se termine. Il s'agit simplement d'une mauvaise conception de l'API. Lorsque votre API fonctionne sur le thread d'interface utilisateur, l'appelant de votre API doit pouvoir lui faire des appels non bloquants.
Si vous refusez la seule solution bon marché ou gracieuse, utilisez une approche événementielle comme indiqué dans mon exemple.
Il fait ce que vous voulez: démarrer une routine - attendre la saisie de l'utilisateur - poursuivre l'exécution - atteindre l'objectif.
J'ai vraiment essayé plusieurs fois d'expliquer pourquoi l'attente / le blocage est une mauvaise conception d'application. Encore une fois, vous ne pouvez pas comparer une interface utilisateur de console à une interface graphique riche, par exemple, la gestion des entrées à elle seule est une multitude plus complexe que la simple écoute du flux d'entrée. Je ne connais vraiment pas votre niveau d'expérience et où vous avez commencé, mais vous devriez commencer à adopter le modèle de programmation asynchrone. Je ne connais pas la raison pour laquelle vous essayez de l'éviter. Mais ce n'est pas du tout sage.
Aujourd'hui, les modèles de programmation asynchrones sont mis en œuvre partout, sur chaque plate-forme, compilateur, chaque environnement, navigateur, serveur, bureau, base de données - partout. Le modèle événementiel permet d'atteindre le même objectif, mais il est moins pratique à utiliser (s'abonner / se désabonner à / des événements, lire des documents (lorsqu'il y a des documents) pour en savoir plus sur les événements), en s'appuyant sur des threads d'arrière-plan. L'événementiel est désuet et ne doit être utilisé que lorsque les bibliothèques asynchrones ne sont pas disponibles ou ne s'appliquent pas.
En guise de remarque: le .NET Framwork (.NET Standard) offre la possibilité TaskCompletionSource
(entre autres) de fournir un moyen simple de convertir une API existante pilotée par paires en une API asynchrone.
"J'ai vu le comportement exact dans Autodesk Revit."
Le comportement (ce que vous vivez ou observez) est très différent de la façon dont cette expérience est mise en œuvre. Deux choses différentes. Votre Autodesk utilise très probablement des bibliothèques asynchrones ou des fonctionnalités de langage ou un autre mécanisme de thread. Et c'est aussi lié au contexte. Lorsque la méthode qui vous vient à l'esprit s'exécute sur un thread d'arrière-plan, le développeur peut choisir de bloquer ce thread. Il a soit une très bonne raison de le faire, soit il a juste fait un mauvais choix de conception. Vous êtes totalement sur la mauvaise voie;) Le blocage n'est pas bon.
(Le code source d'Autodesk est-il open source? Ou comment savez-vous comment il est implémenté?)
Je ne veux pas vous offenser, croyez-moi. Mais veuillez reconsidérer pour implémenter votre API asynchrone. Ce n'est que dans votre tête que les développeurs n'aiment pas utiliser async / wait. Vous avez manifestement eu la mauvaise mentalité. Et oubliez cet argument d'application de console - c'est un non-sens;)
L'API liée à l'interface utilisateur DOIT utiliser async / wait chaque fois que possible. Sinon, vous laissez tout le travail pour écrire du code non bloquant au client de votre API. Vous me forceriez à envelopper chaque appel à votre API dans un fil d'arrière-plan. Ou pour utiliser une gestion d'événements moins confortable. Croyez-moi - chaque développeur décore ses membres plutôt async
que de gérer des événements. Chaque fois que vous utilisez des événements, vous pouvez risquer une fuite de mémoire potentielle - cela dépend de certaines circonstances, mais le risque est réel et n'est pas rare lors d'une programmation imprudente.
J'espère vraiment que vous comprenez pourquoi le blocage est mauvais. J'espère vraiment que vous déciderez d'utiliser async / wait pour écrire une API asynchrone moderne. Néanmoins, je vous ai montré une façon très courante d'attendre le non-blocage, en utilisant des événements, bien que je vous exhorte à utiliser async / wait.
"L'API permettra au programmeur d'avoir accès à l'interface utilisateur, etc. Supposons maintenant que le programmeur souhaite développer un complément qui, lorsqu'un bouton est cliqué, l'utilisateur final est invité à choisir un point dans l'interface utilisateur"
Si vous ne souhaitez pas autoriser le plugin à accéder directement aux éléments de l'interface utilisateur, vous devez fournir une interface pour déléguer des événements ou exposer des composants internes via des objets abstraits.
L'API s'abonne en interne aux événements d'interface utilisateur au nom du complément, puis délègue l'événement en exposant un événement «wrapper» correspondant au client API. Votre API doit proposer des crochets sur lesquels le complément peut se connecter pour accéder à des composants d'application spécifiques. Une API de plugin agit comme un adaptateur ou une façade pour donner aux externes un accès aux internes.
Pour permettre un certain degré d'isolement.
Jetez un œil à la façon dont Visual Studio gère les plugins ou nous permet de les implémenter. Imaginez que vous souhaitez écrire un plugin pour Visual Studio et faites des recherches sur la façon de procéder. Vous vous rendrez compte que Visual Studio expose ses composants internes via une interface ou une API. EG, vous pouvez manipuler l'éditeur de code ou obtenir des informations sur le contenu de l'éditeur sans y avoir réellement accès.
Aync/Await
savoir comment faire l'opération A et enregistrer cette opération ÉTAT maintenant que vous voulez que l'utilisateur clique sur la grille.