Representação do mundo de jogos Bomberman-like

Existem aqueles problemas não-imediatos como desempenho, elegância ou outros que apenas ficam no caminho para a primeira solução funcional. Há aqueles que condenam a prática de preocupação prévia e insistem em resultados imediatos. Estou trabalhando no desenvolvimento de um jogo similar ao Bomberman e precisei desenvolver uma solução para representar o mapa onde o mundo do jogo é executado. E, é claro, acabei tendo o luxo de escolher a abordagem de desenvolvimento onde a preocupação com problemas não-imediatos ocorre mais cedo que a entrega do produto.

Introdução

Para que o problema não fique muito abstrato, considere a imagem abaixo, retirada do jogo BlowThemAll.

shot-2015-05-09_14-08-52

O mapa do jogo possui uma implementação similar ao conceito de uma matriz, onde os elementos do mapa possuem uma posição <x, y> tal que essa posição não pode ultrapassar os limites da matriz e os índices x e y são números discretos, naturais. Cada posição no mapa é um tile e cada tile pode conter vários elementos.

O mapa contém elementos imóveis, tais como blocos a serem explodidos, e elementos móveis, tais como bombas e avatares. Avatar é um personagem que representa a conexão do jogador com o mundo virtual de BlowThemAll.

Há ainda blocos eternos, que não podem ser explodidos.

No jogo original, Bomberman, uma bomba explode e a explosão tem o alcance baseado em tiles. Itens aparecem somente em tiles específicos. Há muitas observações que sugerem fortemente o uso de uma matriz como uma representação eficiente para o jogo.

Problema

Jogando-se Bomberman é possível observar que um avatar pode estar entre dois tiles, então você não pode representá-los como um elemento em uma sequência que faz parte de algum tile, pois um avatar pode estar entre dois tiles. Logo, o avatar precisa ser independente dessa matriz e não pode ter sua posição representada por números discretos.

Abordagens iniciais

A primeira abordagem para o problema foi utilizar uma lista separada para esses elementos do mapa que podem assumir posições entre dois tiles e armazenar suas respectivas posições usando valores do tipo double (ou parecidos).

Entretanto, eu não me sinto confortável usando valores do tipo double em um jogo em rede. Primeiro, porque o comportamento de doubles não é trivial, e segundo, porque é difícil garantir reprodutibilidade/previsibilidade usando doubles, então  eu não quero lidar com o problema desses números sendo utilizados numa parte principal de um jogo em rede. Lembrando que o double que você está usando pode nem ser o IEEE floating point. É claro que eu iria ler o “what every computer scientist should know about floating point numbers” caso o projeto precisasse, mas eu queria, ao menos, considerar uma abordagem alternativa.

A minha ideia foi usar números discretos para as posições dos elementos não-fixos no mapa, e fazer numa escala diferente (se o mapa era 13×11 tiles, os elementos não-fixos estariam em uma matriz virtual 130×110) para permitir um valor mais granular (que emularia o fato do avatar poder estar entre dois tiles). Essa abordagem não me contentou, pois ela levantava outras perguntas (ex.: quão granular devo permitir a posição para permitir uma experiência de jogo agradável?), não era elegante e tirava a simplicidade que eu tinha na solução inicial, onde, era barata, a operação de encontrar quais os avatares eram atingidos por uma explosão.

Pensando nas restrições

Eu voltei ao problema inicial para pensar nas restrições que ele apresentava. A depender de suas restrições, o algoritmo poderia resolver um problema menor e mais simples. Foi essa a abordagem que me permitiu desenvolver a libdepixelize com menos código e mais rápido, e é isso que decidi também fazer no BlowThemAll, entender melhor o problema.

Daí, percebi algo enquanto jogava bomberman. O avatar pode sim, estar entre duas posições, mas caso ele esteja entre duas posições, ainda há um “tile principal” ao qual ele pertence. Isto é, ele só é atingido por uma explosão caso apenas um dos tiles, o “tile principal”, seja incendiado. Além disso, há a restrição de que ele só pode estar entre dois tiles, não entre quatro tiles. Todas essas informações me inspiraram o suficiente para pensar numa solução alternativa.

Solução final

A ideia-chave da solução que acabei adotando é armazenar os elementos na “matriz”, como estava inicialmente planejado, porém adicionar um atributo “filled”, que representava o quanto  daquele tile o elemento ocupa, e um atributo direção, que representa qual o próximo tile a ser ocupado, quando o elemento atual chegar no limite de desocupação do tile atual.

Conceitualmente, 1 representa o estado onde o elemento está ocupando somente aquele tile, 0 representa o estado onde ele não está ocupando nada daquele tile e temos também todos os valores intermediários (como ocorre em contas de probabilidade). Entretanto, queremos usar valores discretos, então usamos inteiros simples e usamos seus limites (0 e 255) para representar os limites conceituais (0 e 1).

Um elemento é transferido para o próximo tile, quando, conceitualmente, ele desocupa mais da metade do tile atual. Se mapearmos o comportamento conceitual para o comportamento em código, então haverá um desperdício de bits, pois o valor de filled nunca seria inferior a 127. Assim sendo, o valor 255 continua representando o conceitual 1, mas o valor 0 representa o conceitual 1/2.

E a solução ficou bem legal até, mas eu ainda queria mais. Uma granularidade de 256 valores possíveis parecia um desperdício para mim e eu queria usá-los para mais informações. Foi então que eu decidi separar preenchimento/ocupação/”filled” por dimensão. “dir” se transformou nos atributos “is_up” e “is_left”, enquanto “filled” se transformou nos atributos “y_filled” e “x_filled”. Usando campos de bits, consegui armazenar toda essa informação em um único byte.

Os atributos “is_up” e “is_left” podem lembrar o bit de sinal da representação de inteiros por complemento de 1, que desperdiça um valor possível, porém eu quero preservar a simetria, então esse desperdício é aceitável.

Essa separação por dimensão, entretanto, faz com que o objeto sempre assuma uma direção para cada dimensão. Se antes as opções de direção eram norte, sul, leste e oeste, agora as direções são nordeste, noroeste, sudeste, sudoeste. Essa característica não é um problema, mas é preciso manter ela em mente quando for implementar funcionalidades que dependem da direção do personagem (socos, por exemplo).

Outra consequência da separação é que o jogo volta a apresentar a possibilidade inicial onde um personagem pode assumir uma posição de estar entre quatro tiles. Essa é uma característica positiva, pois impõe menos restrições e dá mais liberdade artística aos autores de mapas. Essa possibilidade também está presente no jogo Bomberman Tournament e eu não quero fazer menos que os competidores.

E, assim, eu fiquei satisfeito, e surgiu o código do BlowThemAll para tratar mapas. É claro que eu não acho que usaria essa solução para um jogo estilo “The Legend of Zelda: A Link to the Past“, mas eu acho que ficou bem adequada a um jogo ao estilo de Bomberman. Pode ser que você ache interessante pesquisar também sobre Quadtree Collision detection.

Programação em tempo real

Caso não fosse um jogo em rede e que possui algumas características que eu não detalhei, eu estaria também mantendo a preocupação de fazer uma solução para programação em tempo real, afinal, é um jogo e queremos previsibilidade nos tempos de respostas máximos. Sem essa preocupação, a única modificação que pensei para melhorar a previsibilidade nos tempos de resposta seria usar a função vector::reserve para pré-alocar um pouco de memória para a lista de elementos em cada tile.

Final

E para acabar o post, deixo um vídeo de alguns segundos demonstrando o resultado:

Game Jam durante o final de semana

Esse final de semana participei de um evento de game jam. Sinceramente, não pesquisei sobre esse tipo de evento e posso estar sendo infiel aos valores-raízes e desvirtuando o mesmo, pois tudo que sei é informação de segunda-mão, mas, mesmo assim, tive uma experiência positiva com a minha visão do que é um game jam. Ao final do processo, acabei conhecendo como é um game jam e com uma nova sugestão de evento para quem gosta de programar.

O que é?

Game Jam é um evento onde pessoas se reúnem num mesmo local para fazer um jogo do zero. Elas se reúnem, formam times e criam um jogo. O desenvolvimento do jogo deveria durar três dias e, ao final do processo, deveria haver um produto “mostrável”.

Por que?

É divertido, e você se desapega daquelas preocupações de código elegante e reaproveitável que faz verificações de segurança para terminar algo rápido. Ter algo que você fez e está funcionando é algo legal. E o ambiente onde há outras pessoas programando, buscando o mesmo objetivo que você, estimula você a ficar determinado em sua tarefa. E também é uma desculpa para trabalhar com tecnologias novas e melhorar suas habilidades como programador.

Meu game jam

Esse game jam que participei foi uma coisa pequena que organizei na minha casa e chamei uns amigos. Foram três times ao total (sendo que dois times, incluindo o meu, eram formados por apenas um membro).

photo420830555499767723

Time azul

O time azul desenvolveu um jogo estilo Beat ’em up escrito em Python 3 e pygame.

Time vermelho

IMG_20150502_200110

Captura de tela de 2015-05-02 20_06_55

O time vermelho desenvolveu um jogo de asteroides que também foi escrito em pygame, mas utilizando Python 2.

Time laranja

photo284176472941373377

O meu time, que no caso sou só eu mesmo, fez um jogo estilo bomberman via rede, escrito em C++11, Qt5 e Tufão 1.x.

Dicas

Lembre-se de se divertir, é o mais importante. Eu podia ter formado time com a outra pessoa que também ficou sem time, mas nós tínhamos expectativas diferentes e ambos estaríamos frustrados caso tivéssemos formado um time. No final, acho que todo mundo achou legal. Se divertir é importante.

Veredito

A experiência que tive com o evento foi positiva e é um evento que eu recomendo a todo mundo que gosta de programar. Vale a pena.

Sobre os jogos, creio que os participantes vão continuar trabalhando neles por um pouco mais de tempo, pelo menos, para ter algo polido e tal. O meu jogo, pelo menos, tem código ainda não público e você precisa de um manual de instruções para rodar o jogo sem esbarrar nos glitches e bugs.

Para o próximo, planejo fazer algo usando Rust, SDL2 e Chipmunk, três tecnologias com as quais não tenho experiência significativa e, no máximo, apenas uma ligeira familiaridade.

Dificuldade

O modo como as pessoas tratam o tema dificuldade me estimula a refletir. Dificuldade é importante?

Um ponto de vista que acho interessante é o do Bjarne Stroustrup, que divide complexidade, uma característica que é difícil de tratar, logo relacionado a dificuldade, em duas categorias, complexidade essencial e complexidade acidental:

“There are two kinds of complexity […] there are problems that are hard. There are problems that are complicated and you can’t reduce that […] but basically we have to figure it out what is the real problem, what is the real way of doing it, what is the simplest solution to the problem […] but a lot of complexity is introduced by the way we express our solutions”, Bjarne Stroustrup durante a palestra “Make Simple Tasks Simple!“, apresentada no CppCon 2014, ao argumentar sobre a diferença entre complexidade essencial e complexidade acidental

Basicamente, ele argumenta que a complexidade existe, ou da própria natureza do problema, que é difícil, ou pela forma que nós tratamos o problema. Ele chama a complexidade que não pode ser removida de complexidade essencial e a complexidade que nós introduzimos e é independente do problema de complexidade acidental.

Interajo com algumas pessoas que possuem a crença de que mais dificuldade implica em maior aprendizado. Seguindo essa filosofia (ou talvez tendo outros motivos), há professores que adicionam obstáculos (a complexidade acidental do Bjarne) para tornar a experiência de aula mais difícil, o que acaba tornando todo o processo mais ineficiente e deixando aquele gostinho de “desperdício de recursos”. Um exemplo dessa complexidade acidental é a distância que se cria entre o professor e o aluno, impedindo que o aluno descubra o porquê de ele estar errado, e desperdiçando a oportunidade que ele teria de aprender com o erro.

Há também os programadores que acreditam que, ao se programar em alto nível, diminuindo a dificuldade de desenvolver a solução, sua solução imediatamente torna-se menos performática. Essa é outra relação equivocada que vejo por aí. Uma referência legal em relação a esse ponto de vista é o comentário do JuciÊ na lista de C/C++ Brasil.

Um sentimento ruim de insatisfação surge em mim quando surge uma situação em que minha vida é tornada mais difícil sem que haja algum motivo digno. Bom, essa reflexão acaba por aqui.

Análise honesta do OpenPandora

Pandora é um console portátil cuja ideia nasceu em uns fóruns, após discussões sobre “como seria o console perfeito?”, onde cada um palpitava o que deveria haver. O projeto se tornou realidade e eu tive acesso ao console por bastante tempo. Nesse texto faço uma análise do console.

Pandora1ghz

Hardware

Nubs/analógicos

A qualidade dos analógicos é boa e adequada para um portátil (ocupam pouco espaço, mas não causam fadiga) e também possuem boa sensibilidade, mas infelizmente a funcionalidade de auto-calibragem dos mesmos (controlada por hardware e provavelmente imutável) é muito irritante. Ainda não perdi em nenhum jogo por causa disso, mas quando a calibração dá errado, você precisa fazer a “dancinha dos nubs” para eles voltarem ao normal e isso significa fugir do inimigo mais próximo em um jogo de ação até que os controles voltem ao normal. Mesmo com esse defeito, a presença dos nubs é um fator positivo e eu sentiria falta caso eles fossem removidos.

Apesar de seus problemas, os nubs são flexíveis, podendo ser utilizados como mouse ou como botões do mouse, dispensando muitas vezes o uso da touchscreen.

Meu conselho é que você prefira depender dos nubs para garantir precisão de ângulo (direção a ser apontada, por exemplo), do que depender deles para garantir precisão de intensidade (o módulo do vetor, intensidade da força, …).

Botões de jogo

O Pandora foi criado como um sistema para emular jogos e possui muitos dos clássicos botões (4 botões frontais, 2 botões de ombro, um direcional, …).

A qualidade dos botões é excelente. Após muitas e muitas horas de jogo, nunca aconteceu de algum botão “travar”/”emperrar” ou coisa do tipo comigo. E o direcional é excelente também, sendo fácil até de executar aqueles sequências “meia-lua etc”.

Touchscreen

A touchscreen é resistiva, o que pode ser chato para algumas pessoas. Quando eu coloquei uma película protetora, a precisão diminuiu nos cantos da tela e ficou difícil jogar jogos como The Legend of Zelda: Spirit Tracks.

Porta USB

O Pandora tem duas portas USB (uma porta USB-A e uma porta mini-AB OTG). Além da praticidade de poder usar hub, teclado mouse, hd externo e outros, você pode carregar via USB. Você pode até plugar seu robô feito com Arduino e deixar o Pandora com o seu poderoso processador de 1GHz executar os algoritmos mais custosos.

O motivo de eu ter reservado uma parte só para USB é que, no Pandora, ela encontra usos mais inusitados. Há, por exemplo, o software Master Control, que pode ser usado para fazer o Pandora funcionar como teclado, mouse e gamepad. Eu pretendia até fazer interfaces gráficas para auxiliar em fazer o Pandora funcionar como uma placa de som externa e como um leitor de CD/DVD virtual, usando os módulos já existentes do kernel.

A parte chata é que o padrão mini USB caiu em desuso e é meio difícil encontrar cabos para esse padrão. A sorte é que alguns produtos famosos, tipo o PS3, ainda fazem uso de miniUSB.

Bateria

O que eu tenho a comentar sobre a bateria é que ela dura bastante. A bateria vai durar tanto que você vai passar da hora de dormir só para ter o prazer de descarregar ela. Ela dura tanto que tem usuários usando o Pandora para carregar seus smartphones que possuem uma bateria inferior.

Mas apesar de ser muito boa, parece que os desenvolvedores do Pandora não se contentam facilmente, pois fizeram com que a bateria seja facilmente substituída, assim você pode ter umas baterias reservas já carregadas para quando for passar por um apocalipse zumbi longos períodos sem energia.

Áudio

Talvez uma das partes menos importantes em um console (ainda mais em um console portátil), mas apesar disso o áudio do Pandora é muito bom. O áudio do Pandora foi o que me fez perceber que os meus headphones precisam de um amplificador para eu extrair seu potencial (que não é necessário caso eu o plugue no Pandora).

Diferente de escutar músicas em um de meus outros dispositivos, no Pandora as músicas não travam nunca (a velocidade dos leitores de cartão de memória são muito boas), mesmo quando escuto músicas em um formato que costuma ocupar mais espaço/bytes, como o formato FLAC. Esse ainda é outro diferencial, como o sistema é aberto, qualquer um pode adicionar suporte a novos formatos de arquivos. Além de mp3, você pode abrir Ogg Vorbis, FLAC, o recente Opus e um monte de arquivos (coisa que normalmente só posso apreciar num computador, bem menos portável que um Pandora).

Ainda comparando com outro dispositivo que possuo, no Pandora eu não costumo escutar interferência de som enquanto o sistema funciona. No Pandora o único momento onde há uma pequena interferência de áudio é enquanto ele está carregando na tomada (e nem sempre acontece, pois ele costuma carregar enquanto eu durmo). Já no meu outro dispositivo que não é o Pandora, costumo escutar uma barulho de áudio que não é da música enquanto copio grandes arquivos entre pastas e em outras situações não incomuns.

Fugindo do tópico qualidade e entrando no tópico flexibilidade, o Pandora possui microfone embutido, mas também suporta entrada e saída de som através de suas várias conexões (entrada P2 com suporte a microfone, porta de expansão ou mesmo uma placa de som USB externa). O único inconveniente é que os headsets suportados pela entrada P2 foram projetados para o padrão, mas a Apple “criou” seu padrão não-padrão que, entretanto, foi seguido pela maior parte das empresas e, assim, tornou-se o padrão.

Juntando tudo isso, o melhor dispositivo para escutar música que tenho acesso é esse portátil, mas não me considero competente o suficiente no assunto, então deixo um link para a avaliação dos freaks do head-fi. Eu os cito usando o nome “freaks”, pois certa vez me deparei com um artigo que caracteriza um pouco de como eles se divertem durante o tempo livre.

Wifi

Há muitos relatos entre os primeiros usuários do dispositivo sobre problemas com o uso de wifi no aparelho, mas vou limitar a análise ao dispositivo ao qual tenho acesso e o veredito é que hardware wifi do Pandora é bom o suficiente, mas um pouco obscuro. Acabei de baixar momentos atrás um PND de 1.32 GiB do repositório oficial a uma velocidade média de 100 KiB/s e máxima de 900 KiB/s sem ter problemas de reiniciar o download por quedas de conexão. Não sei se é bom o suficiente para jogar online.

Com suporte obscuro eu quis dizer que as opções do Pandora são um pouco escondidas. Além de uma bateria boa, há bastante customização no software para permitir que você possa jogar o dia todo. No meio dessas customizações, foram desabilitados muitas das formas de comunicação (bluetooth, wifi, USB host) que você não usa o tempo todo, afim de maximizar a vida da bateria do aparelho. Essas customizações podem desafiar a vida do novo usuário, mas felizmente há um menu simples e de rápido acesso na barra de tarefas para você (re-)habilitar os meios de comunicação do Pandora.

Um fato curioso que talvez enriqueça a análise é que quando uso o Android o sistema se conecta na rede wifi mais rápido.

Qualidade

A carcaça do Pandora é feita de um plástico resistente. O meu ex-Pandora resistiu a algumas quedas, porém sempre que eu ia abrí-lo para corrigir alguma coisa, os parafusos acabavam luindo e eu tinha que destruir a carcaça (frágil ao calor) e colocar outra. O meu amigo que corrigiu o botão de ombro criticou a forma que o botão de ombro foi projetado, argumentando que tudo que é soldado a placa, deveria necessitar de uma pancada muito bruta para sair.

Sistema operacional

O Pandora, assim como grande parte dos dispositivos portáteis (smartphones inclusos), usa um processador ARM. Normalmente esses dispositivos dispensam um cooler e conseguem fazer um melhor uso da bateria. Sendo assim, um sistema operacional que suporte ARM é necessário e no Pandora esse sistema é o GNU/Linux. O GNU/Linux também é usado no Android, mas com uma série de variações/limitações. Algumas variações do Android são úteis e até acabam encontrando o caminho de volta para o kernel do Linux enquanto outras são prematuras e provavelmente só continuarão a ser usadas para manter retrocompatibilidade.

Um alerta antes que você prossiga: Eu vou ser um pouco mais técnico nessa seção e meu objetivo não será comentar sobre alguns itens que o usuário considera importante como facilidade de uso, mas, ao invés, comentar o que tecnicamente é interessante no sistema (seria algo como comentar o Unix, que ninguém usa, mas influenciou a maior parte dos sistemas operacionais que encontramos com facilidade hoje).

O sistema operacional do Pandora é derivado do OpenEmbedded, uma distribuição GNU/Linux destinada a dispositivos embarcados. O sistema boota rápido, possui um kernel até recente (3.2) e uma série de modificações para suportar melhor o Pandora (um kernel customizado com drivers específicos) e para melhor suportar a comunidade do Pandora (ambiente de aplicativos que facilita a vida de usuários que só querem colocar um monte de emuladores e jogar).

Sobre o OpenEmbedded em si, o sistema é subdocumentado (ainda mais para um usuário como eu, acostumado com o ArchLinux, que possui excelente documentação), possui uma lista de emails pouco ativa (eu participo dela) e parece utilizar um monte de tecnologias antigas e específicas. Se o sistema fosse melhor documentado, seus defeitos seriam reduzidos, pois qualquer usuário poderia se guiar e modificar o sistema. Seu gerenciador de pacotes foi forkado de um projeto morto usado por projetos antigos. Para não ficar muito decepcionado, você pode imaginar esse sistema como um sistema Debian que já está estável e vai receber poucas atualizações durante seu tempo de vida (com a diferença que o Debian é pelo menos uma ordem de magnitude melhor).

Sobre a versão customizada pela comunidade do Pandora, posso afirmar que é melhor mantida e recebe atualizações com uma frequência satisfatória. Em cima do sistema OpenEmbedded, foi criado um novo sistema de pacotes adequado para partições FAT (que normalmente são usadas em cartões de memória, mas carecem de um sistema de permissões adequado para sistemas GNU/Linux), onde você joga arquivos PND e já pode executá-los a partir do menu do Pandora. Há até uma interface gráfica minimalista criada pelo time do Pandora para se adaptar aos botões destinados a jogos.

O sistema PND é o sistema usado para empacotar jogos para o Pandora. O sistema, do ponto de vista do usuário, é muito simples de usar. Você copia o arquivo PND para uma pasta específica no cartão de memória e o software estará disponível alguns segundos após o cartão de memória ser inserido. Os dados do aplicativo são armazenados numa pasta separada (que também fica no cartão de memória) e os arquivos PND ficam intactos, puros. Não há necessidade de jogar o arquivo e executar um instalador ou coisa do tipo. É plug’n’play.

Os únicos poucos softwares que tive de instalar sem usar o sistema de PND, consumindo espaço na NAND interna, foram uma versão diferente do driver de vídeo, um pacote de codecs e o aplicativo timidity.

Uma fraqueza do sistema PND é que ele não é muito seguro, em relação a como ele isola os aplicativos que dispõe do restante do sistema. O sistema é bem menos ambicioso do que o pessoal do GNOME está tentando fazer.

O repositório é recheado de aplicativos úteis e são até bem mantidos. Para uma plataforma de jogos, eu gosto bastante do Pandora, apesar de haver pessoas no fórum afirmando utilizá-lo por outros motivos.

Os drivers que mais dão problemas, interessantemente, são aqueles para os quais não há muita documentação ou código-fonte aberto e isso inclui os drivers de vídeo, wi-fi e alguns outros. Dentro desse sistema, eu não recomendo que você alimente qualquer esperança de algum dia ver suporte a Wayland.

Uma coisa interessante é que o sistema faz uso de mtd, ubi e UBIFS, onde a memória flash é exposta “diretamente”, em vez de tentar se passar por um disco magnético.

E caso você não goste do sistema operacional padrão, pode substituí-lo ou mesmo instalar algum em um cartão de memória. Até um port do Android para Pandora, que roda bem suave.

Emuladores/jogos

O principal uso do Pandora é jogos, então é justo que eu reserve uma parte da análise para comentar sobre seu poder para jogar.

Os emuladores de plataformas antigas (como NES, SNES e alguns outros que não me recordo agora) possuem uma interface bem similar, o que me faz acreditar que sejam variações de emuladores que já existiam antes, mas que algum programador adaptou para o Pandora. No caso desses emuladores, eles costumam funcionar muito bem e ter suporte a save state.

Há também os emuladores que possuem interfaces próprias. No caso do emulador de N64, por exemplo, é mais difícil para interagir com sua interface. Entretanto, o emulador de N64 recebeu bastante investimento humano para ter uma versão decente no Pandora. Eu consegui me divertir jogando o começo de The Legend of Zelda: Ocarina of Time.

Há o emulador de PS1, que é muito bom também, tem as funcionalidades necessárias (trocar de CD sem recarregar o jogo, gerenciamento de memory cards, …) e tem uma interface bem simples de usar.

Para plataformas mais recentes que N64, normalmente a performance do Pandora não é boa o suficiente, porém há o emulador de NDS, que é incrível e não peca em nada. O emulador de NDS até deixa você mandar o sinal de uma das telas do NDS para uma TV enquanto o conteúdo da tela inferior fica na tela sensível ao toque do próprio Pandora. O emulador de NDS permite até que você mostre somente uma das telas, divida horizontalmente ou verticalmente, mostre uma das telas com o dobro do tamanho da outra e várias outras opções.

Suporte

O principal suporte é fórum do Pandora, que é bem ativo e não costuma esconder informações do usuário e é frequentado por algumas pessoas bem inteligentes, além de muitos outros usuários interessados em ajudar. Há até algumas modificações que você encontra por lá, tipo como adicionar suporte a “rumble feedback” no Pandora. Além do fórum, há também o canal IRC, que costuma estar ativo, mas não tanto quanto o fórum.

bastante documentação que você pode encontrar na wiki e o sistema é bem aberto. Você pode até baixar os arquivos STL, útil em combinação com impressoras 3D.

Peças de reposição podem ser conseguidas na loja DragonBox.

Um problema sério, entretanto, são as dificuldades em contribuir patches para o sistema operacional. Caso tenha interesse, o usuário que você deve procurar é o notaz.

Fatality

Eu gostei bastante do dispositivo, no tempo em que ele passou comigo. Pretendo algum dia testar seu sucessor que já está em desenvolvimento, o DragonBox Pyra, principalmente porque agora não estou com nenhuma placa de som que seja digna dos meus fones de ouvido.

E uma dica minha é, que caso tenham interesse no produto, evitem qualquer negociação com um certo Craig.

Meu ambiente Git

Há essas várias ferramentas para resolver o problema de versionamento de código-fonte. Lembro da época em que eu usava o svn, principal ferramenta do modelo centralizado, onde a ferramenta não me ajudava muito mais além do básico e eu usava o RabbitVCS para ter uma interface mais amigável, em contraste com meu uso atual da ferramenta git, onde eu exijo que meu ambiente não tenha nenhuma ferramenta que esconda a complexidade da Git, pois eu quero que todos os seus recursos estejam a minha disposição, sem risco de que qualquer um deles esteja ligeiramente menos poderoso. Após tanto tempo usando git, decidi que era hora de dedicar um post no meu blog sobre a ferramenta, descrevendo qual subconjunto do mesmo eu mais utilizo, algumas dicas, algumas explicações e mais um pouco.

Caso você ainda não saiba como utilizar a ferramenta git, sugiro que você utilize o minicurso interativo sem enrolação do Github, para aprender de forma acelerada e estar apto a se beneficiar dessa ferramenta o quanto antes.

Para reforçar os conceitos, é legal, além da perspectiva prática do treinamento, experimentar visualizações para reforçar os conceitos e talvez ter alguma epifania. Recomendo visitar o site http://www.wei-wang.com/ExplainGitWithD3/ e praticar nele os comandos que você acabou aprendendo para visualizar conceitos do git.

Um resumo muito bem sintetizado do fluxo de trabalho necessário para contribuir com um típico projeto hospedado no Github existe no blog do Dusty Phillips.

Meus jeitos

  • Desde que aprendi as principais ideias de como o bitcoin funciona, passei a usar o comentário “Genesis commit” no commit inicial de novos projetos, em referência ao bloco genesis.
  • git pull é aquele comando que fere o princípio KISS, por apenas uma tarefa a mais. O comando simplesmente adquire modificações que ocorreram no branch remoto e depois mescla ela com o branch atual. O problema é quando o branch remote diverge do seu branch local. Daí lembro de ter lido alguns comentários que git pull era ruim, mal projetado e você não devia usar. Advogavam que você devia usar git fetch, para depois decidir se iria querer fazer git merge ou git rebase.
  • E naqueles momentos em que você quer submeter as modificações feitas em cima do mesmo arquivo, porém usando commits diferentes, é que você vai apreciar o poder do comando git add -p. Eu descobri esse recurso através do post de humilde título “git add -p: The most powerful git feature you’re not using yet“.
  • Há também os momentos em que esqueceu de uma besteria no último commit e gostaria de modificar isso.
    git commit --amend

    é o caminho.

  • E caso você não saiba, git reconhece intervalos de commit, então caso você queira, por exemplo, verificar o log de todos os commits entre os commits 6d09f7ba08871d89ee5b43bf27d547a521155a2f e d2a3e06735ebf74bc88d355875e7d04ff8476eb6 (que no repositório do Tufão são os commits referenciados pelas tags 1.3.0 e 1.3.1, respectivamente), basta usar o comando:
    git log 6d09f7ba08871d89ee5b43bf27d547a521155a2f~...d2a3e06735ebf74bc88d355875e7d04ff8476eb6

    O tio (~) após a referência ao primeiro commit indica que eu quero referenciar o commit-pai daquele commit. Eu fiz isso, porque no git, o primeiro commit do intervalo é aberto (isto é, ele é excluído do intervalo), mas como eu também quero ver o log desse commit, especifico o commit-pai no intervalo.

  • E o item anterior me lembra que eu costumo assinar digitalmente minhas tags, usando o comando git tag -s, para me beneficiar das garantias de autenticidade (acho que é essa a garantia que eu consigo, qualquer coisa me corrijam aí).
  • Ah, e naqueles momentos em que você não trata seu repositório com carinho e acaba perdendo o poder de usar git merge entre dois branches que irão/iriam compartilhar vários commits, git cherry-pick é o comando que vai “salvar” seu(s) dia(s).
  • Não sabe como tratar seu repositório com carinho? Dê uma lida no post “A successful Git branching model” do Vincent Driessen.
  • Está trabalhando em um projeto que não suporta shadow build (aka out of source builds) e quer ter uma área de trabalho limpinha novamente? git clean -f -x -d resolve.
  • “Quais são as modificações que vão entrar quando eu fizer git commit?” é resolvido adicionando o argumento –staged ao comando git diff:
    git diff --staged
  • “véi, tem como você mesclar tal branch no teu repositório?” chega justamente quando você está trabalhando naquele commit por algumas horas já e não quer perder as modificações feitas na área de trabalho (staging area). git stash pode ajudar.
  • Ah, e que commit safado quebrou o sistema? Até nisso o git ajuda com o git bisect.
  • E essa linha suspeita nesse arquivo, quem colocou? git blame para revelar o sabotador.
  • Será que meu commit finalmente já foi lançado em uma versão estável?
    git tag --contains

    irá ajudar. E analogamente, também há o

    git branch --contains

    .

  • Também uso o git remote para administrar branches remotos e acho que não há muito mais que issogit remove -v para mostrar suas URLs sem a necessidade de qualquer atividade na rede.
  • E caso esteja procurando por alguma forma de trabalhar com git e bzr, talvez ache o projeto git-fc útil.
  • Agora, para aumentar minha produtividade, eu uso as fantásticas capacidades (como completadores automágicos e RPROMPT não-muito-intrusivo) do zsh, o shell mais fantástico de todos.
  • E caso queira compartilhar o link para alguma página do GitHub, lembre que essas páginas estão em constantes mudanças, então use o atalho de teclado “y” para pegar o link permanente.
  • E para quem estiver usando ArchLinux, tenho uma dica legal para trabalhar com PKGBUILDs.
  • E para procrastinar enquanto o projeto compila, também há a ferramenta gource para visualizar o histórico do projeto e o giggle para navegar na história sem a ajuda do GitHub.
  • E caso você ache que o git é muito patriarcal, você não está sozinho!

Segurança no Git

Antes das chaves assimétricas, não detínhamos o poder de verificar a identidade muito bem, mas ainda bem que elas foram criadas. E felizmente a Git permite anexar assinaturas a tags/commits. Logo, você pode colocar seu repositório git até através de uma rede anônima como a Tor e as pessoas ainda poderão confiar na autenticidade da origem dos commits, e sem comprometer o autor, que não mais precisa da palavra de alguém para atestar sua identidade.

Como o sistema distribuído que é, o git não usa números de revisões para identificar os commits. O valor de uma função hash é utilizado como identificador. A entrada de tal função hash é derivado do conteúdo do próprio commit, como as modificações que o commit introduz, e o identificador do commit pai. Isso significa, que, caso você modifique o conteúdo de um commit, o identificador dos commits filhos também são modificados. Essa característica é uma grande proteção contra tentativas de forjar o histórico de um projeto, pois, mesmo que um sistema na rede seja invadido, os nós que usem seu repositório irão detectar (e rejeitar) tentativas de forjar o histórico pelo atacante.

Há essas várias pequenas decisões de projeto que foram feitas e que, diferente do que alguns pensam, não foram feitas com objetivos patriarcais.

Fim

Posso finalizar afirmando que a git conquistou espaço no meu conjunto de ferramentas.

Estilos de multitasking cooperativo, loop de eventos e programação assíncrona

introUm dos assuntos que mais me interessa em programação é programação assíncrona. Esse é um tema ao qual eu fui introduzido desde que comecei a brincar com Qt (framework para criar interfaces gráficas) lá por volta de 2010, mas com o tempo fui tendo experiências em vários “paradigmas” de programação assíncrona, passando, por exemplo, por Node.js e Boost.Asio. O tema muito me interessa e foi, por exemplo, o motivador para eu estudar sistemas operacionais e arquitetura de computadores.

Esse é um trabalho que eu tive a ideia de fazer já faz muito tempo. Passou até ideia de, em vez de fazer um post, evoluir mais a ideia e transformar em contribuição e também a ideia de transformar em episódio de podcast. Acho que evolui muito durante esse tempo, para quem antes não entendia nem o porquê de uma troca de contexto ser lenta.

Motivação

Muitas vezes, nos deparamos com problemas que exigem o tratamento contínuo de várias tarefas, como tratar eventos de rede e manipular arquivos locais, por exemplo. Uma intuição ao encarar esse problema seria o uso de threads, já que há várias tarefas em “paralelo”. Entretanto, nem todos os trabalhos efetuados pelo computador são executados exclusivamente pela CPU.

Há outros componentes além da CPU, mas não programáveis e que não executam seu algoritmo, que costumam ser mais lentos e possuem outras tarefas, como transformar um sinal digital em analógico, por exemplo. Além disso, a comunicação com esses componentes ocorre de forma serial, através do ciclo buscar-decodificar-executar-checar-interrupção. Há uma simplificação nesse argumento, mas o fato é que você não lê dois arquivos em paralelo do mesmo HD. Em suma, usar threads não é uma abstração natural ao problema e só introduz gargalo desnecessário e complexo.

“Para quem só sabe usar martelo, tudo parece um prego.” — velho provérbio Klingon

Outro motivo para evitar o uso de threads como resposta ao problema é que logo você terá mais threads que CPUs/núcleos e irá se deparar com o que é conhecido como problema C10K. Mesmo que não fosse um gargalo absurdo, só o fato de você necessitar de mais threads que CPUs disponíves já torna o seu programa mais restrito, pois ele não mais poderá funcionar em um ambiente bare metal, sem a presença de um sistema operacional moderno ou um scheduler.

E o grande problema de desempenho que threads introduzem decorre do fato de elas exigirem uma troca de contexto com o kernel. É claro que esse não é o único problema, pois há também o custo de criação da thread. O programa estaria realizando a criação de threads, que podem acabar tendo um tempo de vida curto e, além disso, passarem a maior parte de seu tempo de vida dormindo. A própria criação da thread não é completamente escalável, pois requer a alocação de sua própria pilha, porém a memória é um recurso global e já aí temos um ponto de contenção.

O problema de desempenho de uma troca de contexto do sistema operacional lembra o problema de desempenho da convenção de chamada de funções usada por compiladores, mas pior.

Funções são unidades isoladas, encapsuladas, e de tal forma deveriam trabalhar. Ao realizar uma chamada de função, a função atual não tem conhecimento de quais registradores serão usados pela nova função. A função atual não detém a informação de quais registradores terão seus valores sobrescritos pela nova função. Assim, as convenções de chamadas de função exigem dois pontos de processamento extra, um para salvar os valores dos registradores na pilha, e outro para restaurar os valores. É por isso que alguns programadores são tão fissurados em fazer function inlining.

O problema de desempenho da troca de contexto é maior, porque ele deve salvar o valor de todos os registradores e ainda sofre com o gargalo do scheduler e da própria troca de contexto. E se processos estiverem envolvidos, então, ainda tem o gargalo de reconfigurar a unidade de gerenciamento de memória. Esse estilo de processamento multitarefa recebe o nome de multitarefa preemptivo. Você pode aprender mais sobre esses custos estudando arquitetura de computadores e sistemas operacionais.

É possível obter concorrência, que é a propriedade de executar várias tarefas no mesmo período de tempo, sem recorrer a paralelismo real. Para essas tarefas que estão mais atreladas a I/O, é interessante abandonarmos o paralelismo durante o tratamento de I/O para alcançarmos mais escalabilidade, evitando o problema do C10K. E já que um novo design se faz necessário, é bom levar em conta multitarefa cooperativo e obter um resultado até melhor do que o inicialmente planejado.

O loop de eventos

E uma abordagem para o problema de programação, que eu vejo sendo usada mais em jogos do que em qualquer outro lugar, é a abordagem de loop de eventos. Há essas peculiaridades de que em jogos você não manipula arquivos como se fosse um banco de dados, com ambiciosos requisitos de desempenho, e também de desenvolver o jogo para quando completar, não mudar o código nunca mais, sem qualquer compromisso com flexibilidade e manutenção. E é com essa abordagem, a abordagem do loop de eventos, que começamos.

Há uma biblioteca, a biblioteca de baixo nível SDL, que tem como objetivo ser apenas uma camada de abstração multimídia, para suprir o que já não é suprido pela própria especificação da linguagem C, focando no desenvolvedor de jogos. A SDL faz uso de um sistema de eventos para tratar da comunicação entre o processo e o mundo externo (teclado, mouse, janelas, …), que é comumente usada em algum loop que o programador prepara. Essa mesma estrutura é utilizada em vários outros locais, incluindo na Allegro, que foi o maior competidor da SDL, no passado.

A ideia é ter um conjunto de funções que faz a ponte da comunicação entre o processo e o mundo externo. No mundo SDL, eventos são descritos através do tipo não-extensível SDL_Event. Então você usa funções como a SDL_PollEvent para receber os eventos e funções dedicadas para iniciar operações que atuem no mundo externo. O suporte a programação assíncrona na biblioteca SDL é fraco, mas esse mesmo princípio de loop de eventos poderia ser usado em uma biblioteca que fornecesse um suporte mais forte a programação assíncrona. Abaixo você pode ver o exemplo de um código que faz uso de eventos da SDL:

Há as bibliotecas de interface gráfica e widgets, como a GTK+, a EFL e a Qt, que levam essa ideia um passo adiante, abstraindo o loop de eventos, antes escrito e reescrito por você, em um objeto. A Boost.Asio, que não é focada em interface gráficas, mas foca exclusivamente em operações assíncronas, possui uma classe de propósito similar, a classe io_service.

Para que você deixe de fazer o código boilerplate de rotear eventos a ações específicas, essas classes tratam essa parte para você, possivelmente através de callbacks, que é uma abstração antiga. A ideia é que você associe eventos a funções que podem estar interessadas em tratar tais eventos. Toda a lógica de rotear os eventos, agora, passa a fazer parte do objeto “loop de eventos”, em vez de um loop de eventos bruto. Esse estilo de programação assíncrona é um estilo do modelo passivo, pois você só registra os callbacks e cede o controle para a framework.

Agora que estamos em um nível de abstração superior ao do loop de eventos, vamos parar a discussão sobre loop de eventos. E esses objetos que estamos mencionando como objetos “loop de eventos”, serão mencionados, a partir de agora, como executors.

O executor

O executor é um objeto que pode executar unidades de trabalho encapsuladas. Utilizando somente C++11, podemos implementar um executor que gerencie o agendamento de operações relacionadas a espera de alguma duração de tempo. Há o recurso global RTC e, em vez de criar várias e várias threads para a execução de operações bloqueantes como a operação sleep_for, vamos usar um executor, que irá agendar e tratar todos os eventos de tempo que se façam necessário. Abaixo está o código para uma possível implementação de tal executor:

Esse código de exemplo me faz lembrar do sleepsort.

No exemplo, sem o uso de threads, foi possível realizar as várias tarefas concorrentes de espera de tempo. Para tal, ao objeto executor, foi dada a responsabilidade de compartilhar o recurso global RTC. Como a CPU é mais rápida que as tarefas requisitadas, uma única thread foi suficiente e, além disso, mesmo assim houve um período de tempo no qual o programa ficou ocioso.

Há alguns conceitos que já podem ser extraídos desse exemplo. Primeiro, vamos considerar que o executor seja uma abstração padrão, já fornecida de alguma forma e interoperável entre todos os códigos que façam uso de operações assíncronas. Quando o programa deseja fazer a operação assíncrona “esperar”, ele requisita o início da operação a alguma abstração (nesse caso é o próprio executor, mas é mais comum encontrar tais interações através de “objetos I/O”) através da função que inicia a operação. O controle é passado para o executor (através do método run), que continuamente verifica notificações de tarefas concluídas. Quando uma tarefa é concluída, o executor empresta o controle da thread para o código do usuário, executando a função que havia sido registrada para tratar a notificação de finalização da tarefa. Quando não há mais nenhuma tarefa na fila, o executor cede o controle da thread, por completo, para o código do usuário.

Como só existe uma thread de execução, mas várias tarefas a executar, nós temos o problema de compartilhamento de recursos, que nesse caso é a própria CPU/thread. O controle deveria ir e voltar entre alguma abstração e o código do usuário. E aí está o princípio do estilo multitarefa cooperativo. Há pontos de customização de comportamento entre os algoritmos responsáveis por executar as tarefas, fazendo com que eles cedam e emprestem o tempo da CPU para realização de outras tarefas.

O estilo de multitarefa cooperativo tem a vantagem de que os pontos de paradas são bem definidos, pois você sabe exatamente quando o controle passa de uma tarefa a outra. Então não há aquele grande gargalo que vimos com trocas de contextos, onde todos os registradores precisam ser salvos, entre outros. Uma solução mais elegante, eficiente e verde.

O objeto que passamos como último argumento da função add_sleep_for_callback é um callback, também conhecido como completion handler. Reflita sobre o que aconteceria se uma nova operação de espera fosse requisitada dentro de um dos callbacks que registramos. Abaixo há uma versão evoluída do executor que implementamos.

Esse detalhe de implementação me lembra a variável de SHELL SHLVL.

Um caso interessante é o da linguagem JavaScript, que possui um tipo de “executor implícito”, que passa a funcionar quando chega ao fim do seu código. Nesse caso, você não precisa escrever códigos como “while (true) executor.run_one()” ou “executor.run()“, mas apenas registrar os callbacks e se assegurar que não há nenhum loop infinito que impeça que o controle passe ao executor.

Introduzida a motivação, o texto passou a prosseguir reduzindo menções a I/O, por motivos de simplicidade e foco, mas tenha em mente que usamos operações assíncronas, majoritariamente, para realizar interações com o mundo externo. Então muitas das operações são agendadas condicionalmente em resposta a notificação de término de uma tarefa anterior (ex.: se protocolo inválido, feche o socket, do contrário, agende mais uma operação de leitura). Propostas como a N3785 e a N4046 definem executors também para gerenciar thread pools, não somente timeouts dentro de uma única thread. E por último, também é possível implementar executors que agendem a execução de operações de I/O dentro de uma mesma thread.

Algoritmos assíncronos descritos de forma síncrona

O problema com essa abordagem de callbacks é que antes, possuíamos código limpo e legível. O código podia ser lido sequencialmente, pois é isso que o código era, uma sequência de instruções. Entretanto, agora precisamos espalhar a lógica entre múltiplos e múltiplos callbacks. Agora você passa a ter blocos de código relacionados longe uns dos outros. Lambdas ajudam um pouco com o problema, mas não o suficiente. O problema é conhecido como callback/nesting hell e é similar ao problema do espaguete. Não sendo o bastante, o fluxo de execução do código se tornou controvertido pela própria natureza assíncrona das operações e construções como estruturas de condição e repetição e até mesmo código de tratamento de erro ficam representáveis de formas longe do ideal, obscuras e difíceis de ler.

Uma abstração de procedimentos muito importante para programação assíncrona é a corotina. Existe a abstração de procedimentos a qual nos referimos por função, que é uma implementação do conceito de subrotina. E temos a corotina, que é uma generalização do conceito de subrotina. A corotina possui duas operações a mais que a subrotina, sendo elas suspender e resumir.

Quando sua função é uma corotina (possível quando a linguagem dá suporte a corotinas), ela pode suspender antes de chegar ao final de sua execução, possivelmente devolvendo um valor durante essa ação de suspender. Um exemplo de onde corotinas são úteis é na implementação de uma suposta função geradora fibonacci e um programa que use essa função para imprimir os dez primeiros números dessa sequência infinita. O código Python abaixo demonstra uma implementação de tal exemplo, onde se percebe a elegância, legibilidade e reaproveitamento que o conceito de corotina permite quando temos o problema de multitarefa cooperativa:

Esse código aí me lembra das funções setjmp/longjmp.

Uma característica a qual se deve dar atenção caso você não esteja familiarizado com o conceito é que o valor das variáveis locais a função fibonacci foi preservado entre as várias “chamadas”. Mais precisamente, quando a função era resumida, ela possuía a mesma pilha de execução de quando foi suspensa. Essa é a uma implementação “completa” do conceito de corotina, uma corotina com pilha. Há também implementações de corotinas sem pilha de execução, onde somente o “número da linha de código” que estava executando é restaurado.

A proposta N4286 introduz uma nova palavra-chave, await, para identificar um ponto para suspender uma corotina. Fazendo uso de tal funcionalidade, é apresentado o seguinte exemplo, que elegantemente define um algoritmo assíncrono, descrito, na minha humilde opinião, de forma bastante síncrona, e fazendo uso das várias construções da linguagem para controle de fluxo de execução (estrutura de seleção, repetição…).

Corotinas resolvem o problema de complexidade que algoritmos assíncronos demandam. Entretanto, há várias propostas para implementação de corotinas e nenhuma delas foi padronizada ainda. Um caso interessante é o da biblioteca Asio, que, usando macros e um mecanismo similar ao Duff’s device, dá suporte a corotinas que não guardem uma pilha de execução. Dentre todo o trabalho que está sendo investido, o que eu espero que aconteça na comunidade de C++ é que a padronização siga o princípio de “você só paga pelo que usa” e que a especificação permita implementações bem performáticas.

Enquanto a padronização de corotinas não acontece e ficamos sem a solução a nível de linguagem, podemos optar por soluções a nível de biblioteca. Como C++ é uma linguagem que suporta abstrações de baixo nível, você consegue acesso a várias facilidades que podem ser usadas para implementar suporte a corotinas, tipo setjmp e longjmp e até mais coisas indo para o mundo não-portável fora da especificação. Uma biblioteca que parece bem promissora e que espero ver sendo incluída na Boost esse ano é a biblioteca Fiber, que imita a API de threads a qual estamos acostumados para fornecer “threads” agendadas cooperativamente, em espaço de usuário. A biblioteca usa o conceito de fibra, análogo ao conceito de thread, e em uma thread, você pode executar várias fibras. Tal biblioteca fornece a expressividade que precisamos para escrever algoritmos assíncronos de forma síncrona.

Outra solução enquanto você não pode esperar, é não usar corotinas, pois ainda será possível conseguir performance excelente através das técnicas comentadas ao longo do texto. O grande porém vai ser o fluxo embaralhado do código-fonte (ofuscação de código, para quê te quero?).

Completion tokens

E nesse período de tempo incerto quanto a que solução tornará-se o padrão para programação assíncrona na linguagem C++, a Boost.Asio, desde a versão 1.54, adotou um princípio interessante, de implementar uma solução extensível. Tal modelo extensível é muito bem documentado na proposta N4045 e aqui nessa seção há somente um resumo do que está contido em tal proposta.

A proposta é que, dado que o modelo de callbacks é confuso, ele seja evoluído para suportar outros modelos. Então, em vez da função receber como argumento um completion handler (o callback), ela deve receber um completion token, que é a solução de customização/extensibilidade proposta.

A proposta N4045 usa uma abordagem top-down, de primeiro mostrar como a proposta é usada, para depois mostrar como ela é implementada. Abaixo há um código de exemplo retirado da proposta:

No código de exemplo anterior, cada vez que a variável yield é passada a alguma operação assíncrona (ex.: open e read), a função é suspensa até que a operação seja concluída. Quando a operação é concluída, a função é resumida no ponto em que foi suspensa e a função que iniciou a operação assíncrona retorna o resultado da operação. A biblioteca Fiber, mencionada anteriormente, fornece um yield_context para o modelo extensível da Asio, o boost::fibers::asio::yield. Código assíncrono escrito de uma forma síncrona. Entretanto, um modelo extensível é adotado, pois não sabemos qual será o padrão para operações assíncronas, então não podemos forçar o uso da biblioteca Fiber goela abaixo.

Para tornar o modelo extensível, o tipo do retorno da função precisa ser deduzido (a partir do token) e o valor do retorno da função também precisa ser deduzido (também a partir do token). O tipo do retorno da função é deduzido a partir do tipo do token e o valor retornado é criado a partir do objeto token passado como argumento. E você ainda tem o handler, que deve ser chamado quando a operação for concluída. O handler também é extraído a partir do token. O modelo de completion tokens faz uso de type traits para extrair todas essas informações e, caso tais traits não sejam especializados, o comportamento padrão é tratar o token como um handler, tornando a abordagem retrocompatível com o modelo de callbacks.

Vários exemplos de token são dados no documento N4045:

  • use_future
  • boost::fibers::asio::use_future
  • boost::fibers::asio::yield
  • block

A abordagem de usar std::future tem impactos significativos na performance e não é uma abstração legal, como o próprio N4045 explica em suas seções iniciais, então vamos evitá-la. É até por isso que eu nem comentei sobre ela até então nesse texto.

Sinais e slots

Uma alternativa que foi proposta ao modelo de callbacks é a abordagem de “signals & slots”. Essa abordagem é implementada na libsigc++, na Boost, na Qt e em várias outras bibliotecas.

A proposta de usar sinais introduz esse conceito, de sinal, que é usado para notificar algum evento, mas abstrai o processo de entregar as notificações e registrar as funções que tratem o evento. O código que notifica os eventos só precisa se preocupar em emitir o sinal toda vez que o evento acontecer, pois o próprio sinal vai cuidar do conjunto de slots, que são as porções de código a serem executadas para tratar os eventos.

Essa abordagem costuma permitir um grande desacoplamento, em oposição a abordagem verbosa muito usada em Java. Um efeito interessante, também, a depender da implementação, é que você pode conectar um sinal a outro sinal, para evitar o trabalho de escrever você próprio o código que sincronize a emissão de um sinal a emissão de outro sinal. É possível também ter muitos sinais conectados a um único slot, assim como um único sinal conectado a múltiplos slots.

O sinal costuma ser associado a um objeto, e quando tal objeto é destruído, as conexões que haviam sido feitas também o são. Assim como também é possível ter slots como métodos de um objeto, que são desconectados de todos os sinais tão logo o objeto é destruído.

Como os sinais são abstrações independentes e operantes assim que se tornam expostos, é natural ser incentivado a remover o argumento de callback das funções que iniciam operações assíncronas, pois haveria duplicação de esforços. Se você for até mais longe e remover também a própria função que inicie a operação assíncrona, expondo ao usuário apenas os sinais para receber as notificações, sua framework deixará de seguir o modelo ativo para seguir o modelo passivo. Exemplos de tais modelos passivos é o socket da biblioteca Qt, que não possui uma função explícita para iniciar a operação de leitura, e a biblioteca POCO, que não possui uma função explícita para iniciar o recebimento de uma requisição HTTP.

Outro detalhe que temos no caso da ideia de sinais, é o controle de acesso. No caso da biblioteca Qt, sinais são implementados de uma forma que exige a cooperação de um pré-processador, o executor da própria Qt e a classe QObject. No caso da biblioteca Qt, a emissão de sinais segue as regras de controle de acesso de métodos protegidos em C++, onde todas as classes-filha podem realizar a emissão de sinais declaradas nas classes-pai. Enquanto a operação de conectar um sinal a outro sinal ou a um slot segue as mesmas regras de membros públicos, onde todo mundo pode realizar.

No caso das bibliotecas que implementam o conceito de sinais como um tipo, é comum ver um tipo sinal que englobe tanto a operação de emitir o sinal, quanto a operação de conectar o sinal a algum slot (diferente do que vemos na proposta de futures e promises, onde é possível ter um controle de acesso separado para as diferentes responsabilidades/operações/entidades).

A abordagem de sinais é legal, mas ela não resolve o problema de complexidade que é resolvido por corotinas. Eu descrevi essa abordagem com o intuito de facilitar a explicação sobre a diferença entre os modelos ativo e passivo.

Modelo ativo versus passivo

No modelo passivo, você não agenda o início das operações e, apesar de ser mais comum em frameworks de produtividade, há muitas perguntas que esse modelo não responde bem (isso é só uma opinião e não um fato), que acabam exigindo o projeto de bem mais abstrações para contornar o problema.

Fazendo um contraste rápido entre a biblioteca Qt e a Boost.Asio. Em ambas as bibliotecas, você possui classes para abstraírem o conceito de socket, mas, enquanto na Qt você trata o evento readyRead e usa o método readAll para receber o buffer com o conteúdo, na Boost.Asio você inicia a operação async_read_some e passa o buffer a ser utilizado como argumento. A Qt usa o modelo passivo e a Boost.Asio usa o modelo ativo.

O evento readyRead, da Qt, age independente do usuário e requer a alocação do buffer toda vez que ocorre. Como, então, você responde a perguntas como “como eu posso customizar o algoritmo de alocação do buffer?”, “como eu posso customizar a aplicação para fazer reaproveitamento de buffers?”, “como eu faço para usar um buffer pré-alocado na stack?” e outras. Como o modelo passivo não responde a perguntas como essa, você precisa inflar a abstração de socket com mais pontos de customização para que comportamentos como esses sejam possíveis. É uma explosão combinatória que acontece para cada abstração que lide com operações assíncronas. No modelo ativo, fica bem natural. Se há algum recurso que a operação que o programador está prestes a iniciar necessita, só precisa passar o recurso como argumento, pois no modelo ativo, o programador explicitamente inicia as operações. E não é só sobre customizar obtenção de recursos. Mais um exemplo de pergunta que o modelo passivo não responde bem é “como eu decido se vou ou não aceitar uma conexão (ou adiar para depois, durante cenários de sobrecarga do servidor)?”. É um grande poder para aplicações sérias que estão seriamente preocupadas com performance e necessitam de ajustes meticulosos.

Além da questão de performance e ajuste fino, o modelo passivo também é problemático para realizar depuração e testes, pois ele faz uma inversão de controle. Esse é um dos motivos pelos quais, até hoje, eu vejo frameworks e frameworks introduzindo race condition nos testes ao depender de timeout para garantir a “robustez” de suas implementações. Essa solução de timeout não resolve um problema que ocorre somente no modelo passivo, mas no modelo passivo, ele ocorre com muito mais frequência. No modelo ativo, seu callback pode ser chamado mesmo quando o erro acontece. No modelo passivo é tão problemático, que já vi eventos de erro serem declarados como notificações separadas, que podem ser facilmente ignoradas, característica indesejável quando você quer diminuir e evitar os bugs no seu código.

Graças ao modelo ativo, muito da biblioteca que estou desenvolvendo com o intuito de submeter a Boost foi simplificado.

O modelo passivo, no entanto, é ótimo para prototipações rápidas. Felizmente podemos implementar abstrações que introduzam o modelo passivo em termos do modelo ativo e ter o melhor (ou o pior, se você for um projetista de C#) dos dois mundos.

Referências bônus

Uns links aleatórios que também me ajudaram como referência ou podem servir de leitura aprofundada para quem quer mais:

Comentários bônus

Espero ter introduzido você, estimado leitor que não comenta, as principais práticas utilizadas no universo da programação assíncrona. E agora que você já está armado com uma visão geral da “área”, se jogar nesse mar da internet para buscar mais referências e cavar mais fundo é só o segundo passo.

Esses dois meses que não postei nada e fui perdendo acessos/visitantes… valeram a pena, pois estou bastante satisfeito com esse trabalho e esse blog é sobre minha satisfação pessoal, por isso que eu nem coloco propaganda.

Há alguns outros textos separados em minha pasta de rascunho com temas variando desde ideias originais até assuntos “manjados” que eu pretendo usar quando bater aquela preguiça de investir muito tempo para um post só.

Agora que eu finalmente discuti concorrência sem paralelismo nesse blog, eu devia atualizar meu notebook (ou corrigir meu desktop) para ter um hardware suficientemente bom e começar a estudar OpenCL para fazer uns posts sobre paralelismo massivo.

EDIT (2015/3/11):

  • Ressaltar sugestão de estudar arquitetura de computadores e sistemas operacionais na seção de motivação.
  • Menção sobre “executor implícito” que ocorre em JavaScript.
  • Adicionado exemplo de motivação não relacionado a buffers na discussão de modelo ativo versus modelo passivo.

Feijão por cima do arroz ou o inverso?

O arroz sempre é seco, enquanto o feijão costuma vir acompanhado de seu próprio molho (que inclusive é a razão de termos a receita do caldo de feijão). No prato, nunca colocamos o molho por baixo, por que deveria ser diferente quando misturamos feijão com arroz?

OFF-TOPIC

Nada nesse blog foi postado desde o início do ano, pois, mais uma vez, estou preparando posts longos, explicativos e que exigem um pouco mais de pesquisa que o normal.

%d blogueiros gostam disto: