(
Nota: o autor do material original é Thomas @tclementdev, um usuário do github e do twitter . A narrativa em primeira pessoa usada pelo autor é salva na tradução abaixo. )
Eu acho que a maioria dos desenvolvedores usa o libdispatch de maneira ineficiente por causa de como foi apresentado à comunidade, bem como devido a documentação e APIs confusas. Cheguei a esse pensamento depois de ler a discussão sobre "simultaneidade" na lista de discussão do desenvolvimento Swift (evolução rápida). As mensagens de Pierre Habouzit (Pierre Habouzit - está envolvido no suporte ao libdispatch na Apple) são especialmente esclarecidas:
Ele também tem muitos tweets sobre este tópico:
Feito por mim:
- O programa deve ter muito poucas filas usando o pool global ( threads - aprox. Por. ). Se todas essas filas estiverem ativas simultaneamente, você receberá o mesmo número de threads em execução simultânea. Essas filas devem ser consideradas como contextos de execução no programa (GUI, armazenamento, trabalho em segundo plano, ...) que se beneficiam da execução simultânea.
- Comece com a execução sequencial. Ao descobrir um problema de desempenho, faça medições para descobrir a causa. E se a execução paralela ajudar, use-a com cuidado. Sempre verifique se o código paralelo está funcionando sob pressão do sistema. Por padrão, reutilize filas. Adicione linhas quando houver benefícios mensuráveis. A maioria dos aplicativos não deve usar mais de três a quatro filas.
- As filas que têm outra fila definida como destino funcionam bem e são dimensionadas.
( Observe perev.: Sobre como definir uma fila como destino para outra fila pode ser lido, por exemplo, aqui . ) - Não use dispatch_get_global_queue (). Isso não é compatível com a qualidade do serviço e as prioridades e pode levar a um crescimento explosivo no número de fluxos. Em vez disso, execute seu código em um dos seus contextos de execução.
- dispatch_async () é um desperdício de recursos para pequenos blocos executáveis (<1 ms), pois essa chamada provavelmente exigirá a criação de um novo encadeamento devido ao zelo excessivo do libdispatch. Em vez de alternar o contexto de execução para proteger o estado compartilhado, use mecanismos de bloqueio para acessar o estado compartilhado ao mesmo tempo.
- Algumas classes / bibliotecas são bem projetadas, pois reutilizam o contexto de execução que o código de chamada passa para eles. Isso permite o uso de travamento convencional para garantir a segurança da linha. os_unfair_lock é geralmente o mecanismo de bloqueio mais rápido do sistema: funciona melhor com prioridades e causa menos alternâncias de contexto.
- No caso de execução paralela, suas tarefas não devem lutar entre si; caso contrário, a produtividade cai acentuadamente. A luta assume muitas formas. O caso óbvio: a luta para capturar a fechadura. Mas, na realidade, essa luta significa nada mais do que usar um recurso compartilhado, que se torna um gargalo: IPC (comunicação entre processos) / daemons OS, malloc (bloqueio), memória compartilhada, E / S.
- Você não precisa de todo o código para executar de forma assíncrona, a fim de evitar um aumento explosivo no número de threads. É muito melhor usar um número limitado de filas inferiores e se recusar a usar dispatch_get_global_queue ().
( Note perev. 1: aparentemente, este é um caso em que ocorre um aumento explosivo no número de threads ao sincronizar um grande número de tarefas paralelas. “Se eu tiver muitos blocos e todos eles quiserem esperar, podemos obter o que chamamos de thread explosão. " )
( Nota p. 2: da discussão , pode-se entender que Pierre Habuzit significa as linhas "que são conhecidas pelo kernel quando têm tarefas" nas filas inferiores. Isso é sobre o kernel do sistema operacional. ) - Não devemos esquecer a complexidade e os erros que surgem em uma arquitetura cheia de execução assíncrona e retornos de chamada. O código executável sequencialmente ainda é muito mais fácil de ler, gravar e manter.
- Filas competitivas são menos otimizadas que sequenciais. Use-os se estiver medindo ganhos de desempenho, caso contrário, é uma otimização prematura.
- Se você precisar enviar tarefas em uma fila de forma assíncrona e síncrona, em vez de dispatch_sync (), use dispatch_async_and_wait (). dispatch_async_and_wait () não garante a execução no encadeamento do qual a chamada se originou, o que reduz a alternância de contexto quando a fila de destino está ativa.
( Note transl. 1: na verdade, dispatch_sync () também não garante, a documentação sobre ele afirma apenas “executa um bloco no encadeamento atual, sempre que possível. Com uma exceção: o bloco enviado para a fila principal é sempre executado no encadeamento principal. " )
( Observe a tradução 2: about dispatch_async_and_wait () na documentação e no código fonte ) - O uso correto de 3-4 núcleos não é tão simples. A maioria dos que tentam, de fato, não consegue lidar com as reduções de escala e desperdiçar energia por causa de um pequeno aumento de produtividade. Como os processadores trabalham com superaquecimento não ajudará. Por exemplo, a Intel desativará o Turbo-Boost se forem usados núcleos suficientes.
- Avalie o desempenho do seu produto no mundo real para garantir que seja mais rápido e não mais lento. Cuidado com os micro testes de desempenho - eles ocultam a influência do cache e mantêm o pool de threads quente. Para verificar o que você está fazendo, você deve sempre fazer um teste de macro.
- libdispatch é eficaz, mas não há milagres. Os recursos não são infinitos. Você não pode ignorar a realidade do sistema operacional e do hardware em que o código é executado. Além disso, nem todo código é bem paralelo.
Dê uma olhada em todas as chamadas dispatch_async () no seu código e pergunte a si mesmo: a tarefa que você envia com essa chamada realmente vale a troca de contexto. Na maioria dos casos, o bloqueio é provavelmente a melhor escolha.
Assim que você começar a usar e reutilizar filas (contextos de execução) de um conjunto pré-projetado, haverá o risco de conflitos. Surge perigo ao enviar tarefas para essas filas usando dispatch_sync (). Isso geralmente acontece quando filas são usadas para segurança do encadeamento. Novamente: a solução é usar mecanismos de bloqueio e usar dispatch_async () somente quando você precisar alternar para outro contexto de execução.
Eu, pessoalmente, vi grandes melhorias de desempenho ao seguir essas diretrizes.
(em programas altamente carregados). Esta é uma nova abordagem, mas vale a pena.
Mais links
O programa deve ter muito poucas filas usando o pool global
(
Nota perev.: Ao ler o último link, não resisti e transferi uma peça do meio da correspondência de Pierre Habuzit com Chris Luttner. Abaixo está uma das respostas de Pierre Habuzit em 039420.html )
<...>
Entendo que é difícil para mim expressar meu ponto de vista, porque não sou um cara em arquitetura de linguagem, sou um cara em arquitetura de sistemas. E definitivamente não entendo os atores o suficiente para decidir como integrá-los ao sistema operacional. Mas para mim, voltando ao exemplo do banco de dados, o Actor-Database-Data, ou o Actor-Network-Interface de uma correspondência anterior, são diferentes, digamos, dessa consulta SQL ou de rede. A primeira são as entidades que o sistema operacional deve conhecer no kernel. Enquanto uma consulta SQL ou de rede são apenas atores enfileirados para execução em primeiro lugar. Em outras palavras, esses atores de nível superior são diferentes porque são de nível superior, diretamente no tempo de execução do kernel / baixo nível. E esta é a essência que o núcleo deve ser capaz de raciocinar. Isso os torna ótimos.
Existem 2 tipos de filas e o nível de API correspondente na biblioteca de expedição:
- filas globais que não são como as outras. E, na realidade, eles são apenas uma abstração sobre o pool de threads.
- todas as outras filas nas quais você pode definir o destino um para o outro conforme desejar.
Hoje ficou claro que isso foi um erro e que deveria haver três tipos de filas:
- filas globais, que não são filas reais, mas representam qual família de sistemas atribui seu contexto de execução (principalmente prioridades). E devemos proibir o envio de tarefas diretamente para essas filas.
- filas inferiores (que o GCD rastreou nos últimos anos e chama de "bases" no código fonte ( parece que o código fonte do próprio GCD se destina a - aprox. transl. ). As filas inferiores são conhecidas pelo kernel quando têm tarefas.
- quaisquer outras filas "internas" que o kernel não conheça.
No grupo de desenvolvimento de despacho, lamentamos todos os dias que a diferença entre o segundo e o terceiro grupo de filas não tenha sido inicialmente esclarecida na API.
Gosto de chamar o segundo grupo de "contextos de execução", mas entendo por que você os chama de Atores. Talvez isso seja mais consistente (e o GCD fez o mesmo, apresentando isso e aquilo como uma fila). Esses "atores" de nível superior devem ser poucos em número, porque, se todos se tornarem ativos ao mesmo tempo, precisarão do mesmo número de threads no processo. E este não é um recurso que pode ser escalado. É por isso que é importante distinguir entre eles. E, como discutimos, eles também são comumente usados para proteger um estado, recurso ou algo compartilhado. Pode não ser possível fazer isso usando atores internos.
<...>
Comece com a execução sequencial.
Não use filas globais
Cuidado com as linhas competitivas
Não use chamadas assíncronas para proteger o estado compartilhado
Não use chamadas assíncronas para pequenas tarefas
Algumas classes / bibliotecas devem ser apenas síncronas
A luta de tarefas paralelas entre si é um matador de produtividade
Para evitar conflitos, use mecanismos de bloqueio quando precisar proteger um estado compartilhado
Não use semáforos para aguardar uma tarefa assíncrona
A API NSOperation tem algumas armadilhas graves que podem levar à degradação do desempenho.
Evite micro testes de desempenho
Os recursos não são ilimitados
Sobre dispatch_async_and_wait ()
Usar 3-4 núcleos não é fácil
Muitas melhorias de desempenho no iOS 12 foram alcançadas com daemons de thread único