Concorrência, Paralelismo, Processos, Threads, sistemas Monotarefa e Multitarefa

É comum achar que concorrência e paralelismo são a mesma coisa, mas não. Rob Pike, um dos criadores da linguagem Go, explicou em uma apresentação:

“Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once.”

Concorrência é sobre lidar com várias coisas ao mesmo tempo e paralelismo é sobre fazer várias coisas ao mesmo tempo.

Concorrência é sobre a execução sequencial e disputada de um conjunto de tarefas. O responsável por esse gerenciamento é o escalonador de processos do sistema operacional.

C# (C Sharp) Avançado
Curso de C# (C Sharp) Avançado
CONHEÇA O CURSO

Paralelismo é sobre a execução paralela de tarefas, ou seja, mais de uma por vez (de forma simultânea), a depender da quantidade de núcleos (cores) do processador. Quanto mais núcleos, mais tarefas paralelas podem ser executadas. Paralelismo lida com linhas de execuções (threads) que são executadas paralelamente com o processo pai (não necessariamente no exato mesmo tempo, pois elas podem concorrer entre si com outras threads de outros processos entre operações de I/O ou CPU bound). Não existe paralelismo sem concorrência. Cada linha de paralelismo é concorrente.

Podemos fazer uma analogia em que concorrência é aquela fila do drive-thru em que carros estão disputando o recurso do atendimento, um por vez:

E paralelismo é o pedágio que permite que carros progridam em diferentes fluxos ao mesmo tempo:

Observe que, mesmo havendo paralelismo, existe a concorrência. Nem sempre um núcleo estará sempre disponível apenas para uma linha de execução (veja que uma saída do pedágio tem fila, ou seja, carros concorrendo para passar). Quem gerencia o que o núcleo vai executar em dado momento do tempo é o escalonador de processos do sistema operacional.

A concorrência é base para o projeto e implementação de sistemas multitarefas, que é o que veremos a seguir.

Monotarefa versus Multitarefa

Os primeiros sistemas operacionais suportavam a execução de apenas uma tarefa por vez. Nesse modelo, o processador, a memória e os periféricos ficavam dedicados a uma única tarefa. Tínhamos um fluxo bem linear, como pode ser visto nesse diagrama:

Apenas no término da execução de uma tarefa que outra poderia ser carregada na memória e então executada.

O problema desse modelo é que enquanto o processo realizava uma operação de I/O para, por exemplo, ler algum dado do disco, o processador ficava ocioso. Ademais, uma operação do processador é infinitamente mais rápida que qualquer uma de leitura ou escrita em periféricos.

Para se ter ideia, quando falamos de uma operação que a CPU executa, lidamos com nanosegundos, enquanto em uma operação de rede consideramos milesegundos.

Se você “pingar” o Google observará isso:

~> ping google.com.br
PING google.com.br (216.58.202.3): 56 data bytes
64 bytes from 216.58.202.3: icmp_seq=0 ttl=52 time=20.845 ms
64 bytes from 216.58.202.3: icmp_seq=1 ttl=52 time=22.222 ms
64 bytes from 216.58.202.3: icmp_seq=2 ttl=52 time=21.342 ms
64 bytes from 216.58.202.3: icmp_seq=3 ttl=52 time=20.613 ms

Conforme as aplicações foram evoluindo, começou a se tornar um problema. Um exemplo clássico é um editor de texto que precisa executar diversas tarefas simultaneamente como, por exemplo, formatar o texto selecionado e verificar a ortografia dele, duas tarefas (simplificando) sendo executadas sobre a mesma “massa” de dados que é o texto selecionado.

A solução encontrada para resolver esse problema foi permitir ao processador suspender a execução de uma tarefa que estivesse aguardando dados externos ou algum evento e passar a executar outra tarefa. Em outro momento de tempo, quando os dados estivessem disponíveis, a tarefa suspensa poderia ser retomada do ponto exato de onde ela havia parado. Nesse modelo, mais de um programa é carregado na memória. O mecanismo que permite a retirada de um recurso (o processador, por exemplo) de uma tarefa, é chamado de preempção.

Sistemas preemptivos são mais produtivos, ademais, várias tarefas podem estar em execução ao mesmo intervalo de tempo alternando entre si o uso dos recursos da forma mais justa que for possível. Nesse tipo de sistema as tarefas alteram de estado e contexto a todo instante.

Os estados de uma tarefa num sistema preemptivo:

  • Nova: A tarefa está sendo criada (carregada na memória);
  • Pronta: A tarefa está em memória aguardando a disponibilidade do processador para ser executada pela primeira vez ou voltar a ser executada (na hipótese de que ela foi substituída por outra tarefa, devido à preempção);
  • Executando: O processador está executando a tarefa e alterando o seu estado;
  • Suspensa: A tarefa não pode ser executada no momento por depender de dados externos ainda não disponíveis (dados solicitados à rede ou ao disco, por exemplo.);
  • Terminada: A execução da tarefa foi finalizada e ela já pode sair da memória;

O diagrama de estado das tarefas com preempção de tempo:

Quantum pode ser entendido como o tempo que o sistema operacional dá para que os processos usem a CPU. Quando o quantum de um processo termina, mesmo que ele ainda não tenha terminado a execução de suas instruções, o contexto dele é trocado, é salvo na sua pilha de execução onde ele parou, os dados necessários e então ele volta pro estado de “pronto”, até que o sistema operacional através do seu escalonador de processos volte a “emprestar” a CPU pra ele (e então ele volta pro estado de “executando”). Trocas de contexto (de pronto pra executando etc) acontecem a todo momento. Milhares delas. É uma tarefa custosa para o sistema operacional, mas que é necessária para que a CPU não fique ociosa.

O processador executa uma tarefa por vez. Cabe ao escalonador do sistema operacional cuidar dessa fila de tarefas, decidir quem tem prioridade, quem tem o quantum disponível etc. Claro, computadores com mais de um processador (core) vão executar mais de uma tarefa por vez (ainda assim uma tarefa cada, de todo modo).

O que é um processo?

Um processo pode ser visto como um container de recursos utilizados por uma ou mais tarefas. Processos são isolados entre si (inclusive, através de mecanismos de proteção a nível de hardware), não compartilham memória, possuem níveis de operação e quais chamadas de sistemas podem executar. Como os recursos são atribuídos aos processos, as tarefas fazem o uso deles a partir do processo. Dessa forma, uma tarefa de um processo A não consegue acessar um recurso (a memória, por exemplo) de uma tarefa do processo B.

O kernel do sistema operacional possui descritores de processos, denominados PCBs (Process Control Blocks) e eles armazenam informações referentes aos processos ativos e cada processo possui um identificador único no sistema, conhecido como PID (Process IDentifier).

As tarefas de um processo podem trocar informações com facilidade, pois compartilham a mesma área de memória. No entanto, tarefas de processos distintos não conseguem essa comunicação facilmente, pois estão em áreas diferentes de memória. Esse problema é resolvido com chamadas de sistema do kernel que permitem a comunicação entre processos (IPC – Inter-Process Communication).

PHP Avançado
Curso de PHP Avançado
CONHEÇA O CURSO

O que é uma thread?

Os processos podem ter uma série de threads associadas e as threads de um processo são conhecidas como threads de usuário, por executarem no modo-usuário e não no modo-kernel. Uma thread é uma “linha” de execução dentro de um processo. Cada thread tem o seu próprio estado de processador e a sua própria pilha, mas compartilha a memória atribuída ao processo com as outras threads “irmãs” (filhas do mesmo processo).

O núcleo (kernel) dos sistemas operacionais também implementa threads, mas essas são chamadas de threads de kernel (ou kernel-threads). Elas controlam atividades internas que o sistema operacional precisa executar/cuidar.

Os sistemas operacionais mais antigos não possuiam suporte a threads e a solução encontrada pelos desenvolvedores foi a construção de bibliotecas para criar e controlar threads dentro de cada processo. O sistema operacional, nesse caso, não tinha conhecimento dessas threads e nem conseguia controlá-las.

Já nos sistemas operacionais modernos as threads de usuário (dos processos) são mapeadas para threads do kernel, dessa forma, o sistema operacional tem conhecimento e controle para escalonar os fluxos.

A visão de como as threads de usuário são mapeadas para as threds de núcleo em sistemas operacionais modernos que usam o modelo N:M (onde N threads de usuário são mapeadas para M threads de núcleo):

Veja que as quatro threads do processo p1 foram mapeadas para três threads no núcleo, enquanto no processo p2 cada thread foi mapeada para outra no núcleo num modelo 1:1.

Em 1995 foi definido o padrão IEEE POSIX 1003.1c, também conhecido como PThreads que busca definir uma interface padronizada para a criação e manipulação de threads na linguagem C. Esse é o padrão mais utilizado hoje em dia pelas linguagens e pelos sistemas operacionais. Ele resolveu o problema que existia no passado de cada sistema operacional implementar a sua forma de se criar threads, o que gerava problema de portabilidade das aplicações.

Voltando ao assunto de paralelismo, agora que já temos uma visão do que é um processo e de que ele pode ter linhas paralelas de execução (threads):

Nesse diagrama o processo tem três threads que estão sendo executadas paralelamente em três diferentes núcleos do processador.

Concluindo

Se você se interessa pelos pormenores desse rico assunto, o livro Sistemas Operacionais Modernos do Tanenbaum é uma ótima referência de estudo. E, de certo, uma inspiração para quem vos escreve, bem como a principal referência desse post.

Deixe seu comentário
Share

Head de desenvolvimento. Vasta experiência em desenvolvimento Web com foco em PHP. Graduado em Sistemas de Informação. Pós-graduando em Arquitetura de Software Distribuído pela PUC Minas. Zend Certified Engineer (ZCE) e Coffee Addicted Person (CAP). @KennedyTedesco