Comment la prise en charge asynchrone C # 5 résoudra-t-elle les problèmes de synchronisation des threads de l'interface utilisateur?

16

J'ai entendu quelque part que l'attente asynchrone C # 5 sera si géniale que vous n'aurez pas à vous soucier de le faire:

if (InvokeRequired)
{
    BeginInvoke(...);
    return;
}
// do your stuff here

Il semble que le rappel d'une opération d'attente se produira dans le thread d'origine de l'appelant. Eric Lippert et Anders Hejlsberg ont déclaré à plusieurs reprises que cette fonctionnalité provenait de la nécessité de rendre les interfaces utilisateur (en particulier celles des appareils tactiles) plus réactives.

Je pense qu'une utilisation courante d'une telle fonctionnalité serait quelque chose comme ceci:

public class Form1 : Form
{
    // ...
    async void GetFurtherInfo()
    {
        var temperature = await GetCurrentTemperatureAsync();
        label1.Text = temperature;
    }
}

Si seul un rappel est utilisé, la définition du texte de l'étiquette déclenche une exception car il n'est pas exécuté dans le thread de l'interface utilisateur.

Jusqu'à présent, je n'ai trouvé aucune ressource confirmant que c'est le cas. Est-ce que quelqu'un sait à propos de cela? Existe-t-il des documents expliquant techniquement comment cela fonctionnera?

Veuillez fournir un lien à partir d'une source fiable, ne vous contentez pas de répondre «oui».

Alex
la source
Cela semble hautement improbable, du moins en ce qui concerne la awaitfonctionnalité. C'est juste beaucoup de sucre syntaxique pour passer la suite . Peut-être y a-t-il d'autres améliorations non liées à WinForms qui sont censées aider? Cela tomberait dans le cadre du .NET lui-même, et non en C # en particulier.
Aaronaught
@Aaronaught Je suis d'accord, c'est pourquoi je pose précisément la question. J'ai édité la question pour préciser d'où je viens. Cela semble étrange qu'ils créent cette fonctionnalité et nécessitent toujours que nous utilisions le fameux style de code InvokeRequired.
Alex

Réponses:

17

Je pense que vous confondez quelques choses ici. Ce que vous demandez est déjà possible à l'aide System.Threading.Tasks, asyncet awaiten C # 5 vont juste fournir un peu plus de sucre syntaxique pour la même fonctionnalité.

Prenons un exemple Winforms - déposez un bouton et une zone de texte sur le formulaire et utilisez ce code:

private void button1_Click(object sender, EventArgs e)
{
    Task.Factory.StartNew<int>(() => DelayedAdd(5, 10))
        .ContinueWith(t => DelayedAdd(t.Result, 20))
        .ContinueWith(t => DelayedAdd(t.Result, 30))
        .ContinueWith(t => DelayedAdd(t.Result, 50))
        .ContinueWith(t => textBox1.Text = t.Result.ToString(),
            TaskScheduler.FromCurrentSynchronizationContext());
}

private int DelayedAdd(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Exécutez-le et vous verrez que (a) il ne bloque pas le thread d'interface utilisateur et (b) vous n'obtenez pas l'erreur habituelle "opération cross-thread non valide" - sauf si vous supprimez l' TaskSchedulerargument du dernier ContinueWith, dans auquel cas vous le ferez.

C'est standard style de passage de continuation . La magie opère dans la TaskSchedulerclasse et plus précisément dans l'instance récupérée par FromCurrentSynchronizationContext. Passez cela dans n'importe quelle continuation et vous lui dites que la continuation doit s'exécuter sur le thread appelé la FromCurrentSynchronizationContextméthode - dans ce cas, le thread d'interface utilisateur.

Les attente sont légèrement plus sophistiquées dans le sens où elles savent sur quel thread elles ont commencé et sur quel thread la suite doit se produire. Le code ci-dessus peut donc être écrit un peu plus naturellement:

private async void button1_Click(object sender, EventArgs e)
{
    int a = await DelayedAddAsync(5, 10);
    int b = await DelayedAddAsync(a, 20);
    int c = await DelayedAddAsync(b, 30);
    int d = await DelayedAddAsync(c, 50);
    textBox1.Text = d.ToString();
}

private async Task<int> DelayedAddAsync(int a, int b)
{
    Thread.Sleep(500);
    return a + b;
}

Ces deux devraient être très similaires, et en fait, ils sont très similaires. leDelayedAddAsync méthode retourne maintenant un Task<int>au lieu d'un int, et donc le awaitfait de simplement gifler les continuations sur chacun d'eux. La principale différence est qu'elle passe le long du contexte de synchronisation sur chaque ligne, vous n'avez donc pas à le faire explicitement comme nous l'avons fait dans le dernier exemple.

En théorie, les différences sont beaucoup plus importantes. Dans le deuxième exemple, chaque ligne de la button1_Clickméthode est réellement exécutée dans le thread d'interface utilisateur, mais la tâche elle-même ( DelayedAddAsync) s'exécute en arrière-plan. Dans le premier exemple, tout s'exécute en arrière - plan , à l' exception de l'affectation à textBox1.Textlaquelle nous avons explicitement attaché le contexte de synchronisation du thread d'interface utilisateur.

C'est ce qui est vraiment intéressant await- le fait qu'un serveur d'attente peut sauter dans et hors de la même méthode sans aucun appel bloquant. Vous appelez await, le thread actuel revient au traitement des messages, et quand c'est fait, le serveur reprendra exactement là où il s'était arrêté, dans le même thread qu'il a laissé. Mais en termes de votre Invoke/ BeginInvokecontraste dans la question, je '' Je suis désolé de dire que vous auriez dû arrêter cela il y a longtemps.

Aaronaught
la source
C'est très intéressant @Aaronaught. J'étais au courant du style de passage de continuation mais je n'étais pas au courant de toute cette histoire de "contexte de synchronisation". Existe-t-il un document liant ce contexte de synchronisation à C # 5 async-wait? Je comprends que c'est une fonctionnalité existante, mais le fait qu'ils l'utilisent par défaut semble être un gros problème, surtout parce que cela doit avoir des impacts majeurs sur les performances, non? Avez-vous d'autres commentaires à ce sujet? Merci pour votre réponse au fait.
Alex
1
@Alex: Pour obtenir des réponses à toutes ces questions complémentaires, je vous suggère de lire Async Performance: Understanding the Costs of Async and Await . La section "Attention au contexte" explique comment tout cela est lié au contexte de synchronisation.
Aaronaught
(Soit dit en passant, les contextes de synchronisation ne sont pas nouveaux; ils sont dans le cadre depuis 2.0. Le TPL les a simplement rendus beaucoup plus faciles à utiliser.)
Aaronaught
2
Je me demande pourquoi il y a encore beaucoup de discussions sur l'utilisation du style InvokeRequired et la plupart des discussions que j'ai vues ne mentionnent même pas les contextes de synchronisation. Cela m'aurait fait gagner du temps pour poser cette question ...
Alex
2
@Alex: Je suppose que vous ne cherchiez pas aux bons endroits . Je ne sais pas quoi te dire; il existe de grandes parties de la communauté .NET qui prennent du temps à rattraper leur retard. Enfer, je vois encore des codeurs utiliser la ArrayListclasse dans un nouveau code. Je n'ai à peine aucune expérience avec RX, moi-même. Les gens apprennent ce qu'ils ont besoin de savoir et partagent ce qu'ils savent déjà, même si ce qu'ils savent déjà est dépassé. Cette réponse pourrait être obsolète dans quelques années.
Aaronaught
4

Oui, pour le thread d'interface utilisateur, le rappel d'une opération d'attente se produira sur le thread d'origine de l'appelant.

Il y a un an, Eric Lippert a écrit une série en 8 parties: Fabulous Adventures In Coding

EDIT: et voici // build / présentation d' Anders : channel9

BTW, avez-vous remarqué que si vous tournez "// build /" à l'envers, vous obtenez "/ plinq //" ;-)

Nicholas Butler
la source