Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introdução

Seja bem-vindo a Programação Paralela e Concorrente, um livro a ser usado na disciplina de mesmo nome. Essa disciplina busca ensinar aos alunos os principais conceitos e técnicas que são úteis ao trabalhar com sistemas concorrentes e paralelos.

Desde o final do século XX, a importância de aprender esses conceitos e técnicas vem crescendo, pois a indústria computacional passou a convergir para arquiteturas, tanto físicas quanto lógicas, que exibem propriedades concorrentes, paralelas e/ou distribuídas. A construção de aplicações e sistemas que satisfaçam os requisistos, cada vez mais exigentes, de performance, responsividade e tolerância a falhas depende do correto entendimento e aproveitamento dessas arquiteturas.

Hoje, qualquer que seja a área de atuação de um profissional de computação — frontend, backend, dados, segurança — ele precisará, no mínimo, estar ciente dos conceitos e técnicas vistos nesse livro, caso queira produzir software da mais alta qualidade.

No entanto, muitos profissionais hoje escolhem fazer vista grossa para essa área da computação, devido ao fato de que os bugs encontrados durante o desenvolvimento de sistemas concorrentes, paralelos e distribuidos estão entre os mais difíceis de entender, capturar e resolver. Enquanto num programa sequencial as mesmas entradas geram as mesmas saídas, em programas concorrentes e paralelos essa garantia não existe, pois a ordem que as tarefas são executadas é definida a cada execução.

Portanto, recomendo que o leitor não faça essa vista grossa, pois os profissionais capacitados para lidar com os problemas mais difíceis são justamente os mais valiosos.

Como este livro é organizado

O livro é dividido em Partes, cada uma contendo um conjunto de capítulos tratando de tópicos próximos entre si.

A Parte I, Conceitos Fundamentais, introduz o vocabulário básico de todo o livro. As partes II à V são focadas, cada uma, em 1 ou mais modelos de concorrência específicos.

A Parte II, Threads e Locks, cobre o modelo de concorrência mais comum e implementado na maioria das linguagens mainstream, e é o modelo que a maioria das pessoas tem em mente quando se fala de concorrência. Consiste em threads do SO se comunicando através do compartilhamento de memória e sincronizando entre si por meio de locks.

A Parte III, Concorrência via Mensagens, cobre dois modelos de concorrência que pertencem a família de modelos baseados em troca de mensagens (message-passing): Communicating Sequential Processes (CSP) e o modelo Actor.

A Parte IV, Programação Assíncrona, cobre um modelo de concorrência baseado numa única thread de execução, mas que extremamente eficiente para programas que ficam a maior parte do seu tempo esperando por I/O, como servidores web e interfaces de usuário.

A Parte V, Atomics e Concorrência Lock-Free, cobre um modelo de concorrência similar ao da Parte II, por também ser baseado em threads do SO, mas trocando locks por operações atômicas implementadas no hardware.

A Parte VII, Memória Transacional, cobre dois modelos de concorrência baseados na ideia de memória transacional, só que um é implementado a nível de hardware (HTM) e outro a nível de software (STM).

A Parte VIII, Paralelismo via Memória Distribuída, cobre uma abordagem de paralelismo muito utilizada no contexto de High-Performance Computing, baseada no padrão MPI.

A Part IX, Tópicos Avançados, contém uma variedade de capítulos cobrindo assuntos mais nichados, como a história da concorrência, modelos de concorrência não cobertos previamente, implementação de runtimes e bibliotecas concorrentes, aplicações de concorrência em domínios específicos, análise de arquiteturas concorrentes no mundo real, entre vários outros.

O Anexo A, Kotlin para o Programador Apressado, introduz a linguagem de programação Kotlin sem perder tempo explicando conceitos que são óbvios para pessoas que já programam, mas focando nos conceitos mais únicos da linguagem e relevantes para nosso contexto. Os anexos B e C fazem a mesma coisa para as linguagens Go e Elixir.

Dentro das Partes do livro, alguns capítulos ou seções são marcados como “Em Detalhe”. Esses apresentam um nível de aprofundamento maior sobre algum assunto, e geralmente assumem um nível de conhecimento maior nos pré-requisitos. Geralmente esses capítulos explicam mecanismos, algoritmos e provas matemáticas. Esse aprofundamento não é essencial para o entendimento do conteúdo, porém é incluído para complementar o conhecimento e dar um entendimento mais profundo para o leitor.

Outros capítulos e seções são marcados como “Interlúdio”. Esses apresentam um conteúdo que distoa daquele que é tratado na Parte e que não foi possível encaixar numa Parte específica.

O que este livro assume

Este livro assume que as Partes I (Conceitos Fundamentais) e II (Threads e Locks) serão lidas de maneira sequencial. Os capítulos dessas partes constroem o modelo mental e a terminologia básica de todo o livro. As partes restantes podem ser lidas na ordem que o leitor desejar.

Este livro assume que o leitor já possui:

  • Conhecimento considerável em Programação Imperativa e Orientada a Objetos
  • Conhecimento considerável em Arquitetura de Computadores
  • Conhecimento básico em Programação Funcional.

Também assume-se que o leitor está fazendo ou já fez um curso de sistemas operacionais. Sistemas operacionais são a aplicação canônica para os conceitos de concorrência, e ambas as áreas se retro-alimentam. Apesar dos conceitos e técnicas de concorrência e paralelismo serem aplicáveis em outros domínios (sistemas de banco de dados, servidores de rede, High-Performance Computing, grid computing, simulação científica, computação gráfica, machine learning, etc), a concorrência como disciplina nasceu diretamente do estudo de sistemas operacionais. Além disso, todos os primitivos que serão estudados, como threads e locks, são implementados pelo kernel do sistema operacional.

Por padrão, os exemplos assumem que serão executados em algum sistema do tipo Unix (Linux, macOS, WSL, etc). Caso seja necessário que um exemplo seja executado num ambiente Windows nativo, será notado em específico.

Finalmente, o livro assume que o usuário sabe ler a documentação técnica de linguagens, bibliotecas, etc. Esse tipo de documentação, inclusive, costuma existir somente em inglês.

Como usar este livro

Este livro busca ensinar os conceitos e técnicas de maneira genérica e tecnologicamente agnóstica. Ou seja, queremos que o conhecimento seja transferível para qualquer tipo de aplicação, linguagem ou framework que o aluno venha a trabalhar.

Isso não significa que não usaremos uma linguagem ou biblioteca(s) específicas ao longo do livro. A maioria dos exemplos está escrito em C/C++ e Kotlin. Alguns capítulos usam Go, Elixir, JavaScript e Clojure, pois essas são linguagens que demonstram melhor certos modelos de concorrência.

Como rodar os exemplos de código

Os exemplos de código desse livro possuem duas funcionalidades relevantes: eles são executáveis e podem omitir partes do código. No Código P-1, temos um bloco de código em C que calcula a sequência de Fibonacci para um dado índice \( n \):

#include <stdio.h>

/*
 * Retorna o elemento de índice n da sequência de
 * Fibonacci (0, 1, 1, 2, 3, 5, ...).
 */
int fibonacci(int n) {
  int a = 0, b = 1;
  for (int i = 0; i < n; i++) {
    int tmp = a + b;
    a = b, b = tmp;
  }
  return a;
}

int main() {
  int n = 4;
  printf("%d\n", fibonacci(n));
}

Código P-1: Calculando a sequência de Fibonacci em C.

Passe o seu mouse (ou dedo) por cima do bloco de código, e verá que dois butões aparecerão no canto superior direito: um para Copiar e outro para Executar o código (com ícone de Play). Clique no botão de executar e veja o que o programa faz.

Ao longo do livro, veremos exemplos de código que serão gradualmente modificados. Fazemos isso para corrigir um bug ou adicionar mais funcionalidades. Geralmente, essas mudanças afetam poucas linhas. Seria desperdício de espaço se repetíssemos o código completo toda vez que fizermos uma mudança pequena. Para evitar isso, alguns exemplos de código possuem partes omitidas, que só são mostradas ao clicar num botão de olhinho para Revelar o código.

Por exemplo, suponha que queremos mudar o Código P-1 para mostrar uma saída mais detalhada. Trata-se de uma mudança apenas na main. O restante do código não é modificado, permanecendo igual ao que era antes. Portanto, omitimos o restante no Código P-2.

#include <stdio.h>

/*
 * Retorna o elemento de índice n da sequência de
 * Fibonacci (0, 1, 1, 2, 3, 5, ...).
 */
int fibonacci(int n) {
  int a = 0, b = 1;
  for (int i = 0; i < n; i++) {
    int tmp = a + b;
    a = b, b = tmp;
  }
  return a;
}
// --restante omitido--

int main() {
  int n = 14;
  printf("Elemento de índice %d da sequência de Fibonacci: %d\n", n, fibonacci(n));
}

Código P-2: Calculando a sequência de Fibonacci em C com uma saída mais amigável.

Tente passar o mouse ou dedo em cima do bloco de código e clique no ícone de olho. O restante do código será revelado.

Sempre que um código tiver omitido algum trecho, iremos usar --restante omitido-- para indicar.

Apesar da funcionalidade de executar código no livro ser bastante útil, o ambiente que os programas são executados é bem restrito. A concorrência é mais limitada, e não podemos executar programas que leem da entrada. Portanto, eu sempre mostrarei o resultado da execução como se estivesse compilando e executando o programa localmente, numa máquina pessoal.