> For the complete documentation index, see [llms.txt](https://concorrencia101.leandronsp.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://concorrencia101.leandronsp.com/parte-ii-concorrencia-em-diferentes-linguagens/concorrencia-em-c/trabalhando-com-i-o.md).

# Trabalhando com I/O

Relembrando o que vimos na primeira parte do guia, uma operação de I/O pode bloquear a execução de uma thread no sistema operacional. Sendo a *thread principal*, neste caso o **programa todo fica bloqueado**.

Para ilustrar como algumas operações de I/O são por natureza  bloqueantes, vamos demonstrar a comunicação entre dois processos utilizando **UNIX named pipes**, ou *FIFO* (veja mais sobre arquivos *FIFO* [no meu artigo](https://dev.to/leandronsp/implementando-um-simples-background-job-com-unix-named-pipes-3eja)).

## Comunicação com FIFO

O nome técnico para este tipo de comunicação é **UNIX named pipes,** ou *pipes nomeados*, cuja funcionalidade é prover uma estrutura de *fila* (daí o nome FIFO) onde:

* um processo escreve mensagens na fila
* outro processo lê mensagens na fila

A primeira coisa que temos que fazer é criar um arquivo especial do tipo "pipe" com o comando `mkfifo` :&#x20;

```bash
$ mkfifo queue

$ ls -l queue
prw-r--r-- 1 leandronsp leandronsp 0 Dec 31 23:59 queue
```

A saída começando com "p" indica justamente que este arquivo é especial, ou seja, um pipe FIFO.&#x20;

### Reader

Vamos representar em C o **leitor** da fila:

{% code title="fifo-reader.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    int fd = open("queue", O_RDONLY); // Abre o arquivo em modo somente leitura
    read(fd, buffer, BUFFER_SIZE); // Tentativa de leitura do arquivo
    printf("Mensagem recebida: %s", buffer);

    close(fd);
    return 0;
}
```

{% endcode %}

Atenção para alguns detalhes:

* o header **fcntl** inclui funções como `open` , que adiciona um determinado arquivo no filesystem na tabela de descritores de arquivos, a *file descriptor table*, e retorna o inteiro representando o **fd**&#x20;
* o header **unistd** inclui funções como `read` e `close` , que são mais genéricas para leitura e fechamento de descritores de arquivos&#x20;

Ao executar o programa, note que ele fica **bloqueado** a espera que alguém escreva na fila.

### Writer

Em *outra janela do terminal*, podemos escrever no FIFO utilizando o comando `echo` : &#x20;

```
$ echo Hello! > queue
```

Veja na janela do *writer* que a mensagem apareceu na saída padrão! *Superb!*&#x20;

Outra característica importante pra notar aqui: o writer também fica bloqueado de escrever, caso não haja nenhum reader disponível.

> Isto é o puro suco do I/O bloqueante

## Limitações do I/O bloqueante

Neste exemplo com FIFO, a operação de abertura do arquivo com `open` é **bloqueante.** Enquanto não chega mensagem no arquivo, o **programa fica bloqueado** nesta operação pois o I/O ainda *não está pronto***.**&#x20;

<figure><img src="/files/iVncKNmzGNZg4EnazJHu" alt=""><figcaption></figcaption></figure>

E é isto que define uma operação *bloqueante:* **quando o I/O ainda não está pronto***,* a nível de estruturas internas, dados disponíveis e buffers no sistema operacional.

Agora vamos imaginar um cenário onde precisamos ler do FIFO, depois ler do STDIN, ou mesmo ler de um socket *TCP,* e mais um **monte de coisas** no programa.

Bloquear o programa inteiro ou a thread inteira por conta de uma operação faz aumentar bastante a latência total do sistema, o que contribui para um throughput final bem baixo. Em outras palavras, nosso sistema não escala adequadamente consoante ao número de operações necessárias pra fazer em I/O.

<figure><img src="/files/lriiTqqw3p7wyjJPQR1e" alt="" width="270"><figcaption></figcaption></figure>

## Vai uma thread aí?

Não é incomum a utilização de threads para paralelizar operações em *I/O bloqueante* o que até faz sentido em alguns casos. Mas quando falamos de *milhares de operações*, podemos esbarrar nas limitações de threads, como já vimos diversas vezes neste guia.

> Sem problemas, Leandro. Já sabemos que dá pra usar pool de threads, certo?

*Certo*?

Mais ou menos. Embora seja uma solução que funcione para *muitos casos*, a pool pode limitar a performance caso o sistema exija um número massivo de operações em I/O, pois ela trabalha com um número fixo de threads, lembra?

> Pensando aqui, seria muito bacana se o sistema operacional permitisse que a leitura de um arquivo fosse "não-bloqueante", de forma que depois ele "avisasse" o programa que o arquivo já ficou pronto para leitura ou algo assim?

Esta técnica existe no sistema operacional, e é chamada de **I/O não-bloqueante**.

## I/O não-bloqueante em C

Como vimos no tópico anterior, a função `open` abre o descritor em modo bloqueante por padrão:

```c
int fd = open("queue", O_RDONLY); // Bloqueante
read(fd, buffer, BUFFER_SIZE); // Bloqueia até haver dados no FIFO
```

Mas é possível passar também uma flag chamada `O_NONBLOCK` , que vai abrir o descritor em modo **não-bloqueante**, e isto serve pra qualquer tipo de descritor, seja um arquivo comum, socket, pipe (FIFO), etc.

<pre class="language-c"><code class="lang-c">int fd = open("queue", O_RDONLY | O_NONBLOCK); // Não-bloqueante
<strong>ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE); // Não bloqueia a leitura
</strong></code></pre>

Desta forma, a operação de I/O não impede o programa de continuar executando caso os dados não estejam imediatamente disponíveis. Mas aqui tem o *pulo do gato*...

Como a leitura não é bloqueante, a função retorna imediatamente. E o retorno pode ter dois valores possíveis:

* os bytes a serem lidos, caso o I/O já esteja pronto
* `-1` , caso o I/O não esteja pronto. Sendo assim, para além do valor ser **-1**, a variável global **errno** também é configurada com o valor `EAGAIN` ou `EWOULDBLOCK` , que indicam que o I/O não está pronto e que é preciso fazer "polling" no descritor para saber quando fica pronto

```c
int fd = open("queue", O_RDONLY | O_NONBLOCK); // Não-bloqueante
ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);

if (bytes_read < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
    printf("Nenhum dado disponível no FIFO no momento. Tentar novamente em instantes");
}
```

E atenção para a frase "tentar novamente em instantes". O que queremos aqui é *ficar em loop*, verificando (polling) quando que de fato há bytes para serem lidos:

{% code title="fifo-nb.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    int fd = open("queue", O_RDONLY | O_NONBLOCK); // Abre o arquivo em modo não-bloqueante
						   //
    while (1) { 
	ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);

	if (bytes_read > 0) {
	    printf("Mensagem recebida: %s", buffer);
	} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
	    printf("Nenhum dado disponível no FIFO agora.\n");
	}

	sleep(1); // Espera 1 segundo
    }

    close(fd);
    return 0;
}
```

{% endcode %}

Dentro do loop, fazemos a leitura do descritor, e caso não esteja disponível, avisamos que não há nada na fila e a seguir é provocado um **sleep** de 1 segundo para não sobrecarregar a CPU com muitas voltas redundantes no loop.

```
Mensagem recebida: Hello
Mensagem recebida: World
```

*Yay!*

Contudo, esta solução ainda é bastante rudimentar e ineficiente para monitorarmos descritores de arquivos. Felizmente, o sistema operacional fornece algumas *syscalls* para resolver este problema de forma mais elegante, permitindo **monitorar os descritores** que estão sendo processados pelo sistema operacional.

## Monitorando I/O com select

A **syscall select** permite monitorar múltiplos descritores de arquivo simultaneamente, sem que o programa precise verificar continuamente cada um deles de forma redundante (como no exemplo anterior). Isso melhora a eficiência, reduz o consumo de CPU e permite que o programa reaja apenas quando houver atividade em um ou mais descritores monitorados.

Para utilizar o **select**, precisamos primeiro definir o conjunto de descritores (**fd\_set**) a serem monitorados, através das macros que estão na biblioteca *sys/select.h:*

* **FD\_ZERO:** para inicializar o conjunto
* **FD\_SET**: adiciona um descritor ao conjunto
* **FD\_ISSET**: verifica se um descritor está pronto para leitura/escrita

{% code title="io-select.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/select.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    // Abre o FIFO em modo não-bloqueante
    int fd = open("queue", O_RDONLY | O_NONBLOCK);
    printf("Monitorando FIFO...\n");

    while (1) {
        fd_set read_fds;
        FD_ZERO(&read_fds);

        // Adiciona o FIFO aos conjuntos de leitura
        FD_SET(fd, &read_fds);

        // Configura timeout opcional (1 segundo)
        struct timeval timeout;
        timeout.tv_sec = 1;
        timeout.tv_usec = 0;

        // Chama select para monitorar os descritores
        int ready = select(fd + 1, &read_fds, NULL, NULL, &timeout);
        if (ready == 0) {
            // Timeout: Nenhuma atividade
            printf("Nenhum descritor disponível. Continuando...\n");
            continue;
        }

        // Verifica se há dados no FIFO, ou seja se está pronto para leitura
        if (FD_ISSET(fd, &read_fds)) {
            ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);

            if (bytes_read > 0) {
                printf("Mensagem recebida: %s", buffer);
            } 
        }
    }

    close(fd);
    return 0;
}

```

{% endcode %}

```
Monitorando FIFO...
Nenhum descritor disponível. Continuando...
Nenhum descritor disponível. Continuando...
Mensagem recebida: hello!
Mensagem recebida: hello!
```

*OMG!* Que dia incrível!

O *select* traz a vantagem que permite monitorar múltiplos descritores de forma simples, pelo que também funciona em diversos sistemas operacionais.

Entretanto ele possui algumas limitações:

* tempo de busca é linear, o que pode trazer problemas de performance para um grande número de descritores
* possui limitação no número máximo de descritores

## Monitorando I/O com epoll

**epoll** é uma interface mais eficiente que o *select* para monitorar um grande número de descritores, e foi introduzida no Linux 2.6.&#x20;

Em vez de retornar todos os descritores para fazer uma busca linear nos que estão prontos, *epoll* retorna **apenas os que estão de fato prontos** para leitura/escrita, diminuindo bastante o overhead para um número muito grande de descritores.

Outra vantagem com relação ao *select* é que não há um limite de descritores, pois ele mantém um número dinâmico de descritores dentro do Kernel.

A mecânica de uso é muito parecida com o select:

{% code title="epoll.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>

#define BUFFER_SIZE 1024
#define MAX_EVENTS 10

int main() {
    char buffer[BUFFER_SIZE];

    // Abre o FIFO em modo não-bloqueante
    int fd = open("queue", O_RDONLY | O_NONBLOCK);

    // Inicializa o epoll (similar ao FD_ZERO do select)
    int epoll_fd = epoll_create1(0);

    // Configura o descritor no epoll
    struct epoll_event event;
    event.events = EPOLLIN; // Monitorar para leitura
    event.data.fd = fd;
    // Adiciona o descritor ao epoll (similar ao FD_SET)
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event);

    printf("Monitorando FIFO com epoll...\n");

    struct epoll_event events[MAX_EVENTS];

    while (1) {
        // Aguarda pelos descritores prontos com timeout de 1 segundo
        int ready = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);

        if (ready == 0) {
            printf("Nenhum descritor disponível. Continuando...\n");
            continue;
        }

        // Itera pelos descritores prontos e realiza as devidas leituras
        for (int i = 0; i < ready; i++) {
            if (events[i].events & EPOLLIN) {
                ssize_t bytes_read = read(events[i].data.fd, buffer, BUFFER_SIZE);

                if (bytes_read > 0) {
                    printf("Mensagem recebida: %s\n", buffer);
                }
            }
        }
    }

    close(fd);
    close(epoll_fd);
    return 0;
}
```

{% endcode %}

A utilização de **epoll** apresenta uma performance significativamente superior ao **select** quando lidamos com um grande número de descritores de arquivo. Isso o torna uma escolha mais adequada para sistemas modernos e de alta escala.

***

Até agora, exploramos os recursos que o sistema operacional oferece para realizar operações de I/O de forma *não-bloqueante*. Essa característica muda fundamentalmente a maneira como escrevemos código, afastando-nos da abordagem tradicional síncrona para adotar uma abordagem assíncrona.

Normalmente, escrevemos código de forma síncrona, onde as funções e rotinas são executadas em sequência e os dados necessários já estão disponíveis na memória. Com I/O não-bloqueante, perdemos essa certeza. Não sabemos quando os dados estarão prontos, o que nos obriga a estruturar o código para lidar com a disponibilidade futura das informações.

Essa mudança nos leva ao **assincronismo**, onde não esperamos pela conclusão de uma operação, mas configuramos o código para reagir quando a operação for concluída.

## I/O assíncrono

Muitas vezes, há confusão entre I/O assíncrono e I/O não-bloqueante, ou mesmo com o uso de threads. No entanto, as threads não têm relação direta com o assincronismo em I/O. Podemos usar threads para multiplexar operações bloqueantes ou não-bloqueantes, mas o conceito de I/O assíncrono está relacionado à **maneira como o código é estruturado** para lidar com operações não-bloqueantes.

Em resumo, se você optar por I/O não-bloqueante, estará implicitamente adotando um estilo de programação **assíncrono** para lidar com a natureza imprevisível das operações.

Vamos ilustrar isso comparando um código síncrono com outro assíncrono em C:

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>

#define BUFFER_SIZE 1024

int main() {
    char buffer[BUFFER_SIZE];

    // Abrindo um arquivo para leitura síncrona
    int fd = open("example.txt", O_RDONLY);

    // Lendo o conteúdo do arquivo
    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
    
    if (bytes_read > 0) {
        printf("Dados lidos: %s\n", buffer);
    } 

    close(fd);
    return 0;
}
```

O programa espera a leitura do arquivo ser concluída antes de continuar. A execução é **bloqueante**: o programa para no *read* até que a operação termine.

Já no código **assíncrono**, o programa continua a execução enquanto espera que a operação seja concluída. Quando os dados estão disponíveis, um **callback é acionado**. Um exemplo com o epoll:

```c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/epoll.h>

#define BUFFER_SIZE 1024
#define MAX_EVENTS 10

// Callback para tratar dados prontos para leitura
void on_data_available(int fd) {
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read = read(fd, buffer, BUFFER_SIZE);
    
    if (bytes_read > 0) {
        printf("Callback: Dados lidos: %s\n", buffer);
    } 
}

int main() {
    // Abre o arquivo em modo não-bloqueante
    int fd = open("example.txt", O_RDONLY | O_NONBLOCK);

    // Cria o epoll
    int epoll_fd = epoll_create1(0);

    // Configura o descritor no epoll
    struct epoll_event event;
    event.events = EPOLLIN; // Monitorar para leitura
    event.data.fd = fd;

    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, fd, &event)
    struct epoll_event events[MAX_EVENTS];
    
    // Loop de eventos do epoll
    while (1) {
        int ready = epoll_wait(epoll_fd, events, MAX_EVENTS, 1000);

        if (ready == 0) {
            printf("Nenhum dado disponível, continuando...\n");
            continue;
        }

        for (int i = 0; i < ready; i++) {
            if (events[i].events & EPOLLIN) {
                // Chama o callback para processar os dados
                on_data_available(events[i].data.fd);
            }
        }
    }

    close(fd);
    close(epoll_fd);
    return 0;
}
```

Vamos analisar passo-a-passo este exemplo assíncrono com epoll:

* criamos um **callback** `on_data_available` que representa a lógica quando o descritor estiver pronto (dados disponíveis)
* no código principal, temos:
  * a abertura do arquivo de forma **não-bloqueante**
  * configuração dos descritores com epoll
  * loop monitorando os eventos do epoll (loop de eventos)
    * dentro do loop, quando o dado fica disponível, chamamos o **callback** que foi registrado previamente

Isto, *senhoras e senhores*, é o puro extrato do assincronismo.

***

Agora vamos falar de uma limitação do *epoll*: só funciona no Linux. Outro desafio no uso do epoll também é que temos que configurar muita coisa manualmente e também temos de escrever nosso próprio "loop de eventos".

> E isso dá trabalho

## Libuv, o queridinho do NodeJS&#x20;

Não ganhamos na loteria mas estamos com muita sorte, pois a biblioteca [libuv](https://github.com/libuv/libuv) resolve estes e outros problemas:

* multi-plataforma (usa epoll no Linux, IOCP no Windows e kqueue no macOS)
* tem um loop de eventos muito bem feito
* traz suporte  uma pool de threads para algumas operações específicas de modo a aumentar o throughput
* permite tratamento de sinais
* entre otras cositas más...

Vamos a um exemplo de código assíncrono com libuv, a título de curiosidade:

{% code title="io-libuv.c" lineNumbers="true" %}

```c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <uv.h>

#define BUFFER_SIZE 1024

// Callback chamado quando a leitura é concluída
void on_data_available(uv_fs_t* req) {
    if (req->result > 0) {
        printf("Conteúdo do arquivo: %s\n", (char*)req->bufs->base);
    } 

    // Libera recursos
    uv_fs_req_cleanup(req);
    free(req->bufs->base);
    free(req);
}

int main() {
    // Inicializa o loop de eventos
    uv_loop_t* loop = uv_default_loop();

    uv_fs_t* open_req = malloc(sizeof(uv_fs_t));
    uv_fs_t* read_req = malloc(sizeof(uv_fs_t));

    // Abre o arquivo de forma assíncrona
    uv_fs_open(loop, open_req, "example.txt", O_RDONLY, 0, NULL);

    if (open_req->result >= 0) {
        char* buffer = malloc(BUFFER_SIZE);
        uv_buf_t iov = uv_buf_init(buffer, BUFFER_SIZE);

        // Lê o arquivo de forma assíncrona
        read_req->bufs = &iov;
        uv_fs_read(loop, read_req, open_req->result, &iov, 1, -1, on_data_available);
    }

    uv_fs_req_cleanup(open_req);
    free(open_req);

    uv_run(loop, UV_RUN_DEFAULT);
    return 0;
}
```

{% endcode %}

Repare como que o código fica mais simples, e não à toa o NodeJS utiliza o libuv como parte central de todo o assincronismo que o NodeJS fornece.

<figure><img src="/files/nSGn3XB7YreJT1k9A0Mz" alt="" width="563"><figcaption></figcaption></figure>

> Calma jovem, mais pra frente no guia vou dedicar uma seção só pra falar de concorrência em NodeJS

## Deixando seu código assíncrono, mas síncrono

Apesar do poder do assincronismo, a indireção que deixa no código dificulta muito a manutenção do sistema, porque lidamos com callbacks ou estruturas de eventos que fragmentam o fluxo lógico do programa. Callbacks e mais callbacks podem nos fazer entrar num cenário horrível na programação: o famigerado **callback hell**.

<figure><img src="/files/fJ5E1gilJNX5ylV97frA" alt=""><figcaption></figcaption></figure>

No entanto, podemos organizar o **código assíncrono de forma que pareça síncrono** aos olhos de quem está vendo o código, mesmo lidando com operações assíncronas internamente.

### I/O assíncrono e escalonamento cooperativo

Essa abordagem é frequentemente implementada usando **estruturas cooperativas** como *async/await, corrotinas ou geradores*. Elas pausam a execução no ponto de espera, liberando o controle do programa enquanto aguardam o evento (como dados de I/O) e retomam automaticamente quando o evento ocorre.

Usando algumas técnicas é possível fazer isto em C, mas não vamos aprofundar muito pois estas técnicas são implementadas de forma muito mais robusta em linguagens de mais alto nível que C.

***

E é isto que vamos cobrir nos tópicos a seguir, em como implementações de linguagens de alto nível lidam com concorrência, seja a nível de forking, threading, sincronização, abstrações em runtime e I/O assíncrono.

*Vamos continuar firmes*, a viagem ainda está no início :rocket:

## Está gostando deste trabalho?

Se está gostando e considera colaborar para que mais trabalhos assim sejam feitos, não deixe de entrar para a [lista de **investidores premium**](https://concorrencia101.leandronsp.com/agradecimentos). Qualquer apoio financeiro via PIX é mais que bem-vindo, inclusive compartilhar o guia é uma excelente forma de apoiar o projeto também!

<figure><img src="/files/FUUkoZLpU19gvljXPbr1" alt="" width="283"><figcaption></figcaption></figure>

Ou copia e cola:

```
00020126850014BR.GOV.BCB.PIX013638ee4bde-574b-4197-b10f-68742087b00b0223Gratidão pelo cafezinho5204000053039865802BR5925Leandro Freitas Maringolo6009SAO PAULO62140510qrN6Ov1wRl63041A3C
```

Obrigado :pray: E bora lá continuar na jornada!
