Definindo ambientes de execução
Last updated
Last updated
Antes de falarmos das diferenças entre algumas implementações de linguagens de programação, precisamos definir o que é uma linguagem de programação. De acordo com a Wikipedia:
A linguagem de programação é um método padronizado, formado por um conjunto de regras sintáticas e semânticas, de implementação de um código fonte - que pode ser compilado e transformado em um programa de computador, ou usado como script interpretado - que informará instruções de processamento ao computador.
Ou seja, quando falamos em linguagem de programação, estamos nos referindo a uma especificação sintática e semântica que, através de um processo de compilação (ou interpretação), será transformado em código de máquina que o computador possa entender.
Para que uma linguagem seja executada no computador, é preciso ser implementada através de um programa, ou conjunto de programas e ferramentas, formando um "ambiente", onde será feito o processo de compilação, interpretação e/ou execução. Estas instruções irão utilizar recursos do sistema operacional que, por sua vez, irá manipular recursos de hardware, como CPU, memória e I/O.
Com isso conseguimos entender porque é tão importante a primeira parte deste guia, amém?
Compilador é um programa que, dado um input que é código-fonte que segue a especificação de uma linguagem, aplica transformações e gera um executável que contém código de máquina da arquitetura do computador ou um código intermediário.
Exemplos de linguagens que utilizam compiladores: C (GCC, Clang), Java (javac), Kotlin (kotlinc), Go (gc), Rust (rustc)
É possível haver mais de uma implementação para cada linguagem de programação. Vai depender da adesão, investimentos feitos entre outros aspectos.
Interpretador é um programa que, dado um input que é código-fonte que segue a especificação de uma linguagem, aplica transformações e executa as instruções da arquitetura "em tempo real".
Exemplos de linguagens que utilizam interpretadores: Python (CPython), Ruby (CRuby), PHP (Zend), Javascript (V8), Java (JVM)
Assim como no caso dos compiladores, é possível haver mais de uma implementação de interpretador para cada linguagem:
Repare na principal diferença entre interpretador e compilador: o compilador gera um executável, enquanto que o interpretador vai traduzindo e executando diretamente. Há vantagens e desvantagens em ambos os casos, inclusive há diversos ambientes que fazem um misto de processo de compilação com um interpretador embutido.
Por exemplo, em Java, o compilador javac
não gera um código de máquina da arquitetura diretamente, mas sim um "bytecode" intermediário. Então, o bytecode é executado por um interpretador que vem dentro do ambiente Java. A este ambiente de execução (runtime), que é capaz de interpretar os bytecodes gerados pelo javac
e traduzir para instruções da arquitetura do computador, chamamos de Java Virtual Machine, ou simplesmente JVM.
Como vimos na primeira parte do guia, o sistema operacional oferece recursos (chamadas de sistema, syscalls) para criar processos, threads e gerenciar recursos de I/O:
criação de processos e threads por meio da syscall clone
gerenciar recursos de I/O utilizando as syscalls read, write, open etc
gerenciar recursos de I/O assíncrono com as syscalls select, epoll, io_uring etc
Ou seja, as implementações de linguagens de alto nível precisariam fazer estas chamadas. E, pra nossa sorte, praticamente todas as linguagens mainstream (de propósito geral) fornecem formas de fazer estas chamadas ao sistema operacional.
Seria muito estranho uma linguagem não permitir a chamada de syscalls, né? Pra quê serviria esta linguagem então, se não fosse pra chamar syscalls no SO com o intuito de manipular recursos de hardware?
Algumas implementações acabam por ter um conjuto de compilador com interpretador, como vimos no exemplo do Java. E não só, podem também trazer técnicas de alto nível para criação de "user threads" (que são threads que vivem apenas dentro do ambiente, e não threads do SO), garantindo a troca de contexto entre elas e sua execução, tudo dentro do ambiente.
Geralmente, estes ambientes se chamam runtimes. Poderíamos tecnicamente chamar tudo de runtime, mas em alguns casos isto não é precisamente correto, como no caso do GCC, que é apenas um conjunto de ferramentas para compilação de código C.
Entretanto, podemos acordar, neste guia, que iremos tratar tudo por "runtime" (ambientes de execução), não importa se é apenas um compilador, um interpretador, um híbrido, se implementa user threads ou não, enfim. Vou chamar tudo de runtime simplesmente.
Me perdoem, acadêmicos de plantão. Não é hoje que vocês vão me cancelar
Aqui vai uma lista das principais implementações de algumas linguagens que vamos abordar ao longo deste guia:
C (GCC)
Java JDK (javac + JVM)
Python (CPython)
Ruby (CRuby, ou MRI)
Javascript (V8, usado no Chrome e também no Node.js)
PHP (Zend)
Go (gc)
Rust (rustc)
Kotlin (kotlinc + JVM)
Elixir (BEAM)
Acho que com estas linguagens dá pra cobrir diversas funcionalidades de concorrência. Iremos entender as principais diferenças, casos de uso e suas vantagens/desvantagens. Vamos fazer uma viagem longa através dos processos, criação de threads no SO, criação de threads dentro do runtime, manipulação de I/O assíncrono entre outras coisas interessantes.
Aperte os cintos, porque agora sim, vamos começar a ver o negócio na prática!