Ao desenvolver um software, quando você começa a pensar / projetar as seções simultâneas?


8

Seguindo o princípio de não otimizar muito cedo, estou me perguntando em que momento do design / desenvolvimento de um software você começa a pensar nas oportunidades de concorrência?

Eu posso imaginar que uma estratégia seria escrever um único aplicativo encadeado e, através do perfil, identificar seções que são candidatas a serem executadas em paralelo. Outra estratégia que eu já vi um pouco é considerar o software por grupos de tarefas e tornar paralelas as tarefas independentes.

Parte do motivo da pergunta é que, é claro, se você esperar até o final e refatorar apenas o software para operar simultaneamente, poderá estruturar as coisas da pior maneira possível e ter uma tarefa importante em mãos.

Que experiências ajudaram a determinar quando você considera a paralelização no seu design?

Respostas:


7

O problema das tarefas de encadeamento é que você deseja alta coesão, baixo acoplamento e bom encapsulamento. Curiosamente, esses também são objetivos de projeto dignos para aplicativos de thread único. Hoje eu tinha uma tarefa que originalmente não planejava paralelizar, mas, quando o fiz, envolvia pouco mais do que renomear uma função run()e alterar como ela era chamada.

Não otimizar muito cedo significa não colocar tudo em um encadeamento "apenas por precaução", mas você também não deve pintar-se em um canto da arquitetura, para que seja muito difícil otimizar se necessário.


Sim, funções reentrantes, filas de trabalho e similares podem ajudar em aplicativos de thread único e multithread, mas também permitem uma boa extensibilidade para o processamento paralelo posteriormente. O que economiza muita dor de cabeça com problemas de sincronização posteriormente.
Coder

2

Programadores Java devem adotar a Callableinterface para as unidades de trabalho. Se todo o seu aplicativo consiste em um ciclo de criação de Chamadas, envio de todas as unidades para um Executor, manipulação de tarefas pós-geração, você tem algo que pode ser facilmente transformado em processamento serial, "três filas de trabalho" e "fazer tudo de uma só vez "simplesmente escolhendo o executor certo.

Estamos lentamente adaptando esse padrão, pois é muito comum que a abordagem serial fique muito lenta em um ponto e, então, precisamos fazê-lo de qualquer maneira.


1

Isso varia com o projeto. Às vezes, é muito fácil ver o que pode ser paralelo: talvez o seu programa processe lotes de arquivos. Suponha que o processamento de cada arquivo seja completamente independente de todos os outros arquivos, portanto, pode ser óbvio que você possa processar 1 arquivo por vez, 10 ou 100, e nenhum desses trabalhos afetará o outro.

Fica um pouco mais complicado quando os trabalhos paralelos em potencial não são os mesmos. Ao processar um arquivo de imagem, você pode ter um trabalho que cria um histograma, outro que produz uma miniatura e talvez outro que extrai os metadados EXIF ​​e, em seguida, um trabalho final que pega a saída de todos esses trabalhos e os armazena em um banco de dados. Neste exemplo, talvez não esteja claro se eles devem ser executados em paralelo ou se devem (o último trabalho terá que esperar que os trabalhos anteriores sejam concluídos com êxito).

Nas minhas experiências, a maneira mais fácil de paralelizar algo é procurar processos que possam ser executados da maneira mais independente possível (como no primeiro exemplo) e começar com eles. Eu só tentaria executar o segundo exemplo em paralelo se pensasse em obter um ganho significativo de desempenho com ele.


1

Você deve criar simultaneidade no seu aplicativo desde o início. Normalmente, como otimização, eu concordaria que deveria ser deixado para depois, se não for inerentemente óbvio. O problema é que a simultaneidade pode exigir a re-arquitetura do seu aplicativo a partir do zero, na pior das hipóteses - alguns sistemas são praticamente impossíveis de ter a simultaneidade implementada. Um exemplo fácil disso são os sistemas que compartilham dados - por exemplo, os aspectos de simulação e renderização de um jogo.


0

Eu diria que o encadeamento é parte da arquitetura do aplicativo. Portanto, é uma das primeiras coisas em que preciso pensar.

Por exemplo, quando eu faço um aplicativo da GUI, o código da GUI é de thread único, para que tarefas em execução longa (por exemplo, processamento XML) bloqueiem a GUI e devam ser executadas em um thread de segundo plano.

Por exemplo, um servidor seria baseado em encadeamento, onde cada solicitação é tratada por um novo encadeamento ou o servidor poderia ser orientado a eventos e usar apenas um encadeamento por núcleo de CPU, mas, novamente, tarefas de execução longa devem ser executadas em um thread de segundo plano ou seja dividido em tarefas menores.


0

Com a maneira como abordo as coisas, o multithreading meio que é gratuito e relativamente simples de aplicar em retrospectiva. Mas estou pensando nos dados primeiro. Não sei se isso funciona para todos os domínios, mas tentarei abordar como faço para fazê-lo.

Então, primeiro, trata-se do tipo mais grosseiro de dados necessários para o software que será processado com frequência. Se é um jogo que pode ser algo como malhas, sons, movimento, emissores de partículas, luzes, texturas, coisas desse tipo. E é claro que há muito em que pensar se você detalhar apenas as malhas e pensar em como elas devem ser representadas, mas vamos pular isso por enquanto. No momento, estamos pensando no nível arquitetônico mais amplo.

E meu primeiro pensamento é: "Como unificamos a representação de todas essas coisas para que possamos alcançar um padrão de acesso relativamente uniforme para todos esses tipos de coisas?" E meu primeiro pensamento pode ser armazenar cada tipo de coisa em sua própria matriz contígua com uma maneira gratuita de recuperar espaços vazios. E isso tende a unificar a API para que possamos usar mais facilmente, digamos, o mesmo tipo de código para serializar malhas que realizamos luzes e texturas, pelo menos na medida em que local e como esses componentes são acessados. Quanto mais podemos unificar como tudo é representado, mais o código que acessa essas coisas tende a assumir uma forma uniforme.

Isso é legal. Agora também podemos apontar essas coisas com índices de 32 bits e ocupar apenas metade da memória de um ponteiro de 64 bits. E, ei, podemos definir interseções em tempo linear agora, se pudermos associar um conjunto de bits paralelo, por exemplo. Ah, e esse conjunto de bits pode nos devolver um conjunto de índices classificados para percorrer em ordem seqüencial para melhorar os padrões de acesso à memória, sem precisar recarregar a mesma linha de cache várias vezes em um único loop. Podemos testar 64 bits por vez. Se todos os 64 bits não estiverem definidos, podemos pular mais de 64 elementos de uma vez. Se todos eles estiverem definidos, podemos processá-los todos de uma vez. Se algumas estão definidas, mas não todas, podemos usar as instruções do FFS para determinar rapidamente quais bits estão definidos.

Mas, espere, isso é meio caro se quisermos associar dados a algumas centenas de milhares de coisas. Então, vamos usar uma matriz esparsa, assim:

insira a descrição da imagem aqui

E ei, agora que temos tudo armazenado em matrizes esparsas e indexadas, seria muito fácil tornar isso uma estrutura de dados persistente.

insira a descrição da imagem aqui

Agora podemos escrever funções mais baratas, livres de efeitos colaterais, pois elas não precisam copiar profundamente o que não mudou.

E aqui já me foi dada uma dica após aprender sobre os motores ECS, mas agora vamos pensar sobre que tipo de funções gerais devem estar operando em cada tipo de componente. Podemos chamar esses "sistemas". O "SoundSystem" pode processar os componentes "Sound". Cada sistema é uma função ampla que opera em um ou mais tipos de dados.

insira a descrição da imagem aqui

Isso nos deixa com muitos casos em que, para qualquer tipo de componente, apenas um ou dois sistemas geralmente os acessam. Hmm, com certeza parece que isso ajudaria na segurança do encadeamento e absolutamente reduziria a contenção do encadeamento.

Além disso, tento pensar em como fazer passagens homogêneas sobre os dados. Em vez de gostar:

for each thing:
    play with it
    cuddle it
    kill it

Eu procuro dividi-lo em vários passes mais simples:

for each thing:
    play with it
for each thing:
    cuddle it
for each thing:
    kill it

Às vezes, isso exige o armazenamento de algum estado intermediário para a próxima passagem adiada homogênea ao processo, mas descobri que isso realmente me ajuda a manter e raciocinar sobre o código, sabendo que cada loop tem uma lógica mais simples e uniforme. E, ei, isso parece simplificar a segurança do thread e reduzir a contenção de threads.

E você continua assim até descobrir que possui uma arquitetura realmente fácil de paralelizar com a confiança sobre a segurança e a exatidão do encadeamento, mas inicialmente com o foco de unificar representações de dados, ter padrões de acesso à memória mais previsíveis, reduzindo uso de memória, simplificando o fluxo de controle para passes mais homogêneos, reduzindo o número de funções em seu sistema que causam efeitos colaterais sem incorrer em custos profundos de cópia muito caros, unificando sua API etc.

Quando você combina todas essas coisas, tende a acabar com um sistema que minimiza a quantidade de estado compartilhado em que você se depara com um design realmente amigável à simultaneidade. E, se qualquer estado precisar ser compartilhado, você geralmente acha que não há muita disputa, onde é barato usar alguma sincronização sem causar um congestionamento de tráfego de encadeamento e, além disso, que muitas vezes pode ser tratado por sua estrutura de dados central que unifica a representação de todas as coisas no sistema, para que você não precise aplicar sincronizações de threads em centenas de lugares diferentes, apenas um punhado.

Agora, quando analisamos um dos componentes mais complexos, como malhas, repetimos o mesmo processo de design, começando com o pensamento sobre os dados. E se fizermos isso corretamente, poderemos até paralelizar facilmente o processamento de uma única malha, mas o projeto arquitetônico mais amplo que estabelecemos já nos permite paralelizar o processamento de várias malhas.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.