C++ Multihilo
Un hilo es una unidad de ejecución ligera dentro de un programa, que permite al programa ejecutar varias tareas al mismo tiempo.
El multihilo es una forma especial de multitarea; la multitarea permite que la computadora ejecute dos o más programas al mismo tiempo.
Por lo general, existen dos tipos de multitarea:Basada en procesos y basada en hilos.。
- La multitarea basada en procesos es la ejecución concurrente de programas.
- La multitarea basada en hilos es la ejecución concurrente de segmentos del mismo programa.

La programación multihilo en C++ implica crear y gestionar múltiples hilos que se ejecutan concurrentemente en un programa.
C++ proporciona un sólido soporte para multihilo, especialmente a partir del estándar C++11, mediante<thread>la biblioteca estándar, lo que hace que la programación multihilo sea más sencilla y segura.
Explicación de conceptos
Hilo (Thread)
- Un hilo es un flujo de control secuencial único en la ejecución de un programa; varios hilos pueden ejecutarse de forma independiente dentro del mismo proceso.
- Los hilos comparten recursos del proceso como el espacio de direcciones, los descriptores de archivo, el montón y las variables globales, pero cada hilo tiene su propia pila, registros y contador de programa.
Concurrencia (Concurrency) y paralelismo (Parallelism)
- Concurrencia: múltiples tareas se ejecutan alternadamente en intervalos de tiempo, dando la apariencia de que ocurren simultáneamente.
- Paralelismo: múltiples tareas se ejecutan simultáneamente en múltiples procesadores o núcleos de procesador.
Los estándares de C++11 y posteriores ofrecen soporte para multihilo; los componentes principales incluyen:
std::thread: se utiliza para crear y gestionar hilos.std::mutex: se utiliza para la exclusión mutua entre hilos, evitando que varios hilos accedan simultáneamente a recursos compartidos.std::lock_guardystd::unique_lock: se utiliza para gestionar la adquisición y liberación de bloqueos.std::condition_variable: se utiliza para variables de condición entre hilos, coordinando la espera y la notificación entre hilos.std::futureystd::promise: se utiliza para implementar el paso de valores y la sincronización de tareas entre hilos.
Crear hilos
Después de C++11, se añadió una nueva biblioteca estándar de hilos.std::thread,std::thread en <thread>se declara en el archivo de cabecera; por lo tanto, para usarstd::threades necesario incluiren <thread>archivo de cabecera.
Nota:Antes, algunos compiladores usaban el parámetro de compilación de C++11 era-std=c++11:
g++ -std=c++11 test.cpp
std::thread
Crear un hilo mediante un puntero a función es la forma más básica: Usandog++ -std=c++11después de compilar, el resultado de ejecución es: A través del Usandog++ -std=c++11después de compilar, el resultado de ejecución es: Las expresiones lambda pueden definir directamente el código de ejecución del hilo de forma inline: Usandog++ -std=c++11después de compilar, el resultado de ejecución es: join() se usa para esperar a que el hilo termine su ejecución. Si se destruye el objeto del hilo sin llamar a join() o detach(), el programa puede fallar. detach() separa el hilo del hilo principal; el hilo se ejecuta independientemente en segundo plano y el hilo principal ya no espera por él. Los parámetros pueden pasarse al hilo por valor: Si se necesita pasar parámetros por referencia, se debe usar std::ref: El siguiente es un ejemplo completo que muestra cómo crear hilos usando los tres métodos anteriores y realizar la gestión de hilos. Usando el parámetro de compilación de C++11-std=c++11: La salida del código anterior puede variar según la plataforma o cambiar en cada ejecución, porque el orden de ejecución de los hilos lo decide el algoritmo de planificación del sistema operativo; varios hilos se ejecutan de forma concurrente y la salida puede entremezclarse, por ejemplo: En la programación multihilo, la sincronización y la exclusión mutua son dos conceptos muy importantes. Se utilizan para controlar el acceso de múltiples hilos a los recursos compartidos, con el fin de evitar problemas como la condición de carrera y el punto muerto. El mutex es una primitiva de sincronización que se utiliza para evitar que varios hilos accedan simultáneamente a recursos compartidos. Cuando un hilo necesita acceder a un recurso compartido, primero debe bloquear (lock) el mutex. Si el mutex ya está bloqueado por otro hilo, el hilo que solicita el bloqueo se bloqueará hasta que el mutex se desbloquee (unlock). std::mutex: se utiliza para proteger los recursos compartidos y evitar la condición de carrera.
std::lock_guardystd::unique_lock: gestiona automáticamente la adquisición y liberación del bloqueo. Ejemplo de uso del mutex: C++ proporciona varios tipos de bloqueos para simplificar el uso y la gestión de los mutex. Los tipos de bloqueo más comunes incluyen: Ejemplo de uso de los bloqueos: La variable de condición se utiliza para la coordinación entre hilos; permite que uno o más hilos esperen a que ocurra una determinada condición. Normalmente se usa junto con un mutex para lograr la sincronización entre hilos. std::condition_variableSe utiliza para implementar el mecanismo de espera y notificación entre hilos. Ejemplo de uso de la variable de condición: Las operaciones atómicas aseguran que el acceso a los datos compartidos sea indivisible, es decir, en un entorno de subprocesos múltiples, una operación atómica se ejecuta por completo o no se ejecuta en absoluto, sin estados intermedios. Ejemplo de uso de operaciones atómicas: El almacenamiento local de subprocesos permite que cada subproceso tenga su propia copia de datos. Esto se puede lograr mediante la palabra clave thread_local, evitando la contención por recursos compartidos. Ejemplo de uso del almacenamiento local de subprocesos: El punto muerto ocurre cuando varios subprocesos esperan mutuamente a que los otros liberen recursos, pero ninguno puede continuar su ejecución. Las estrategias para evitar el punto muerto incluyen: std::future y std::promise: implementan la transferencia de valores entre subprocesos. Cola de mensajes (basada en std::queue y std::mutex) para implementar una comunicación simple entre subprocesos. C++17 introdujo la biblioteca de algoritmos paralelos ( Más ejemplos de referencia: C++ Multithreading:http://www.example.com/w3cnote/cpp-multithread-demo.html C++ std::thread: https://www.example.com/w3cnote/cpp-std-thread.html#include<thread>
std::thread thread_object(callable, args...);
callable: objeto invocable, puede ser un puntero a función, un objeto de función, una expresión lambda, etc.args...: pasado acallablela lista de parámetros.Usar punteros a función
Ejemplo
#include <thread>
void printMessage(int count) {
for (int i = 0; i < count; ++i) {
std::cout << "Hello from thread (function pointer)!\n";
}
}
int main() {
std::thread t1(printMessage, 5); // Crea el hilo, pasa el puntero a función y los argumentos
t1.join(); // Espera a que el hilo termine
return 0;
}
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Hello from thread (function pointer)!
Usar objetos de función
operator()método de la clase se define el objeto de función para crear el hilo:Ejemplo
#include <thread>
class PrintTask {
public:
void operator()(int count) const {
for (int i = 0; i < count; ++i) {
std::cout << "Hello from thread (function object)!\n";
}
}
};
int main() {
std::thread t2(PrintTask(), 5); // Crea el hilo, pasa el objeto de función y los argumentos
t2.join(); // Espera a que el hilo termine
return 0;
}
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Hello from thread (function object)!
Usar expresiones lambda
Ejemplo
#include <thread>
int main() {
std::thread t3([](int count) {
for (int i = 0; i < count; ++i) {
std::cout << "Hello from thread (lambda)!\n";
}
}, 5); // Crea el hilo, pasa la expresión lambda y los argumentos
t3.join(); // Espera a que el hilo termine
return 0;
}
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Hello from thread (lambda)!
Gestión de hilos
join()
t.join();
detach()
t.detach();
Paso de parámetros a hilos
Paso por valor
std::thread t(func, arg1, arg2);
Paso por referencia
Ejemplo
#include <thread>
void increment(int& x) {
++x;
}
int main() {
int num = 0;
std::thread t(increment, std::ref(num)); // Usa std::ref para pasar la referencia
t.join();
std::cout << "Value after increment: " << num << std::endl;
return 0;
}
Ejemplo completo
Ejemplo
#include <thread>
using namespace std;
// Una función simple que sirve como función de entrada del hilo
void foo(int Z) {
for (int i = 0; i < Z; i++) {
cout << "El hilo usa puntero a función como parámetro invocable\n";
}
}
// Definición de la clase del objeto invocable
class ThreadObj {
public:
void operator()(int x) const {
for (int i = 0; i < x; i++) {
cout << "El hilo usa objeto de función como parámetro invocable\n";
}
}
};
int main() {
cout << "Los hilos 1, 2 y 3 se ejecutan de forma independiente" << endl;
// Crea el hilo usando puntero a función
thread th1(foo, 3);
// Crea el hilo usando objeto de función
thread th2(ThreadObj(), 3);
// Crea el hilo usando una expresión lambda
thread th3([](int x) {
for (int i = 0; i < x; i++) {
cout << "El hilo usa expresión lambda como parámetro invocable\n";
}
}, 3);
// Espera a que todos los hilos terminen
th1.join(); // Espera a que el hilo th1 termine
th2.join(); // Espera a que el hilo th2 termine
th3.join(); // Espera a que el hilo th3 termine
return 0;
}
g++ -std=c++11 test.cpp
线程 1 、2 、3 独立运行
线程使用函数指针作为可调用参数
线程使用函数对象作为可调用参数
线程使用 lambda 表达式作为可调用参数
线程使用函数指针作为可调用参数
...
Sincronización y exclusión mutua de hilos
1. Mutex (exclusión mutua)
std::mutex mtx;
mtx.lock(); // 锁定互斥锁
// 访问共享资源
mtx.unlock(); // 释放互斥锁
std::lock_guard<std::mutex> lock(mtx); // 自动锁定和解锁
// 访问共享资源
Ejemplo
std::mutex mtx; // Mutex global
void safeFunction() {
mtx.lock(); // Solicita bloquear el mutex
// Accede o modifica el recurso compartido
mtx.unlock(); // Libera el mutex
}
int main() {
std::thread t1(safeFunction);
std::thread t2(safeFunction);
t1.join();
t2.join();
return 0;
}
2. Bloqueos (Locks)
Ejemplo
std::mutex mtx;
void safeFunctionWithLockGuard() {
std::lock_guard<std::mutex> lk(mtx);
// Accede o modifica el recurso compartido
}
void safeFunctionWithUniqueLock() {
std::unique_lock<std::mutex> ul(mtx);
// Accede o modifica el recurso compartido
// ul.unlock(); // Opcional: desbloqueo manual
// ...
}
3. Variable de condición (Condition Variable)
std::condition_variable cv;
std::mutex mtx;
bool ready = false;
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 等待条件满足
// 条件满足后执行
Ejemplo
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void workerThread() {
std::unique_lock<std::mutex> lk(mtx);
cv.wait(lk, []{ return ready; }); // Esperar condición
// Ejecutar el trabajo cuando se cumpla la condición
}
void mainThread() {
{
std::lock_guard<std::mutex> lk(mtx);
// Preparar datos
ready = true;
} // Desbloquear al salir del ámbito
cv.notify_one(); // Notificar a un hilo en espera
}
4. Operaciones atómicas (Atomic Operations)
Ejemplo
#include <thread>
std::atomic<int> count(0);
void increment() {
count.fetch_add(1, std::memory_order_relaxed);
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
return count; // Debería devolver 2
}
5. Almacenamiento local de hilo (Thread Local Storage, TLS)
Ejemplo
#include <thread>
thread_local int threadData = 0;
void threadFunction() {
threadData = 42; // Cada subproceso tiene su propia copia de threadData
std::cout << "Thread data: " << threadData << std::endl;
}
int main() {
std::thread t1(threadFunction);
std::thread t2(threadFunction);
t1.join();
t2.join();
return 0;
}
6. Punto muerto (Deadlock) y estrategias para evitarlo
Comunicación entre hilos
Ejemplo
std::promise<int> p;
std::future<int> f = p.get_future();
std::thread t([&p] {
p.set_value(10); // Establecer el valor, activar el future
});
int result = f.get(); // Obtener el valor
<algorithm>), donde algunos algoritmos admiten la ejecución en paralelo, lo que permite aprovechar CPU multinúcleo para mejorar el rendimiento.Ejemplo
#include <vector>
#include <execution>
std::vector<int> vec = {1, 2, 3, 4, 5};
std::for_each(std::execution::par, vec.begin(), vec.end(), [](int &n) {
n *= 2;
});
Algoritmos paralelos en la biblioteca estándar de C++
Otras extensiones