El problema de la concurrencia a escala tiene dos soluciones conocidas y un problema con cada una. Los OS threads preservan la legibilidad del código —secuencial, top-down, fácil de razonar— pero son caros: 256 KB a 8 MB de stack por hilo, cambios de contexto que pasan por el kernel, y un techo real alrededor de los 10.000 conexiones concurrentes antes de la degradación. El modelo async/await escala bien pero invierte el control de flujo: el código deja de leerse como una secuencia lógica y se convierte en una máquina de estados, los backtraces dejan de mostrar la llamada original, y manejar errores se vuelve un ejercicio de imaginación.
Go popularizó una tercera salida con las goroutines: user-space threads cooperativos, multiplexados sobre un pool de OS threads, con cambios de contexto en espacio de usuario. El mismo modelo existe ahora en desarrollo activo para SBCL, el compilador de Common Lisp de referencia. El proyecto se llama SBCL Fibers y está disponible en la rama fibers-v2 del fork de Anthony Green en GitHub. Aún es un work-in-progress —el propio documento técnico lo declara así— pero la arquitectura ya está lo suficientemente detallada para entender qué resuelve y cómo.
¿Por qué Lisp necesita fibers ahora?
Common Lisp tiene más estado implícito por hilo que la mayoría de lenguajes: los special variables viven en un binding stack separado, los unwind-protect y los catch mantienen cadenas propias, y todo eso complica implementar green threads correctamente. Una implementación que solo conmute el control stack corrupta el estado silenciosamente.
Claude Desbloqueado
Mi curso avanzado para aprender a sacarle mucho más provecho a Claude en el trabajo y en el día a día, con funciones y usos más potentes. Comienza el 23 de marzo.
→ Inscríbete hoy 🚀SBCL Fibers resuelve esto con una arquitectura M:N completa: muchas fibers sobre un pool pequeño de OS threads (los carrier threads), con un scheduler por carrier y work-stealing entre ellos usando Chase-Lev deques lock-free. El punto delicado —la integración con el GC— se maneja con un diseño de dos listas que mantiene visibles los stacks de todas las fibers suspendidas sin necesidad de detener el mundo en momentos inconvenientes.
Cómo funciona el context switch
El corazón del sistema es fiber_switch, una rutina de assembler que salva únicamente los registros callee-saved del ABI de la plataforma: 6 registros (48 bytes) en x86-64 SysV, contra el costo completo de un cambio de contexto de kernel que incluye registros de segmento, estado del FPU y manejo de señales. La rutina escribe el RSP actual a un slot del struct de la fiber, carga el RSP del destino, restaura registros y ejecuta ret —que “aterriza” en el punto exacto donde el receptor cedió el control.
Cada fiber recibe un control stack de 256 KB por defecto (con una guard page para detectar overflows) y un binding stack de 16 KB. Los stacks se poolizan con madvise(MADV_DONTNEED): cuando una fiber muere, el kernel libera las páginas físicas pero mantiene el mapeo virtual, de modo que la siguiente fiber toma un stack “limpio” sin necesitar un mmap nuevo.
Para I/O, el scheduler integra epoll en Linux, kqueue en BSD y un fallback de poll en otras plataformas. Cuando una fiber espera en un descriptor de archivo, el scheduler registra el fd y la fiber queda suspendida; el idle hook bloquea el carrier con epoll_wait hasta que el fd esté listo, luego reencola la fiber. El código que llama a sleep, grab-mutex o wait-until-fd-usable detecta automáticamente si está dentro de una fiber y cede cooperativamente en vez de bloquear el carrier thread.
¿Cuándo tiene sentido?
Las fibers brillan exactamente en el mismo escenario que las goroutines: workloads I/O-bound con muchas conexiones concurrentes y poco cómputo por request. Un servidor web, un proxy, un gateway de APIs. Si el cuello de botella es CPU, la recomendación del propio diseño es combinar fibers con lparallel para paralelismo real —las fibers son cooperativas, no preemptivas, y una fiber en loop de cómputo intensivo bloquea su carrier thread.
Hay limitaciones concretas a tener en cuenta. El soporte en Windows es experimental. Las librerías que hacen syscalls bloqueantes directamente anulan la ventaja de las fibers (es necesario usar wrappers no bloqueantes o delegar a un thread pool separado). Y el debugging es más complejo: print-fiber-backtrace permite inspeccionar el stack de una fiber suspendida, pero solo en ese estado.
La integración con Hunchentoot —el servidor HTTP más usado en el ecosistema Common Lisp— se resuelve con un Fiber Taskmaster que lanza una fiber por request, eliminando el overhead de crear un OS thread por conexión.
Por qué importa
SBCL Fibers no inventa nada que Go, Erlang o los green threads de Java no hayan hecho antes. El mérito está en otro lado: demostrar que Common Lisp puede ofrecer el mismo modelo de concurrencia sin sacrificar las propiedades del lenguaje —el sistema de condiciones, el recolector de basura, los special variables, la expresividad del REPL.
Para el ecosistema Lisp, que tiene una base de código legacy enorme en infraestructura financiera y sistemas científicos, una primitiva de concurrencia moderna abre la posibilidad de escalar workloads existentes sin reescribirlos en Go o Rust. Para el resto, SBCL Fibers es un caso de estudio impecable de cómo diseñar concurrencia cooperativa cuando el lenguaje tiene estado implícito complejo —algo que proyectos similares en Python (greenlet) o Ruby (Fiber) han resuelto con trade-offs más agresivos.
El proyecto está en desarrollo activo. Si trabajás con SBCL o simplemente te interesa el diseño de runtimes, el documento técnico de Anthony Green es una de las explicaciones más detalladas disponibles sobre cómo implementar green threads en un lenguaje con GC compactador. En un ecosistema donde el mantenimiento de proyectos open source está bajo presión creciente, que una implementación de esta complejidad esté documentada con este nivel de detalle es, en sí mismo, un dato relevante.

