Introducción: ¿por qué la concurrencia en C++ no da miedo, sino que es interesante?
\n\nHace 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\nEn 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\nLo clásico: multiprocesamiento con std::thread
\n\nEmpecemos 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.
Ejemplo sencillo: cálculo paralelo
\n\nImagina 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\nAquí 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
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\nLa revolución de C++20: corrutinas (gorutinas)
\n\nLas 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\nMuchos 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\nUna 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:
#include <iostream>\n#include <coroutine>\n#incl