🧠FFVAcademy

I/O bloqueante, não-bloqueante, async: select/poll/epoll

17 min de leitura·+85 XP

O problema de servidores que precisam lidar com milhares de conexões simultâneas se resolve entendendo os modelos de I/O: bloqueante, não-bloqueante, e multiplexação com epoll. É o fundamento de asyncio, Node.js e Nginx.

Os modelos de I/O: evolução histórica

ModeloComo funcionaThroughputComplexidade
Bloqueanteread() espera até ter dadosBaixo (1 thread bloqueada)Simples
1 Thread/conexãoThread bloqueada por conexãoModerado (até ~1000)Moderada
select()Monitora N fds, max 1024ModeradoModerada
poll()Igual select, sem limite de 1024ModeradoModerada
epoll()Event-driven, escala O(1)Alto (10k+ conexões)Moderada
io_uringZero-copy async I/O realMuito altoAlta

Bloqueante vs não-bloqueante: a diferença fundamental

import socket
import os

# I/O BLOQUEANTE (padrão):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("example.com", 80))
sock.send(b"GET / HTTP/1.0

")
dados = sock.recv(4096)   # BLOQUEIA até receber dados — thread fica parada
sock.close()

# I/O NÃO-BLOQUEANTE:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(False)    # ou os.O_NONBLOCK
sock.connect_ex(("example.com", 80))   # connect_ex não lança exceção

try:
    dados = sock.recv(4096)   # retorna imediatamente com dados ou...
except BlockingIOError:        # ...levanta se não há dados ainda
    print("sem dados ainda, tente depois")

# Problema: como saber quando tentar de novo?
# Solução: epoll monitora quando o fd ficou pronto

epoll: multiplexação de I/O eficiente

import select
import socket

# select() — funciona em qualquer SO mas tem limitações:
# - max 1024 fds no Linux (FD_SETSIZE)
# - O(n) para verificar quais fds ficaram prontos
# - tem que repassar toda a lista a cada chamada

# epoll() — Linux, O(1), sem limite prático
# selectors.EpollSelector encapsula epoll para Python

import selectors

seletor = selectors.DefaultSelector()    # usa epoll no Linux, kqueue no macOS

def aceitar(sock, mascara):
    conn, addr = sock.accept()
    conn.setblocking(False)
    seletor.register(conn, selectors.EVENT_READ, ler)

def ler(conn, mascara):
    dados = conn.recv(1024)
    if dados:
        conn.send(dados)    # echo
    else:
        seletor.unregister(conn)
        conn.close()

servidor = socket.socket()
servidor.bind(("", 8080))
servidor.listen()
servidor.setblocking(False)
seletor.register(servidor, selectors.EVENT_READ, aceitar)

# Event loop simples (o que asyncio faz internamente):
while True:
    eventos = seletor.select(timeout=None)   # epoll_wait — bloqueia até evento
    for chave, mascara in eventos:
        callback = chave.data
        callback(chave.fileobj, mascara)     # chama aceitar() ou ler()

asyncio internamente: event loop + epoll

import asyncio

# asyncio abstrai o event loop + epoll em coroutines
async def handle_client(reader, writer):
    while True:
        data = await reader.read(1024)    # asyncio registra socket no epoll
        # Event loop faz epoll_wait() enquanto await está pendente
        # Outras coroutines rodam enquanto isso
        if not data:
            break
        writer.write(data)
        await writer.drain()
    writer.close()

async def main():
    server = await asyncio.start_server(handle_client, "0.0.0.0", 8080)
    async with server:
        await server.serve_forever()

asyncio.run(main())

# Como verificar que asyncio usa epoll:
import asyncio
loop = asyncio.new_event_loop()
print(type(loop._selector))   # <class 'selectors.EpollSelector'>

# Exemplo real com httpx async (10k requests simultâneos):
import asyncio, httpx

async def buscar(client, url):
    return await client.get(url)

async def main():
    urls = ["http://example.com"] * 100
    async with httpx.AsyncClient() as client:
        # 100 requests simultâneos — apenas 1 thread, 1 epoll
        respostas = await asyncio.gather(*[buscar(client, u) for u in urls])
    print(f"{len(respostas)} respostas recebidas")
Modelo mental: bloqueante = thread parada esperando. epoll = thread acordada só quando há trabalho. asyncio/Node.js = event loop em cima de epoll. Para I/O-bound com muitas conexões: async/event loop escala melhor que threads. io_uring é o futuro — zero-copy async I/O que o Tokio (Rust) e futuras versões do Python usarão amplamente.
💡
Próximo: Threads vs processos — modelos de concorrência no SO e como linguagens modernos os aproveitam.
🧩

Quiz rápido

3 perguntas · Acerte tudo e ganhe o badge 🎯 Gabarito

Continue lendo