Concurrencia en C++: de hilos a corrutinas y async/await

Entrenador de Python en Línea para Principiantes

Aprende Python fácilmente sin sobrecargarte de teoría. Resuelve tareas prácticas con verificación automática, recibe pistas y escribe código directamente en el navegador, sin instalar nada.

Start Course

Introducción: ¿por qué la concurrencia en C++ no da miedo, sino que es interesante?

\n\n

Hace mucho tiempo, en la era de los procesadores de un solo núcleo, los programas se ejecutaban estrictamente de forma secuencial. Hoy en día, incluso tu teléfono inteligente tiene 8 núcleos y los servidores, decenas. Hacer que todos estos núcleos trabajen para ti: ahí es donde se esconde la verdadera magia del rendimiento. Y C++, uno de los lenguajes más potentes y de bajo nivel, nos brinda todo un arsenal de herramientas para ello. Pero, ¿cómo no confundirse? Hilos, corrutinas (a menudo llamadas «gorutinas» por analogía con Go), async/await: ¿qué elegir?

\n\n

En este artículo, desglosaremos la concurrencia moderna en C++. Aprenderás cuándo usar hilos clásicos y cuándo usar las ligeras corrutinas de C++20, y cómo async/await simplifica la programación asíncrona. ¡Allá vamos!

\n\n

Lo clásico: multiprocesamiento con std::thread

\n\n

Empecemos con lo más obvio: los hilos. std::thread es el acceso directo a los hilos del sistema operativo. Cada hilo es un contexto de ejecución independiente que puede funcionar en un núcleo de CPU separado.

\n\n

Ejemplo sencillo: cálculo paralelo

\n\n

Imagina que necesitas calcular la suma de los cuadrados de los números del 1 al 10 millones. Hacerlo secuencialmente es lento. Vamos a paralelizarlo:

\n\n
#include <iostream>\n#include <thread>\n#include <vector>\n#include <numeric>\n\nvoid sum_squares(int start, int end, long long& result) {\n    result = 0;\n    for (int i = start; i <= end; ++i) {\n        result += i * i;\n    }\n}\n\nint main() {\n    const int N = 10'000'000;\n    const int NUM_THREADS = 4;\n    std::vector<std::thread> threads;\n    std::vector<long long> results(NUM_THREADS, 0);\n    \n    int chunk = N / NUM_THREADS;\n    for (int i = 0; i < NUM_THREADS; ++i) {\n        int start = i * chunk + 1;\n        int end = (i == NUM_THREADS - 1) ? N : (i + 1) * chunk;\n        threads.emplace_back(sum_squares, start, end, std::ref(results[i]));\n    }\n    \n    for (auto& t : threads) {\n        t.join();\n    }\n    \n    long long total = std::accumulate(results.begin(), results.end(), 0LL);\n    std::cout << "Suma de cuadrados: " << total << std::endl;\n    return 0;\n}\n
\n\n

Aquí creamos 4 hilos, cada uno calcula su parte del rango. Los resultados se recogen en un vector. Todo es simple y predecible. Pero hay matices:

\n\n
    \n
  • Gastos generales: crear cada hilo es costoso (asignación de pila, llamadas al sistema). Para miles de tareas pequeñas, esto no es eficiente.
  • \n
  • Sincronización: si los hilos acceden a datos compartidos, se necesitan mutex, que pueden causar bloqueos y reducir el rendimiento.
  • \n
  • Cantidad limitada: el número de hilos generalmente no debe exceder la cantidad de núcleos de CPU (teniendo en cuenta el hyperthreading).
  • \n
\n\n

Para cálculos pesados y largos, los hilos son una excelente opción. Pero para operaciones de E/S (lectura de archivos, solicitudes de red), a menudo son excesivos. Aquí es donde entran en escena las corrutinas.

\n\n

La revolución de C++20: corrutinas (gorutinas)

\n\n

Las corrutinas en C++20 no son solo una característica más. Son un cambio fundamental en la forma en que escribimos código asíncrono. A diferencia de los hilos, las corrutinas son subrutinas ligeras que pueden suspender su ejecución y reanudarla más tarde, sin bloquear el hilo del SO.

\n\n

Muchos desarrolladores las llaman «gorutinas» debido a su similitud con las goroutines de Go, aunque técnicamente son conceptos diferentes. La principal ventaja: puedes ejecutar cientos de miles de corrutinas en un solo hilo sin pérdida de rendimiento.

\n\n

¿Cómo funciona?

\n\n

Una corrutina es una función que contiene la palabra clave co_await, co_return o co_yield. El compilador la transforma automáticamente en una máquina de estados. Aquí hay un ejemplo básico:

\n\n
#include <iostream>\n#include <coroutine>\n#incl                

Blogs

Book Recommendations