select en Python: monitorea varios I/O

Publicado el: 24/08/2026
Tempo de leitura: 6 minutos
Vibrant green tree python elegantly coiled on branch, showcasing its natural beauty.

El módulo select permite esperar hasta que sockets, pipes y otros descriptores estén listos para lectura, escritura o condiciones excepcionales. Esta técnica, llamada multiplexación de I/O, permite que un solo thread coordine muchas conexiones sin bloquearse en una sola.

Las primitivas cambian por sistema operativo: select() está ampliamente disponible, poll() aparece en Unix, epoll() es específico de Linux, kqueue() pertenece a BSD y macOS y /dev/poll existe en Solaris. Para aplicaciones portables, normalmente se prefiere selectors, que elige una implementación eficiente.

Disponibilidad no significa finalización

Un socket legible tiene alguna condición: datos, EOF o error. No garantiza que haya llegado un mensaje completo. Un socket escribible puede aceptar alguna salida, pero quizá solo una parte del buffer pendiente.

Los protocolos necesitan buffers de entrada y salida. Nunca supongas que un recv() devuelve un mensaje entero o que un send() transmite todos los bytes.

Servidor básico con select()

import select
import socket

servidor = socket.socket()
servidor.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
servidor.bind(("127.0.0.1", 9000))
servidor.listen()
servidor.setblocking(False)

lecturas = [servidor]

while True:
    listos, _, excepciones = select.select(lecturas, [], lecturas, 1.0)

    for sock in listos:
        if sock is servidor:
            cliente, direccion = servidor.accept()
            cliente.setblocking(False)
            lecturas.append(cliente)
        else:
            datos = sock.recv(4096)
            if datos:
                print(datos)
            else:
                lecturas.remove(sock)
                sock.close()

    for sock in excepciones:
        if sock in lecturas:
            lecturas.remove(sock)
        sock.close()

Es un ejemplo didáctico. Un servidor real necesita framing, buffers limitados, colas de salida, autenticación, timeouts y manejo robusto de excepciones. Consulta socketserver en Python para una abstracción de mayor nivel.

Las tres listas

La primera lista contiene objetos esperados para lectura. La segunda, objetos esperados para escritura. La tercera representa condiciones excepcionales definidas por la plataforma. Cada entrada puede ser un file descriptor entero o un objeto con fileno().

El resultado contiene tres subconjuntos listos. Un timeout None espera sin límite; cero consulta sin bloquear; un float positivo limita la espera en segundos.

Diferencias en Windows

En Windows, select() funciona solo con sockets de WinSock. Archivos normales, stdin y pipes ordinarios no se monitorean como en Unix. Programas portables pueden necesitar threads, asyncio o APIs específicas para fuentes que no sean sockets.

El módulo no está disponible en WASI.

Por qué selectors suele ser mejor

selectors.DefaultSelector elige epoll, kqueue, poll o select según la plataforma. Su modelo permite asociar datos a cada registro y oculta diferencias de bitmasks.

Usa select directamente cuando necesites flags del kernel, edge-trigger, eventos de procesos o archivos de kqueue o integración con código de bajo nivel existente.

Lecturas parciales y framing

TCP proporciona un flujo de bytes, no límites de mensajes. La aplicación puede definir delimitador, longitud prefijada, tamaño fijo o formato estructurado.

buffers = {}

def recibir(sock):
    bloque = sock.recv(4096)
    if not bloque:
        return False

    buffer = buffers.setdefault(sock, bytearray())
    buffer.extend(bloque)

    while b"\n" in buffer:
        linea, _, restante = buffer.partition(b"\n")
        buffer[:] = restante
        procesar_linea(linea)
    return True

Define un tamaño máximo. Un cliente que nunca envía delimitador no debe consumir memoria indefinidamente.

Escrituras parciales y backpressure

Monitorea escritura solo mientras exista salida pendiente. Los sockets conectados suelen estar escribibles, así que registrarlos permanentemente puede generar busy loop.

salidas = {}

def enviar_pendiente(sock):
    buffer = salidas.get(sock)
    if not buffer:
        return False

    enviados = sock.send(buffer)
    del buffer[:enviados]
    return bool(buffer)

Limita cada cola. Si un cliente lee lentamente, pausa productores, rechaza más datos, descarta mensajes según política o cierra la conexión. Ese control evita que un cliente agote la memoria.

EOF, half-close y errores

recv() devolviendo b"" indica que el peer cerró su lado de escritura. La aplicación quizá todavía tenga salida pendiente. poll() y epoll() exponen flags como POLLHUP, EPOLLHUP y EPOLLRDHUP.

Procesa errores y hangups aunque también exista lectura. Puede haber datos finales antes del cierre.

poll()

poll() registra descriptores con máscaras como POLLIN, POLLOUT, POLLERR, POLLHUP y POLLNVAL.

import select

poller = select.poll()
poller.register(sock, select.POLLIN)

for fd, evento in poller.poll(1000):
    if evento & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
        cerrar_fd(fd)
    elif evento & select.POLLIN:
        leer_fd(fd)

El timeout está en milisegundos. Mantén un mapping entre números y objetos de conexión.

epoll en Linux

epoll() escala bien con muchos descriptores y admite level-triggered y edge-triggered. En level-triggered el evento continúa mientras la condición sea verdadera. Con EPOLLET, las notificaciones indican cambios de estado.

Edge-trigger exige sockets no bloqueantes y leer o escribir hasta EAGAIN o EWOULDBLOCK. Parar antes puede dejar datos sin una nueva notificación.

import errno
import select

with select.epoll() as ep:
    ep.register(sock.fileno(), select.EPOLLIN | select.EPOLLET)
    for fd, evento in ep.poll(1.0):
        if evento & select.EPOLLIN:
            while True:
                try:
                    bloque = descriptores[fd].recv(65536)
                except BlockingIOError as exc:
                    if exc.errno in {errno.EAGAIN, errno.EWOULDBLOCK}:
                        break
                    raise
                if not bloque:
                    cerrar_fd(fd)
                    break
                procesar(bloque)

errno en Python explica esos códigos no bloqueantes.

EPOLLONESHOT y EPOLLEXCLUSIVE

EPOLLONESHOT desactiva un descriptor después de un evento hasta rearmarlo con modify(). Puede evitar que varios workers procesen la misma conexión. EPOLLEXCLUSIVE reduce el thundering herd cuando varios epoll esperan el mismo descriptor.

Son opciones específicas de Linux que requieren tests con los kernels usados.

kqueue

kqueue() monitorea I/O, procesos, señales, timers y eventos de vnode. Los cambios se representan mediante kevent. Flags como KQ_EV_ADD, KQ_EV_DELETE, KQ_EV_ONESHOT y KQ_EV_CLEAR controlan registros.

BSD y macOS pueden informar rename, delete, write y cambios de atributos en archivos. Ese mecanismo no es portable a Linux o Windows.

Integración con señales

El artículo anterior, signal en Python, presentó set_wakeup_fd(). Un pipe o socketpair no bloqueante puede despertar al selector cuando llega SIGTERM o SIGINT.

import os
import select
import signal

rfd, wfd = os.pipe()
os.set_blocking(rfd, False)
os.set_blocking(wfd, False)
signal.set_wakeup_fd(wfd)
signal.signal(signal.SIGTERM, lambda signum, frame: None)

while True:
    listos, _, _ = select.select([rfd, servidor], [], [], None)
    if rfd in listos:
        os.read(rfd, 4096)
        break

Drena el descriptor, restaura la configuración anterior y cierra ambos extremos. El handler permanece mínimo y el loop normal realiza el shutdown.

Timeouts y reloj monotónico

Los timeouts sirven para expiración, mantenimiento y deadlines. Calcula plazos con reloj monotónico.

import time

deadline = time.monotonic() + 30
while True:
    restante = max(0, deadline - time.monotonic())
    listos, _, _ = select.select(lecturas, [], [], restante)
    if time.monotonic() >= deadline:
        expirar_conexiones()

Descriptores cerrados y reutilizados

El sistema puede reutilizar números de descriptor. Desregistra antes de cerrar y elimina todo estado asociado. Un evento atrasado no debe aplicarse a una conexión nueva con el mismo número.

En epoll, unregister() puede mostrar EBADF después de un cierre prematuro. Centraliza el orden de limpieza y hazlo idempotente.

PIPE_BUF

En Unix, select.PIPE_BUF indica el mínimo que POSIX garantiza escribir atómicamente en un pipe marcado como escribible. No aplica a sockets ni garantiza tamaños arbitrarios.

Fairness

Un descriptor siempre activo puede monopolizar el loop. Limita bytes o mensajes procesados por evento y vuelve al multiplexor.

Mueve trabajo pesado de CPU a workers, procesos o executor. El thread de I/O debe mantenerse responsivo.

Seguridad y límites

Limita conexiones, bytes por cliente, tamaño de frame, cola de salida, tiempo ocioso, tasa y CPU. Multiplexar miles de sockets no crea memoria ilimitada.

TLS no bloqueante requiere cuidado: handshake y operaciones SSL pueden necesitar alternar lectura y escritura. Consulta ssl en Python.

Pruebas recomendadas

Prueba entrada fragmentada, salida parcial, cliente lento, cierre durante envío, half-close, timeout, señales durante espera, descriptor inválido, muchos sockets, backpressure, edge-trigger no drenado y diferencias Windows/Unix.

Usa socket.socketpair() y buffers pequeños para forzar operaciones parciales. Verifica que no haya busy loop ni crecimiento ilimitado.

Errores comunes

Los fallos frecuentes son tratar disponibilidad como mensaje completo, monitorear escritura siempre, ignorar envíos parciales, permitir buffers ilimitados, usar edge-trigger sin drenar hasta EAGAIN, cerrar antes de desregistrar, asumir archivos en Windows y ejecutar CPU pesada en el loop.

Conclusión

select da acceso directo a la multiplexación del sistema. Puede coordinar muchos descriptores con pocos threads si la aplicación gestiona buffers, backpressure, cierre y diferencias de plataforma.

Prefiere selectors para portabilidad, usa primitivas específicas solo cuando sean necesarias y mantén I/O no bloqueante. Consulta la documentación oficial de select y la documentación oficial de selectors.

Compartilhe:

Facebook
WhatsApp
Twitter
LinkedIn

Contenido del artículo

    Artículos relacionados

    Desarrollador configurando un servidor HTTPS y certificado TLS con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    HTTPSServer: crea un servidor HTTPS local en Python

    Aprende HTTPSServer en Python para crear servicios HTTPS locales, configurar certificados, usar hilos y comprender sus límites.

    Ler mais

    Tempo de leitura: 4 minutos
    08/10/2026
    Archivos protegidos que representan extracción segura de TAR con Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    tarfile extraction_filter: extrae TAR con seguridad

    Aprende tarfile extraction_filter en Python para extraer archivos TAR con validación, seguridad y control de rutas.

    Ler mais

    Tempo de leitura: 6 minutos
    08/10/2026
    Código Python mostrando avisos controlados con catch_warnings
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    catch_warnings: captura warnings en pruebas Python

    Aprende catch_warnings en Python para capturar, probar y controlar avisos con filtros específicos y alcance seguro.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código Python que representa referencias persistentes de pickle
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    pickle persistent_id: serializa referencias externas

    Aprende pickle persistent_id en Python para referencias externas estables, validación, seguridad, rendimiento y compatibilidad.

    Ler mais

    Tempo de leitura: 5 minutos
    07/10/2026
    Código binario que representa buffers y vistas de memoria en Python
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    memoryview.count: cuenta valores sin copiar buffers

    Aprende memoryview.count en Python para contar bytes y valores en buffers sin copias, con formatos, límites y buenas prácticas.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026
    Código Python y anotaciones de tipos
    Python Avanzado
    Foto de perfil de Leandro Hirt da Academify

    annotationlib: evita imports circulares en anotaciones

    Aprende annotationlib en Python para inspeccionar anotaciones diferidas, evitar imports circulares y crear herramientas seguras.

    Ler mais

    Tempo de leitura: 6 minutos
    06/10/2026