Python 3.5 Implementando a simultaneidade usando asyncio

Tradução do Capítulo 13 Concorrência
do livro 'Expert Python Programming',
Segunda edição
Michał Jaworski e Tarek Ziadé, 2016

Programação assíncrona


Nos últimos anos, a programação assíncrona ganhou grande popularidade. O Python 3.5 finalmente conseguiu algumas funções de sintaxe que reforçam os conceitos de soluções assíncronas. Mas isso não significa que a programação assíncrona se tornou possível apenas desde o Python 3.5. Muitas bibliotecas e estruturas foram fornecidas muito antes, e a maioria delas se originou em versões mais antigas do Python 2. Existe até uma implementação alternativa completa do Python chamada Stackless (consulte o Capítulo 1, “O status atual do Python”), que se concentra nessa única abordagem de programação. Para algumas soluções, como Twisted, Tornado ou Eventlet , as comunidades ativas ainda existem e valem a pena conhecer. De qualquer forma, a partir do Python 3.5, a programação assíncrona ficou mais fácil do que nunca. Portanto, espera-se que suas funções assíncronas incorporadas substituam a maioria das ferramentas antigas, ou projetos externos gradualmente se transformem em um tipo de estruturas de alto nível baseadas no Python interno.

Ao tentar explicar o que é programação assíncrona, é mais fácil pensar nessa abordagem como algo semelhante aos threads, mas sem um planejador de sistema. Isso significa que um programa assíncrono pode processar tarefas ao mesmo tempo, mas seu contexto é alternado internamente e não pelo planejador do sistema.

Mas, é claro, não usamos threads para processamento paralelo de tarefas em um programa assíncrono. A maioria das soluções usa conceitos diferentes e, dependendo da implementação, são chamados de maneira diferente. Alguns exemplos de nomes usados ​​para descrever esses objetos de programa paralelo são:

  • Linhas verdes - linhas verdes (projetos de greenlet, gevent ou eventlet)
  • Coroutines - coroutines (programação assíncrona pura em Python 3.5)
  • Tasklets (Python sem pilha) Estes são basicamente os mesmos conceitos, mas geralmente são implementados de maneiras ligeiramente diferentes.

Por razões óbvias, nesta seção, focaremos apenas as corotinas que são inicialmente suportadas pelo Python, começando na versão 3.5.

Multitarefa colaborativa e E / S assíncrona


A multitarefa colaborativa é o núcleo da programação assíncrona. Nesse sentido, a multitarefa no sistema operacional não é necessária para iniciar uma troca de contexto (para outro processo ou encadeamento), mas cada processo libera voluntariamente o controle quando está no modo de espera para garantir a execução simultânea de vários programas. É por isso que é chamado de colaborativo. Todos os processos devem trabalhar juntos para garantir que a multitarefa seja bem-sucedida.

O modelo multitarefa às vezes era usado em sistemas operacionais, mas agora dificilmente pode ser encontrado como uma solução no nível do sistema. Isso ocorre porque existe o risco de que um serviço mal projetado possa prejudicar facilmente a estabilidade de todo o sistema. Planejar encadeamentos e processos usando comutadores de contexto controlados diretamente pelo sistema operacional é atualmente a abordagem dominante para simultaneidade no nível do sistema. Mas a multitarefa colaborativa ainda é uma excelente ferramenta de simultaneidade no nível do aplicativo.

Falando em multitarefa conjunta no nível do aplicativo, não estamos lidando com threads ou processos que precisam liberar o controle, pois toda a execução está contida em um processo e thread. Em vez disso, temos várias tarefas (corotinas, tasklets e threads verdes) que transferem o controle para uma única função que controla a coordenação das tarefas. Essa função geralmente é um tipo de loop de eventos.

Para evitar confusão (devido à terminologia do Python), agora chamaremos essas tarefas paralelas de rotinas. A questão mais importante na multitarefa colaborativa é quando transferir o controle. Na maioria dos aplicativos assíncronos, o controle é passado para o planejador ou loop de eventos durante operações de E / S. Independentemente de o programa ler dados do sistema de arquivos ou se comunicar por meio de um soquete, essa operação de E / S sempre está associada a algum tempo de espera quando o processo se torna inativo. A latência depende de um recurso externo; portanto, é uma boa oportunidade de liberar o controle para que outras corotinas possam fazer seu trabalho, até que também precisem esperar que essa abordagem seja um pouco semelhante no comportamento de como o multithreading é implementado no Python. Sabemos que o GIL serializa threads Python, mas também é liberado a cada operação de E / S. A principal diferença é que os threads no Python são implementados como threads no nível do sistema, para que o sistema operacional possa descarregar o thread em execução no momento e transferir o controle para outro.

Na programação assíncrona, as tarefas nunca são interrompidas pelo loop do evento principal. É por isso que esse estilo de multitarefa também é chamado de multitarefa sem prioridade.

Obviamente, todo aplicativo Python é executado em um sistema operacional em que existem outros processos competindo por recursos. Isso significa que o sistema operacional sempre tem o direito de descarregar todo o processo e transferir o controle para outro. Mas quando nosso aplicativo assíncrono é iniciado, ele continua de onde foi pausado quando o agendador do sistema interveio. É por isso que as corotinas nesse contexto são consideradas sem aglomeração.

Python assíncrono e aguardar palavras-chave


As palavras - chave async e wait são os principais componentes da programação assíncrona do Python.

A palavra-chave assíncrona usada antes da declaração def define uma nova corotina. Uma função de rotina pode ser suspensa e retomada em circunstâncias estritamente definidas. Sua sintaxe e comportamento são muito semelhantes aos geradores (consulte o Capítulo 2, “Recomendações de sintaxe”, abaixo do nível da classe). De fato, os geradores devem ser usados ​​em versões mais antigas do Python para implementar corotinas. Aqui está um exemplo de declaração de uma função que usa a palavra-chave async :

async def async_hello(): print("hello, world!") 

As funções definidas usando a palavra-chave assíncrona são especiais. Quando chamados, eles não executam o código dentro, mas retornam um objeto de rotina:

 >>>> async def async_hello(): ... print("hello, world!") ... >>> async_hello() <coroutine object async_hello at 0x1014129e8> 

O objeto coroutine não faz nada até sua execução ser agendada no loop de eventos. O módulo asyncio está disponível para fornecer uma implementação básica do loop de eventos, bem como muitos outros utilitários assíncronos:

 >>> import asyncio >>> async def async_hello(): ... print("hello, world!") ... >>> loop = asyncio.get_event_loop() >>> loop.run_until_complete(async_hello()) hello, world! >>> loop.close() 

Naturalmente, criando apenas uma simples rotina, em nosso programa não implementamos paralelismo. Para ver algo verdadeiramente paralelo, precisamos criar mais tarefas que serão executadas por um loop de eventos.

Novas tarefas podem ser adicionadas ao loop chamando o método loop.create_task () ou fornecendo outro objeto para aguardar a função asyncio.wait () ser usada. Usaremos a última abordagem e tentaremos imprimir de forma assíncrona uma sequência de números gerados usando a função range () :

 import asyncio async def print_number(number): print(number) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete( asyncio.wait([ print_number(number) for number in range(10) ]) ) loop.close() 

A função asyncio.wait () aceita uma lista de objetos da corotina e retorna imediatamente. O resultado é um gerador que produz objetos que representam resultados futuros (futuros). Como o nome sugere, é usado para aguardar a conclusão de todas as corotinas fornecidas. O motivo pelo qual ele retorna um gerador em vez de um objeto de rotina é porque é compatível com versões anteriores do Python, que serão explicadas mais adiante. O resultado da execução desse script pode ser o seguinte:

 $ python asyncprint.py 0 7 8 3 9 4 1 5 2 6 

Como podemos ver, os números não são impressos na ordem em que criamos nossas corotinas. Mas é exatamente isso que queríamos alcançar.

A segunda palavra-chave importante adicionada no Python 3.5 está aguardando . É usado para aguardar os resultados de uma corrotina ou evento futuro (explicado posteriormente) e liberar o controle sobre a execução no loop de eventos. Para entender melhor como isso funciona, precisamos considerar um exemplo de código mais complexo.

Suponha que desejemos criar duas corotinas que executam algumas tarefas simples em um loop:

  • Aguarde um número aleatório de segundos
  • Imprima algum texto fornecido como argumento e a quantidade de tempo gasto em espera. Vamos começar com uma implementação simples que possui alguns problemas de simultaneidade que tentaremos melhorar posteriormente com o uso adicional de wait:

     import time import random import asyncio async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 time.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) ) async def main(): await asyncio.wait([waiter("foo"), waiter("bar")]) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) loop.close() 

Quando executado no terminal (usando o comando time para medir o tempo), você pode ver:

 $ time python corowait.py bar waited 0.25 seconds bar waited 0.25 seconds bar waited 0.5 seconds bar waited 0.5 seconds foo waited 0.75 seconds foo waited 0.75 seconds foo waited 0.25 seconds foo waited 0.25 seconds real 0m3.734s user 0m0.153s sys 0m0.028s 


Como podemos ver, ambas as corotinas concluíram sua execução, mas não de forma assíncrona. O motivo é que ambos usam a função time.sleep () , que bloqueia, mas não libera o controle no loop de eventos. Isso funcionará melhor em uma instalação multithread, mas não queremos usar fluxos no momento. Então, como podemos corrigir isso?

A resposta é usar asyncio.sleep () , que é uma versão assíncrona de time.sleep (), e esperar o resultado usando a palavra-chave wait. Já usamos essa declaração na primeira versão do main () , mas isso foi apenas para melhorar a clareza do código. Isso claramente não tornou nossa implementação mais paralela. Vejamos uma versão aprimorada da rotina waiter () que usa waitit asyncio.sleep ():

 async def waiter(name): for _ in range(4): time_to_sleep = random.randint(1, 3) / 4 await asyncio.sleep(time_to_sleep) print( "{} waited {} seconds" "".format(name, time_to_sleep) ) 


Executando o script atualizado, veremos como a saída de duas funções se alterna entre si:

 $ time python corowait_improved.py bar waited 0.25 seconds foo waited 0.25 seconds bar waited 0.25 seconds foo waited 0.5 seconds foo waited 0.25 seconds bar waited 0.75 seconds foo waited 0.25 seconds bar waited 0.5 seconds real 0m1.953s user 0m0.149s sys 0m0.026s 


Um benefício adicional desse aprimoramento simples é que o código é executado mais rapidamente. O tempo total de execução foi menor que a soma de todos os tempos de suspensão, porque as corotinas assumiram o controle uma a uma.

Assíncio em versões anteriores do Python


O módulo asyncio apareceu no Python 3.4. Portanto, esta é a única versão do Python que oferece suporte sério à programação assíncrona antes do Python 3.5. Infelizmente, parece que essas duas versões subseqüentes são suficientes para apresentar problemas de compatibilidade.

De qualquer forma, o núcleo de programação assíncrona no Python foi introduzido anteriormente aos elementos de sintaxe que suportam esse modelo. Antes tarde do que nunca, isso criou uma situação em que existem duas sintaxes para trabalhar com corotinas.

A partir do Python 3.5, você pode usar o assíncrono e aguardar :

 async def main (): await asyncio.sleep(0) 


No entanto, no Python 3.4, você precisará aplicar adicionalmente o decorador asyncio.coroutine e produzir no texto da corotina:

 @asyncio.couroutine def main(): yield from asyncio.sleep(0) 


Outro fato útil é que o rendimento da instrução foi introduzido no Python 3.3 e o PyPI possui um backport assíncrono. Isso significa que você também pode usar esta implementação de multitarefa colaborativa com o Python 3.3.

Um exemplo prático de programação assíncrona


Como mencionado muitas vezes neste capítulo, a programação assíncrona é uma ótima ferramenta para lidar com E / S. É hora de criar algo mais prático do que apenas imprimir sequências ou espera assíncrona.

Para garantir consistência, tentaremos resolver o mesmo problema que resolvemos com a ajuda de multithreading e multiprocessing. Portanto, tentaremos extrair assincronamente alguns dados de recursos externos por meio de uma conexão de rede. Seria ótimo se pudéssemos usar o mesmo pacote python-gmaps que nas seções anteriores. Infelizmente, não podemos.

O criador do python-gmaps era um pouco preguiçoso e levou apenas o nome. Para simplificar o desenvolvimento, ele escolheu o pacote de solicitação como sua biblioteca de clientes HTTP. Infelizmente, as solicitações não suportam E / S assíncrona com assíncrona e aguardam . Existem outros projetos que visam fornecer algum paralelismo para o projeto de consulta, mas eles dependem do Gevent ( grequests , consulte https://github.com/ kennethreitz / grequests ) ou executam um pool de processos / processos (query-futuros consulte github.com/ross/requests-futures ). Nenhum deles resolve o nosso problema.

Antes de me censurar por censurar um desenvolvedor de código aberto inocente, acalme-se. A pessoa por trás do pacote python-gmaps sou eu. Uma má escolha de dependências é um dos problemas deste projeto. Eu só gosto de me criticar publicamente de tempos em tempos. Esta será uma lição amarga para mim, já que o python-gmaps em sua versão mais recente (0.3.1 no momento da redação deste livro) não pode ser facilmente integrado à E / S assíncrona do Python. De qualquer forma, isso pode mudar no futuro, para que nada se perca.
Conhecendo as limitações da biblioteca, que eram tão fáceis de usar nos exemplos anteriores, precisamos criar algo que preencha essa lacuna. A API do Google Maps é realmente fácil de usar; portanto, criaremos um utilitário assíncrono apenas para ilustração. A biblioteca padrão do Python 3.5 ainda não possui uma biblioteca que possa executar solicitações HTTP assíncronas tão facilmente quanto chamar urllib.urlopen () . Definitivamente, não queremos criar suporte completo a protocolos do zero, portanto, usaremos uma pequena ajuda do pacote aiohttp disponível no PyPI. Esta é uma biblioteca realmente promissora que adiciona implementações de cliente e servidor para HTTP assíncrono. Aqui está um pequeno módulo construído sobre o aiohttp que cria uma função auxiliar geocode () que executa solicitações de geocodificação para o serviço da API do Google Maps:

 import aiohttp session = aiohttp.ClientSession() async def geocode(place): params = { 'sensor': 'false', 'address': place } async with session.get( 'https://maps.googleapis.com/maps/api/geocode/json', params=params ) as response: result = await response.json() return result['results'] 


Vamos supor que esse código seja armazenado em um módulo chamado asyncgmaps , que usaremos mais tarde. Agora estamos prontos para reescrever o exemplo usado na discussão sobre multithreading e multiprocessing. Anteriormente, costumávamos separar toda a operação em dois estágios separados:

  1. Atenda a todas as solicitações ao serviço externo em paralelo usando a função fetch_place () .
  2. Exiba todos os resultados em um loop usando a função present_result () .

Mas como a multitarefa colaborativa é completamente diferente do que o uso de vários processos ou threads, podemos mudar um pouco nossa abordagem. A maioria dos problemas levantados em Usando um único encadeamento por item não é mais nossa preocupação.
As corotinas não são preventivas, portanto, podemos exibir facilmente os resultados imediatamente após o recebimento das respostas HTTP. Isso simplificará nosso código e o tornará mais compreensível:

 import asyncio # note: local module introduced earlier from asyncgmaps import geocode, session PLACES = ( 'Reykjavik', 'Vien', 'Zadar', 'Venice', 'Wrocław', 'Bolognia', 'Berlin', 'Słubice', 'New York', 'Dehli', ) async def fetch_place(place): return (await geocode(place))[0] async def present_result(result): geocoded = await result print("{:>25s}, {:6.2f}, {:6.2f}".format( geocoded['formatted_address'], geocoded['geometry']['location']['lat'], geocoded['geometry']['location']['lng'], )) async def main(): await asyncio.wait([ present_result(fetch_place(place)) for place in PLACES ]) if __name__ == "__main__": loop = asyncio.get_event_loop() loop.run_until_complete(main()) # aiohttp will raise issue about unclosed # ClientSession so we perform cleanup manually loop.run_until_complete(session.close()) loop.close() 


A programação assíncrona é excelente para desenvolvedores de back-end interessados ​​em criar aplicativos escaláveis. Na prática, essa é uma das ferramentas mais importantes para criar servidores altamente competitivos.

Mas a realidade é triste. Muitos pacotes populares que lidam com problemas de E / S não devem ser usados ​​com código assíncrono. As principais razões para isso são:

  • Ainda baixa implementação do Python 3 e alguns de seus recursos avançados
  • Baixo entendimento de vários conceitos de simultaneidade entre iniciantes para aprender Python

Isso significa que muitas vezes a migração de aplicativos e pacotes síncronos multithread existentes é impossível (devido a restrições de arquitetura) ou muito cara. Muitos projetos podem se beneficiar muito da implementação do estilo de multitarefa assíncrona, mas apenas alguns o farão. Isso significa que, no momento, você terá muitas dificuldades tentando criar aplicativos assíncronos desde o início. Na maioria dos casos, isso será semelhante ao problema mencionado na seção "Exemplo prático de programação assíncrona" - interfaces incompatíveis e bloqueio não síncrono das operações de E / S. Obviamente, às vezes você pode deixar de esperar quando sente essa incompatibilidade e obter os recursos necessários de forma síncrona. Mas isso impedirá a execução de seu código uma da outra rotina enquanto você espera pelos resultados. Tecnicamente, isso funciona, mas também destrói todos os benefícios da programação assíncrona. Portanto, no final, combinar E / S assíncrona com E / S síncrona não é uma opção. Este é um jogo de tudo ou nada.

Outro problema são as longas operações vinculadas ao processador. Quando você executa uma operação de E / S, não há problema em liberar o controle de uma corotina. Ao escrever / ler a partir de um sistema de arquivos ou soquete, você acabará aguardando; portanto, uma chamada em espera é o melhor que você pode fazer. Mas e se você precisar calcular alguma coisa e souber que levará algum tempo? Obviamente, você pode dividir o problema em partes e cancelar o controle toda vez que avançar um pouco no trabalho. Mas logo você descobrirá que este não é um modelo muito bom. Uma coisa dessas pode tornar o código confuso e também não garante bons resultados.

A ligação temporal deve ser de responsabilidade do intérprete ou do sistema operacional.

Combinando código assíncrono com futuros assíncronos


Então, o que fazer se você tiver um código que execute E / S síncrona longa que não pode ou não deseja reescrever. Ou o que fazer quando você precisar executar algumas operações pesadas do processador em um aplicativo projetado principalmente para E / S assíncrona? Bem ... você precisa encontrar uma solução alternativa. E com isso quero dizer multithreading ou multiprocessing.

Isso pode não parecer muito bom, mas às vezes a melhor solução pode ser a que tentamos evitar. O processamento paralelo de tarefas que consomem muitos recursos em Python sempre é executado melhor devido ao multiprocessamento. E o multithreading pode lidar com operações de E / S igualmente bem (rapidamente e sem muitos recursos), como assíncrono e aguardando se configurado e manuseado com cuidado.

Portanto, às vezes, quando você não sabe o que fazer quando algo simplesmente não se encaixa no seu aplicativo assíncrono, use um pedaço de código que o coloque em um processo ou thread separado. Você pode fingir que é uma rotina, liberar controle para o loop de eventos e, finalmente, processar os resultados quando estiverem prontos.

Felizmente para nós, a biblioteca padrão do Python fornece o módulo concurrent.futures , que também é integrado ao módulo asyncio . Juntos, esses dois módulos permitem planejar funções de bloqueio executadas em encadeamentos ou processos adicionais, como se fossem corotinas assíncronas sem bloqueio.

Executores e futuros


Antes de vermos como incorporar threads ou processos em um loop de evento assíncrono, examinamos mais de perto o módulo concurrent.futures , que mais tarde se tornará o principal componente da nossa solução alternativa.

As classes mais importantes no módulo concurrent.futures são Executor e Future .

Executor é um conjunto de recursos que podem processar itens de trabalho em paralelo. Pode parecer muito semelhante em propósito às classes do módulo multiprocessador - Pool e dummy.Pool - mas tem uma interface e semântica completamente diferentes. Esta é uma classe base que não se destina a ser implementada e tem duas implementações específicas:

  • ThreadPoolExecutor : que representa um conjunto de encadeamentos
  • ProcessPoolExecutor : que representa um pool de processos

Cada executor apresenta três métodos:

  • submit (fn, * args, ** kwargs) : agenda a função fn para executar no pool de recursos e retorna um objeto Future que representa a execução do objeto chamado
  • map (func, * iterables, timeout = None, chunksize = 1) : a função func é executada na iteração de maneira semelhante ao multiprocessamento. Método Pool.map ()
  • shutdown (wait = True) : desliga o executor e libera todos os seus recursos.

O método mais interessante é submit () por causa do objeto Future que ele retorna. Representa a execução assíncrona dos chamados e indiretamente representa apenas o resultado. Para obter o valor de retorno real do objeto chamado despachado, você deve chamar o método Future.result () . E se o objeto chamado já estiver concluído, o método result () não o bloqueará e simplesmente retornará a saída da função. Caso contrário, ele o bloqueará até que o resultado esteja pronto. Pense nisso como uma promessa de resultado (na verdade, é o mesmo conceito que uma promessa em JavaScript). Você não precisa descompactá-lo imediatamente após recebê-lo (usando o método result () ), mas se você tentar fazer isso, é garantido que retornará algo:

 >>> def loudy_return(): ... print("processing") ... return 42 ... >>> from concurrent.futures import ThreadPoolExecutor >>> with ThreadPoolExecutor(1) as executor: ... future = executor.submit(loudy_return) ... processing >>> future <Future at 0x33cbf98 state=finished returned int> >>> future.result() 42 


Se você deseja usar o método Executor.map () , ele não difere em uso do método Pool.map () da classe Pool do módulo multiprocessador:

 def main(): with ThreadPoolExecutor(POOL_SIZE) as pool: results = pool.map(fetch_place, PLACES) for result in results: present_result(result) 


Usando o executor em um loop de eventos


As instâncias da classe Future retornadas pelo método Executor.submit () são conceitualmente muito próximas das corotinas usadas na programação assíncrona. É por isso que podemos usar artistas para criar um híbrido entre multitarefa colaborativa e multiprocessamento ou multithreading.

O núcleo desta solução alternativa é o método BaseEventLoop.run_in_executor (executor, func, * args) da classe do loop de eventos . Isso permite planejar a execução da função func em um conjunto de processos ou encadeamentos representado pelo argumento do executor. O mais importante sobre esse método é que ele retorna o novo objeto esperado (o objeto que pode ser esperado usando o operador de espera). Assim, graças a isso, você pode executar uma função de bloqueio que não é uma corotina exatamente como uma corotina, e ela não será bloqueada, não importa quanto tempo leve para terminar. Ele interromperá apenas a função que espera resultados dessa chamada, mas todo o ciclo de eventos continuará.

E um fato útil é que você nem precisa criar sua própria instância de executor. Se você passar None como argumento para o executor , a classe ThreadPoolExecutor será usada com o número padrão de threads (para Python 3.5, este é o número de processadores multiplicados por 5).

Portanto, suponha que não desejássemos reescrever a parte problemática do pacote python-gmaps que estava causando nossa dor de cabeça. Podemos adiar facilmente uma chamada de bloqueio para um thread separado, chamando loop.run_in_executor () , enquanto deixamos a função fetch_place () como a rotina prevista:

 async def fetch_place(place): coro = loop.run_in_executor(None, api.geocode, place) result = await coro return result[0] 


Essa solução é pior do que ter uma biblioteca totalmente assíncrona para fazer o trabalho, mas você sabe que pelo menos algo é melhor que nada.

Depois de explicar o que realmente é simultaneidade, agimos e analisamos um dos problemas paralelos típicos usando multithreading. Depois de identificar as principais falhas do nosso código e corrigi-las, passamos ao multiprocessamento para ver como funcionaria no nosso caso.

Depois disso, descobrimos que, com um módulo multiprocessador, o uso de vários processos é muito mais fácil do que threads básicos com multithreading. Mas somente depois disso percebemos que podemos usar a mesma API com threads, graças ao multiprocessing.dummy. Portanto, a escolha entre multiprocessamento e multithreading agora depende apenas de qual solução corresponde melhor ao problema, e não de qual solução possui a melhor interface.

Falando em adaptar o problema, finalmente experimentamos a programação assíncrona, que deve ser a melhor solução para aplicativos relacionados a E / S, apenas para entender que não podemos esquecer completamente os encadeamentos e processos. Então fizemos um círculo, de volta para onde começamos!

E isso nos leva à conclusão final deste capítulo. Não há solução que atenda a todos. Existem várias abordagens que você pode preferir ou gostar mais. Existem algumas abordagens que são mais adequadas para esse conjunto de problemas, mas você precisa conhecer todas elas para ter sucesso. Em cenários realistas, você pode usar todo o arsenal de ferramentas e estilos de paralelismo em um aplicativo, e isso não é incomum.

A conclusão anterior é uma excelente introdução ao tópico do próximo capítulo, capítulo 14 “Padrões de design úteis”. Como não há um modelo único que resolva todos os seus problemas. Você deve saber o máximo possível, porque, em última análise, você os usará todos os dias.

Source: https://habr.com/ru/post/pt484446/


All Articles