Skip to main content
Le Microsoft Game Development Kit (GDK) implémente un nouveau modèle pour les API asynchrones qui répond aux commentaires que nous avons reçus des développeurs de jeux concernant le modèle asynchrone implémenté dans le cadre du modèle de programmation ERA de la XBOX One. Notre objectif est que ce nouveau modèle soit beaucoup plus facile à intégrer dans les architectures de jeu classiques et qu’il offre aux développeurs de jeux le haut degré de contrôle qu’ils ont demandé. Cette rubrique décrit ce modèle de conception et propose une bibliothèque pouvant être utilisée pour implémenter des modèles asynchrones.

Modèle conceptuel

La programmation asynchrone dans le Microsoft Game Development Kit (GDK) se divise en 2 composants principaux : les tâches et les files d’attente de tâches. Bien que les bibliothèques offrent davantage de fonctionnalités, l’ensemble du modèle conceptuel repose sur ces 2 composants principaux. Une tâche est un ensemble unique de travail asynchrone qui peut être démarré, dont l’état peut être vérifié, qui peut éventuellement être annulé, qui se termine et qui retourne ses informations d’achèvement. Dans le modèle du Microsoft Game Development Kit (GDK), les tâches se composent de deux corps : le rappel de travail et le rappel d’achèvement. Cela permet un contrôle accru, comme un traitement entièrement parallèle ou un travail parallèle combiné à un achèvement monothread. Une file d’attente de tâches est un conteneur qui met en file d’attente les rappels de travail et d’achèvement en vue d’une exécution ultérieure. Une file d’attente de tâches contient deux files internes, appelées ports, qui gèrent séparément les rappels de travail et d’achèvement. Elles sont appelées port de travail et port d’achèvement. Figure 1. Diagramme d’une tâche et d’une file d’attente de tâches Diagramme d'une tâche et d'une file d'attente de tâches. Chaque port de la file d’attente de tâches est configuré différemment au moment de la création afin d’obtenir différents comportements d’exécution des rappels. Par exemple, le port de travail peut être configuré pour être asynchrone et le port d’achèvement pour s’exécuter en série sur un thread principal. Un paramètre manuel peut être défini pour permettre un contrôle total du comportement d’exécution. Les modes de configuration des ports sont expliqués ci-dessous. Lorsqu’une tâche asynchrone est démarrée, les rappels ne sont pas immédiatement mis en file d’attente dans la file d’attente de tâches. Un fournisseur asynchrone gère les changements d’état pour garantir que le travail est mis en file d’attente et distribué avant que le rappel d’achèvement ne soit mis en file d’attente et distribué. La file d’attente de tâches ne gère pas elle-même directement le threading. Elle s’appuie plutôt sur un appel externe pour distribuer ses ports. Les appels externes déterminent le comportement de threading et de concurrence. La file d’attente de tâches elle-même est entièrement thread-safe. Figure 2. Port distribué sur plusieurs threads Image montrant un port distribué sur plusieurs threads. C’est essentiellement tout ! Les rappels d’une tâche sont mis en file d’attente sur les ports de travail et d’achèvement d’une file d’attente de tâches, et cette file d’attente fait distribuer ces rappels d’une manière ou d’une autre. L’API contient toute une série de fonctionnalités pour gérer les files d’attente de tâches, vérifier l’état des rappels, suivre les données de travail, créer une gestion personnalisée des tâches, et plus encore. Les appels d’API asynchrones du Microsoft Game Development Kit (GDK) implémentent toujours le rappel de travail en interne, et les rappels d’achèvement sont toujours facultatifs. Pour une utilisation au-delà des appels asynchrones du Microsoft Game Development Kit (GDK), vous devez fournir le rappel de travail.

Exigences

Les développeurs de jeux ont énoncé les exigences suivantes pour les appels d’API.
  1. Préférer les appels synchrones aux appels asynchrones
  2. Fournir un mode asynchrone avec interrogation
  3. Fournir un mode asynchrone avec rappels
  4. Permettre de contrôler le thread sur lequel s’exécute le travail asynchrone
  5. Permettre de contrôler le thread sur lequel s’exécutent les rappels d’achèvement

Types d’API

Le Microsoft Game Development Kit (GDK) s’efforce d’être très simple dans la conception de ses API. Les développeurs de jeux sont experts dans l’optimisation de leur code pour tirer le meilleur parti du matériel. Nous leur donnons le contrôle chaque fois que possible. Les implémentations d’API se répartissent dans les types suivants.
  • Sûres pour les opérations critiques en temps : une API sûre pour les opérations critiques en temps peut être appelée sur un thread critique en temps. Notez que, bien que cela signifie généralement que l’API est triviale ou très rapide, le concept clé est que les caractéristiques de performances de l’API sont constantes. Ces API sont toujours synchrones et n’ont jamais besoin d’une version asynchrone. Ces API doivent être documentées comme sûres pour les opérations critiques en temps.
  • Non sûres pour les opérations critiques en temps : ces API ne peuvent pas être appelées en toute sécurité depuis le thread de rendu. Leurs caractéristiques de performances peuvent varier considérablement. La plupart des API entrent dans cette catégorie.
  • Asynchrones : ces API sont asynchrones par nature, comme un appel de service web. Elles utilisent le modèle asynchrone décrit dans cette rubrique. Les API asynchrones sont moins courantes dans le Microsoft Game Development Kit (GDK) que dans le modèle de programmation ERA de la XBOX One : une API asynchrone est généralement de longue durée et annulable. À l’exception de quelques cas d’usage spécifiques, les API asynchrones disposent d’une version synchrone non sûre pour les opérations critiques en temps. L’appel d’une API asynchrone doit toujours être sûr pour les opérations critiques en temps.
  • Notifications : les notifications sont périodiques par nature et n’ont pas de fin définie. Elles sont liées aux API asynchrones, mais en raison de leur nature périodique, elles doivent se présenter et se comporter différemment pour les développeurs. L’inscription à une notification doit toujours être sûre pour les opérations critiques en temps.

Modèle d’API asynchrone

Le Microsoft Game Development Kit (GDK) introduit un modèle d’API asynchrone à usage général que les composants du Microsoft Game Development Kit (GDK) peuvent utiliser pour fournir une prise en charge asynchrone cohérente. Au cœur de ce modèle se trouve une structure semblable à OVERLAPPED appelée XAsyncBlock :
Un XAsyncBlock est une structure fournie par l’appelant. L’appelant renseigne les champs facultatifs de cette structure, comme indiqué dans le tableau suivant. Les champs Internal sont utilisés par le système et ne doivent pas être modifiés. Les champs de cette structure définissables par l’utilisateur ne doivent pas être modifiés pendant une opération asynchrone. Un XAsyncBlock doit rester en mémoire pendant toute la durée de vie de l’opération asynchrone. Si le XAsyncBlock est alloué dynamiquement, le rappel d’achèvement est le moment le plus tôt où il peut être supprimé. En plus de XAsyncBlock, il existe un petit nombre d’API d’assistance, présentées ci-dessous.
XAsyncGetStatus retourne l’état d’un appel asynchrone. Au début de l’appel, cet état est E_PENDING. Il devient S_OK ou une erreur spécifique une fois l’appel terminé. Si l’appel est annulé, la fonction retourne E_ABORT. XAsyncGetResultSize retourne la taille de mémoire tampon requise pour obtenir les résultats de l’appel. L’API réelle permettant de récupérer les résultats est adaptée à chaque appel asynchrone. XAsyncCancel peut être utilisé pour annuler un appel. L’annulation dépend de l’opération annulée et peut se produire de manière synchrone, asynchrone ou pas du tout. Si une opération est annulée, XAsyncGetResult, XAsyncGetResultSize ou XAsyncGetStatus retourne E_ABORT. Un appel annulé signale le paramètre XAsyncCompletionRoutine du XAsyncBlock et appelle son rappel. XAsyncRun est une méthode d’assistance qui peut exécuter n’importe quel code de manière asynchrone.

Utilisation de l’API asynchrone

Commençons par examiner une API synchrone dans l’exemple de code suivant.
Cette API appelle un service web pour déterminer la quantité de stockage de sauvegarde de jeu encore disponible. Pour ajouter la prise en charge asynchrone, nous déclarons une paire de nouvelles API.
XGameSaveGetRemainingQuotaAsync retourne S_OK si l’appel asynchrone a été lancé (comme cette API est uniquement asynchrone, il n’y a aucun intérêt à retourner E_PENDING). XGameSaveGetRemainingQuotaResult retourne E_PENDING jusqu’à ce que l’appel soit terminé. Voyons ce que cela donne en pratique.
Les XAsyncBlocks nécessitent tous une file d’attente de tâches (décrite ci-après), qui contrôle où et comment l’appel asynchrone est exécuté. Une file d’attente de tâches à l’échelle du processus est utilisée si aucune n’est fournie. Notez que le XAsyncBlock doit rester en mémoire pendant toute la durée de l’appel asynchrone. Dans cet exemple, il a été alloué dynamiquement et supprimé dans le rappel d’achèvement. Il pourrait également être stocké en tant que variable globale ou membre. Le comportement est indéfini si le même XAsyncBlock est utilisé pour plusieurs appels asynchrones simultanément. XGameSaveGetRemainingQuotaResult termine le cycle d’un appel asynchrone. Il libère les données internes du bloc asynchrone, de sorte que le bloc peut désormais être utilisé pour un nouvel appel. Les appels suivants à XGameSaveGetRemainingQuotaResult échouent. XGameSaveGetRemainingQuotaAsync et XGameSaveGetRemainingQuotaResult sont également associés à l’intérieur du bloc asynchrone : une erreur se produit si vous associez un appel asynchrone à une API de résultat qui ne lui correspond pas. Si un appel asynchrone n’a pas de charge utile de données, c’est-à-dire si seul l’état HRESULT est important, définissez une méthode Result qui prend uniquement le bloc asynchrone, comme suit.

Contrôle de la distribution du travail

Quel thread a effectué le travail asynchrone dans les appels précédents ? Quel thread a appelé le rappel d’achèvement ? Cela est déterminé par la file d’attente de tâches affectée au XAsyncBlock. Les files d’attente de tâches possèdent deux « ports » : un port de travail et un port d’achèvement. Chaque port possède un mode de distribution qui détermine la façon dont les rappels mis en file d’attente sur un port sont traités. Il existe plusieurs modes de distribution.
  • Pool de threads : les rappels mis en file d’attente dans une file de pool de threads sont exécutés sur le pool de threads système. Le pool de threads appelle les rappels en parallèle, en prenant tour à tour un appel à exécuter dans la file d’attente à mesure que des threads du pool deviennent disponibles.
  • Pool de threads sérialisé : les rappels sont mis en file d’attente et exécutés sur le pool de threads, mais un seul à la fois.
  • Manuel : les rappels mis en file d’attente dans une file manuelle ne sont pas automatiquement distribués. Il revient au développeur de les distribuer sur le thread de son choix.
  • Immédiat : le mode de distribution immédiat ne met rien en file d’attente. Il exécute immédiatement l’appel sur le thread qui a soumis le rappel.
Une file d’attente de tâches de processus par défaut est configurée de sorte que les ports de travail et les ports d’achèvement soient distribués via le pool de threads système. Cette file d’attente de tâches de processus est utilisée si aucun paramètre de file d’attente n’est transmis dans le XAsyncBlock. Un jeu peut également désactiver la file d’attente de tâches du processus, ce qui impose de transmettre une file d’attente dans le XAsyncBlock. Nous nous attendons à ce que de nombreux développeurs choisissent le mode de distribution manuel afin d’exercer un contrôle total sur le moment et l’endroit où le travail asynchrone et les rappels d’achèvement s’exécutent. Pour plus d’informations sur les files d’attente de tâches, consultez Conception de la file d’attente de tâches asynchrones.

Notifications

Une notification peut ne pas avoir de fin et peut être appelée de nombreuses fois. Les notifications doivent prendre en charge un sous-ensemble des exigences d’un appel asynchrone.
  1. Mode asynchrone avec interrogation
  2. Mode asynchrone avec rappels
  3. Contrôle du thread sur lequel les rappels se produisent
Les notifications utilisent une file d’attente de tâches pour permettre au développeur de contrôler le thread de rappel, mais elles n’utilisent pas de blocs asynchrones : elles sont conçues pour ressembler davantage à des événements standard, avec des méthodes Register et Unregister.
  • Une méthode Register qui prend les paramètres propres à l’appel, une file d’attente de tâches, un contexte void facultatif et un pointeur de rappel fortement typé. Le dernier paramètre est un paramètre out qui retourne un jeton.
  • Une méthode Unregister qui prend le contexte propre à l’appel et le jeton.
  • L’interrogation est prise en charge en ajoutant une méthode distincte, sans lien avec le rappel de notification.
Examinons l’exemple suivant, qui pourrait récupérer des messages Windows.
Notez que dans cet exemple, UnregisterMessageAvailable prend un dernier paramètre « wait » et retourne un bool. Cela permet aux appelants de décider comment gérer la désinscription pendant qu’un appel est en cours.

Bibliothèque asynchrone

Pour faciliter la création d’API cohérentes qui prennent en charge le modèle asynchrone, nous fournissons une bibliothèque qui peut être utilisée pour implémenter la « plomberie asynchrone » d’une API. L’API de la bibliothèque se présente comme suit.
Cette API utilise un seul rappel, combiné à une valeur d’opération qui indique pourquoi l’API est appelée. Il existe également une seule structure de données, renseignée au fur et à mesure de la progression de l’appel. Pour utiliser cette API, procédez comme suit.
  1. Appelez XAsyncBegin avec le bloc asynchrone transmis par l’appelant, et fournissez un rappel qui fournit l’implémentation.
  2. Effectuez le travail asynchrone de l’appel. Si vous devez exécuter le travail sur un thread de travail, appelez XAsyncSchedule. Si vous pouvez effectuer le travail à l’aide de primitives asynchrones du système d’exploitation et configurer ces primitives suffisamment rapidement pour rester sûr pour les opérations critiques en temps, c’est préférable.
  3. Si vous devez appeler un autre travail asynchrone à partir d’un rappel de thread de travail, vous pouvez retourner E_PENDING depuis le thread de travail. Vous pouvez également appeler XAsyncSchedule depuis un thread de travail pour replanifier du travail supplémentaire.
  4. Lorsque tout le travail est terminé, appelez XAsyncComplete.
  5. Fournissez un wrapper fortement typé autour de XAsyncGetResult pour retourner les résultats.
  6. Si votre appel asynchrone n’a pas de charge utile de données, vous devez fournir un wrapper fortement typé autour de XAsyncGetStatus et transmettre zéro comme taille de mémoire tampon requise à XAsyncComplete.
Le rappel du fournisseur asynchrone est appelé avec les opérations suivantes.
  • Begin Un fournisseur asynchrone est appelé avec ce code d’opération pendant XAsyncBegin. Si le fournisseur implémente ce code d’opération, il doit démarrer sa tâche asynchrone en appelant XAsyncSchedule ou par des moyens externes. Ce rappel est appelé de manière synchrone dans la chaîne d’appels de XAsyncBegin ; il ne doit donc jamais être bloquant.
  • DoWork Appelé lorsque XAsyncSchedule a été appelé pour planifier un travail asynchrone à l’aide de la file d’attente de tâches. La fonction du fournisseur effectue tout le travail nécessaire. Une fois terminé, elle appelle XAsyncComplete avec le code de résultat et la taille de la charge utile de données, qui peut être zéro si l’appel ne produit aucune charge utile de données. Si du travail asynchrone supplémentaire doit être effectué, le fournisseur peut planifier ce travail et doit retourner E_PENDING.
  • GetResult Appelé pour récupérer le résultat de l’appel. Comme la taille des données est transmise à XAsyncComplete lors de l’achèvement de l’appel, aucune vérification des arguments n’est nécessaire ici : toutes les mémoires tampons et leurs tailles ont été vérifiées par la bibliothèque.
  • Cancel Appelé lorsque l’utilisateur annule un appel asynchrone. Si l’appel peut être annulé, annulez-le et appelez XAsyncComplete avec E_ABORT comme code de résultat.
  • Cleanup Appelé lorsque l’appel est entièrement terminé et que le fournisseur peut supprimer toute mémoire dynamique.
Un fournisseur asynchrone n’a besoin d’implémenter que les opérations dont il a besoin. Par exemple, une E/S asynchrone non annulable qui ne nécessite aucun nettoyage n’a besoin d’implémenter que GetResult. Voici un exemple de méthode FactorialAsync qui calcule une factorielle de manière asynchrone.

Documentation de référence de l’API

Voir aussi

Objectifs de conception et améliorations de la programmation asynchrone Conception de la file d’attente de tâches asynchrones
Last modified on October 6, 2026