⚡
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
| Modelo | Como funciona | Throughput | Complexidade |
|---|---|---|---|
| Bloqueante | read() espera até ter dados | Baixo (1 thread bloqueada) | Simples |
| 1 Thread/conexão | Thread bloqueada por conexão | Moderado (até ~1000) | Moderada |
| select() | Monitora N fds, max 1024 | Moderado | Moderada |
| poll() | Igual select, sem limite de 1024 | Moderado | Moderada |
| epoll() | Event-driven, escala O(1) | Alto (10k+ conexões) | Moderada |
| io_uring | Zero-copy async I/O real | Muito alto | Alta |
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 prontoepoll: 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