Skip to main content
O Microsoft Game Development Kit (GDK) implementa um novo padrão para APIs assíncronas que atende aos comentários que recebemos de desenvolvedores de jogos em relação ao padrão assíncrono implementado como parte do modelo de programação ERA do XBOX One. Nosso objetivo é que esse novo padrão seja muito mais fácil de integrar a arquiteturas de jogos típicas e ofereça aos desenvolvedores de jogos o alto grau de controle que eles solicitaram. Este tópico descreve esse padrão de design e oferece uma proposta de biblioteca que pode ser usada para implementar padrões assíncronos.

Modelo conceitual

A programação assíncrona no Microsoft Game Development Kit (GDK) é dividida em 2 componentes principais: tarefas e filas de tarefas. Embora haja mais funcionalidades nas bibliotecas, todo o modelo conceitual utiliza esses 2 componentes principais. Uma tarefa é um conjunto único de trabalho assíncrono que pode ser iniciado, ter seu status verificado, possivelmente ser cancelado, ser concluído e retornar suas informações de conclusão. No modelo do Microsoft Game Development Kit (GDK), as tarefas são compostas de dois corpos: o retorno de chamada de trabalho e o retorno de chamada de conclusão. Isso permite mais controle, como processamento totalmente paralelo ou trabalho paralelo combinado com conclusão em thread único. Uma fila de tarefas é um contêiner que enfileira retornos de chamada de trabalho e de conclusão para execução posterior. Há duas filas internas em uma fila de tarefas, chamadas portas, que tratam os retornos de chamada de trabalho e de conclusão separadamente. Elas são chamadas de porta de trabalho e porta de conclusão. Figura 1. Diagrama de uma tarefa e de uma fila de tarefas Diagrama de uma tarefa e de uma fila de tarefas. Cada porta da fila de tarefas é configurada de forma diferente no momento da criação para criar diferentes comportamentos de execução de retornos de chamada. Por exemplo, a porta de trabalho pode ser configurada para ser assíncrona e a porta de conclusão pode ser configurada para ser executada em série em um thread principal. Uma configuração manual pode ser definida para permitir controle total sobre o comportamento de execução. Os modos de configuração das portas são explicados abaixo. Quando uma tarefa assíncrona é iniciada, os retornos de chamada não são enfileirados na fila de tarefas imediatamente. Um provedor assíncrono trata as alterações de estado para garantir que o trabalho seja enfileirado e despachado antes que o retorno de chamada de conclusão seja enfileirado e despachado. A fila de tarefas não trata diretamente o threading. Em vez disso, ela depende de uma chamada externa para despachar suas portas. As chamadas externas determinam o comportamento de threading e simultaneidade. A própria fila de tarefas é totalmente thread-safe. Figura 2. Porta sendo despachada em vários threads Imagem que mostra uma porta sendo despachada em vários threads. Basicamente, é isso! Os retornos de chamada de uma tarefa são enfileirados nas portas de trabalho e de conclusão de uma fila de tarefas, e essa fila de tarefas tem esses retornos de chamada despachados de alguma maneira. A API contém um conjunto completo de funcionalidades para gerenciar filas de tarefas, verificar o status de retornos de chamada, acompanhar dados de trabalho, criar tratamento personalizado de tarefas e muito mais. As chamadas de API assíncronas do Microsoft Game Development Kit (GDK) sempre implementam o retorno de chamada de trabalho internamente, e os retornos de chamada de conclusão são sempre opcionais. Para usos além das chamadas assíncronas do Microsoft Game Development Kit (GDK), você deve fornecer o retorno de chamada de trabalho.

Requisitos

Os desenvolvedores de jogos listaram os seguintes requisitos para chamadas de API.
  1. Preferir chamadas síncronas a chamadas assíncronas
  2. Fornecer modo assíncrono com sondagem
  3. Fornecer modo assíncrono com retornos de chamada
  4. Fornecer controle sobre em qual thread o trabalho assíncrono é executado
  5. Fornecer controle sobre em qual thread os retornos de chamada de conclusão são executados

Tipos de APIs

O Microsoft Game Development Kit (GDK) se esforça para ser muito simples no design de suas APIs. Os desenvolvedores de jogos são especialistas em ajustar seu código para maximizar o uso do hardware. Damos controle a eles sempre que possível. As implementações de API se dividem nos tipos a seguir.
  • Seguras para tempo crítico: Uma API segura para tempo crítico é aquela que pode ser chamada em um thread sensível ao tempo. Observe que, embora isso geralmente signifique que a API é trivial ou muito rápida, o conceito-chave é que as características de desempenho da API são consistentes. Elas são sempre síncronas e nunca precisam ter uma versão assíncrona. Essas APIs devem ser documentadas como seguras para tempo crítico.
  • Não seguras para tempo crítico: Não é seguro chamar essas APIs a partir do thread de renderização. Suas características de desempenho podem variar amplamente. A maioria das APIs se enquadra nessa categoria.
  • Assíncronas: Essas APIs são assíncronas por natureza, como uma chamada de serviço web. Elas usam o padrão assíncrono descrito neste tópico. As APIs assíncronas não são tão comuns no Microsoft Game Development Kit (GDK) quanto no modelo de programação ERA do XBOX One: uma API assíncrona geralmente é de longa duração e cancelável. Exceto em alguns casos de uso específicos, as APIs assíncronas terão uma versão síncrona não segura para tempo crítico. Chamar uma API assíncrona deve sempre ser seguro para tempo crítico.
  • Notificações: As notificações são periódicas por natureza e não têm um fim definido. Elas estão relacionadas às APIs assíncronas, mas, devido à sua natureza periódica, devem ter aparência e comportamento diferentes para os desenvolvedores. O registro para uma notificação deve sempre ser seguro para tempo crítico.

Padrão de API assíncrona

O Microsoft Game Development Kit (GDK) introduz um padrão de API assíncrona de uso geral que os componentes do Microsoft Game Development Kit (GDK) podem usar para fornecer suporte assíncrono consistente. No centro dele está uma estrutura semelhante a OVERLAPPED chamada XAsyncBlock:
Um XAsyncBlock é uma estrutura fornecida pelo chamador. O chamador preenche os campos opcionais dessa estrutura, conforme mostrado na tabela a seguir. Os campos Internal são usados pelo sistema e não devem ser modificados. Os campos configuráveis pelo usuário nessa estrutura não devem ser modificados durante uma operação assíncrona. Um XAsyncBlock deve permanecer na memória durante todo o tempo de vida da operação assíncrona. Se o XAsyncBlock for alocado dinamicamente, o retorno de chamada de conclusão é o momento mais cedo em que ele pode ser excluído. Além do XAsyncBlock, há um pequeno número de APIs auxiliares, mostradas a seguir.
XAsyncGetStatus retorna o status de uma chamada assíncrona. Quando a chamada começa, esse status é E_PENDING. Ele muda para S_OK ou para um erro específico quando concluída. Se a chamada for cancelada, ela retornará E_ABORT. XAsyncGetResultSize retorna o tamanho de buffer necessário para obter os resultados da chamada. A API real para buscar os resultados é adaptada a cada chamada assíncrona. XAsyncCancel pode ser usada para cancelar uma chamada. O cancelamento depende da operação que está sendo cancelada e pode ocorrer de forma síncrona, assíncrona ou nem ocorrer. Se uma operação for cancelada, XAsyncGetResult, XAsyncGetResultSize ou XAsyncGetStatus retornarão E_ABORT. Uma chamada cancelada sinaliza o parâmetro XAsyncCompletionRoutine do XAsyncBlock e invoca seu retorno de chamada. XAsyncRun é um método auxiliar que pode executar qualquer código de forma assíncrona.

Uso da API assíncrona

Primeiro, vamos analisar uma API síncrona no exemplo de código a seguir.
Essa API chama um serviço web para determinar quanto armazenamento de jogos salvos ainda resta. Para adicionar suporte assíncrono, declaramos um par de novas APIs.
XGameSaveGetRemainingQuotaAsync retorna S_OK se a chamada assíncrona tiver sido iniciada (como essa API é somente assíncrona, não há valor em retornar E_PENDING). XGameSaveGetRemainingQuotaResult retorna E_PENDING até que a chamada seja concluída. Vamos ver isso na prática, conforme mostrado a seguir.
Todos os XAsyncBlocks exigem uma fila de tarefas (descrita a seguir), que controla onde e como a chamada assíncrona é executada. Uma fila de tarefas de todo o processo é usada se nenhuma for fornecida. Observe que o XAsyncBlock precisa permanecer na memória durante toda a vida da chamada assíncrona. Neste exemplo, ele foi alocado dinamicamente e excluído no retorno de chamada de conclusão. Ele também poderia ser armazenado como uma variável global ou membro. Ocorre um comportamento indefinido se o mesmo XAsyncBlock for usado para mais de uma chamada assíncrona ao mesmo tempo. XGameSaveGetRemainingQuotaResult conclui o ciclo de uma chamada assíncrona. Ela libera os dados internos do bloco assíncrono, de modo que o bloco agora pode ser usado para uma nova chamada. Chamadas subsequentes a XGameSaveGetRemainingQuotaResult falham. XGameSaveGetRemainingQuotaAsync e XGameSaveGetRemainingQuotaResult também são pareadas dentro do bloco assíncrono: ocorrerá um erro se você combinar uma chamada assíncrona com outra API de resultado incompatível. Se uma chamada assíncrona não tiver carga de dados, ou seja, se apenas o status HRESULT for importante, defina um método Result que receba apenas o bloco assíncrono, conforme mostrado a seguir.

Controlando o despacho de trabalho

Qual thread executou o trabalho assíncrono nas chamadas anteriores? Qual thread invocou o retorno de chamada de conclusão? Isso é decidido pela fila de tarefas atribuída ao XAsyncBlock. As filas de tarefas têm duas “portas”: uma porta de trabalho e uma porta de conclusão. Cada porta tem um modo de despacho que determina como os retornos de chamada enfileirados em uma porta são processados. Há vários modos de despacho.
  • Pool de threads: Os retornos de chamada enfileirados em uma fila de pool de threads são executados no pool de threads do sistema. O pool de threads invoca as chamadas em paralelo, retirando uma chamada da fila para executar, uma de cada vez, à medida que os threads do pool ficam disponíveis.
  • Pool de threads serializado: Os retornos de chamada são enfileirados e executados no pool de threads, mas um de cada vez.
  • Manual: Os retornos de chamada enfileirados em uma fila manual não são despachados automaticamente. Cabe ao desenvolvedor despachá-los em qualquer thread que desejar.
  • Imediato: O modo de despacho imediato não usa fila. Ele executa imediatamente a chamada no thread que enviou o retorno de chamada.
Há uma fila de tarefas de processo padrão configurada para que tanto as portas de trabalho quanto as portas de conclusão sejam despachadas pelo pool de threads do sistema. Essa fila de tarefas do processo é usada se nenhum parâmetro de fila for passado no XAsyncBlock. Um jogo também pode desabilitar a fila de tarefas do processo, exigindo que uma fila seja passada no XAsyncBlock. Esperamos que muitos desenvolvedores escolham o modo de despacho manual para exercer controle total sobre quando e onde o trabalho assíncrono e os retornos de chamada de conclusão são executados. Para obter detalhes sobre filas de tarefas, consulte Design da fila de tarefas assíncronas.

Notificações

Uma notificação pode não ter fim e pode ser chamada muitas vezes. As notificações devem oferecer suporte a um subconjunto dos requisitos de uma chamada assíncrona.
  1. Modo assíncrono com sondagem
  2. Modo assíncrono com retornos de chamada
  3. Controle sobre em qual thread os retornos de chamada ocorrem
As notificações usam uma fila de tarefas para permitir que o desenvolvedor controle o thread do retorno de chamada, mas, fora isso, não usam blocos assíncronos: elas são projetadas para se parecer mais com eventos padrão, com métodos Register e Unregister.
  • Um método Register que recebe quaisquer parâmetros específicos da chamada, uma fila de tarefas, um contexto void opcional e um ponteiro de retorno de chamada fortemente tipado. O último parâmetro é um parâmetro out que retorna um token.
  • Um método Unregister que recebe qualquer contexto específico da chamada e o token.
  • A sondagem é suportada pela adição de um método separado que não está relacionado ao retorno de chamada de notificação.
Vamos analisar o exemplo a seguir, que poderia buscar mensagens do Windows.
Observe que, neste exemplo, UnregisterMessageAvailable recebe um parâmetro final “wait” e retorna um bool. Isso permite que os chamadores decidam como tratar o cancelamento do registro enquanto uma chamada está sendo invocada.

Biblioteca assíncrona

Para facilitar a criação de APIs consistentes que ofereçam suporte ao padrão assíncrono, fornecemos uma biblioteca que pode ser usada para implementar a “infraestrutura assíncrona” de uma API. A API da biblioteca tem a seguinte aparência.
Essa API usa um único retorno de chamada, combinado com um valor de operação que indica por que a API está sendo chamada. Há também uma única estrutura de dados que é preenchida à medida que a chamada avança. Para usar essa API, faça o seguinte.
  1. Chame XAsyncBegin com o bloco assíncrono passado pelo chamador e forneça um retorno de chamada que forneça a implementação.
  2. Execute o trabalho assíncrono da chamada. Se você precisar executar o trabalho em um thread de trabalho, chame XAsyncSchedule. Se você puder realizar o trabalho usando primitivos assíncronos do sistema operacional e configurar esses primitivos rápido o suficiente para continuar seguro para tempo crítico, essa é a opção preferencial.
  3. Se você precisar invocar outro trabalho assíncrono a partir de um retorno de chamada de thread de trabalho, poderá retornar E_PENDING do worker. Você também pode chamar XAsyncSchedule de dentro de um worker para reagendar trabalho adicional.
  4. Quando todo o trabalho estiver concluído, chame XAsyncComplete.
  5. Forneça um wrapper fortemente tipado em torno de XAsyncGetResult para retornar os resultados.
  6. Se a sua chamada assíncrona não tiver carga de dados, você deverá fornecer um wrapper fortemente tipado em torno de XAsyncGetStatus e passar zero como o tamanho de buffer necessário para XAsyncComplete.
O retorno de chamada do provedor assíncrono é invocado com as seguintes operações.
  • Begin Um provedor assíncrono é invocado com esse opcode durante XAsyncBegin. Se o provedor implementar esse opcode, ele deverá iniciar sua tarefa assíncrona chamando XAsyncSchedule ou por meios externos. Esse retorno de chamada é chamado de forma síncrona na cadeia de chamadas de XAsyncBegin, portanto, nunca deve bloquear.
  • DoWork Chamado nos casos em que XAsyncSchedule foi chamado para agendar trabalho assíncrono usando a fila de tarefas. A função do provedor faz todo o trabalho necessário. Ao concluir, ela chama XAsyncComplete com o código de resultado e o tamanho da carga de dados, que pode ser zero se não houver carga de dados da chamada. Se for necessário realizar mais trabalho assíncrono, o provedor poderá agendar esse trabalho e deverá retornar E_PENDING.
  • GetResult Chamado para buscar o resultado da chamada. Como o tamanho dos dados é passado para XAsyncComplete durante a conclusão da chamada, nenhuma verificação de argumentos é necessária aqui: todos os buffers e tamanhos de buffer foram verificados pela biblioteca.
  • Cancel Chamado quando o usuário cancela uma chamada assíncrona. Se a chamada puder ser cancelada, cancele-a e chame XAsyncComplete com E_ABORT como código de resultado.
  • Cleanup Chamado quando a chamada foi totalmente concluída e o provedor pode excluir qualquer memória dinâmica.
Um provedor assíncrono só precisa implementar as operações de que necessita. Por exemplo, uma E/S assíncrona não cancelável que não tem limpeza só precisa implementar GetResult. Veja a seguir um exemplo de um método FactorialAsync que implementa o fatorial de forma assíncrona.

Documentação de referência da API

Consulte também

Metas de design e melhorias da programação assíncrona Design da fila de tarefas assíncronas
Last modified on October 6, 2026