Consulte el blog https://blog.csdn.net/hai008007/article/details/80246437 para organizarlo y modificarlo.
La interacción de datos entre varios subprocesos en el mismo proceso es inevitable. Las colas y los datos compartidos son formas comunes de lograr la interacción de datos entre varios subprocesos. La cola encapsulada es relativamente menos propensa a errores y los datos compartidos son los más básicos y propensos a errores. , porque causará contención de datos, es decir, más de un hilo intenta tomar un recurso al mismo tiempo, como leer y escribir un bloque de memoria, como se muestra en el siguiente ejemplo:
#include <iostream>
#include <thread>
using namespace std;
#define COUNT 10000
void inc(int *p){
for(int i = 0; i < COUNT; i++){
(*p)++;
}
}
int main()
{
int a = 0;
thread ta(inc, &a);
thread tb(inc, &a);
ta.join();
tb.join();
cout << " a = " << a << endl;
return 0;
}
El ejemplo anterior es un simple intercambio de datos, se puede ver que dos hilos leen y escriben la dirección de memoria de & a al mismo tiempo. En la superficie, después de que se ejecutan los dos subprocesos, el valor final a debería ser COUNT * 2, pero de hecho no es el caso, porque puede haber dos subprocesos que necesiten acceder a esta pieza de memoria al mismo tiempo para la operación. , y el hilo se interrumpirá. Para resolver este problema, para tipos básicos simples de datos como caracteres, números enteros, punteros, etc., C ++ proporciona la clase de plantilla atómica atómica, y para objetos complejos, proporciona el mecanismo de bloqueo más utilizado, como la clase de exclusión mutua mutex. , cerradura de puerta lock_guard, cerradura única unique_lock, condición variable condition_variable, etc.
std :: atómico
Para los subprocesos, los tipos atómicos pertenecen a datos de "tipo de recurso", lo que significa que varios subprocesos normalmente solo pueden acceder a una copia de un solo tipo atómico.
Por lo tanto, en C ++ 11, los tipos atómicos solo se pueden construir a partir de sus tipos de parámetros de plantilla.
El estándar no permite que los tipos atómicos realicen operaciones como la construcción de copias, la construcción de movimientos y el uso de operator =. De hecho, las operaciones como la construcción de copias, la construcción de movimientos y las plantillas operator = de clases atómicas se eliminan de forma predeterminada.
Sin embargo, es posible construir variables del tipo de parámetro de plantilla T a partir de las variables de tipo atómico. Esto se debe a que la plantilla de clase atómica siempre define la función de conversión de tipo de atomic <T> a T.Cuando sea necesario, el compilador implícitamente Complete la conversión del tipo atómico al tipo de parámetro de plantilla correspondiente.
El archivo de encabezado C11 <cstdatomic> simplemente define el tipo atómico correspondiente al tipo incorporado
atómico | Tipos de |
---|---|
atomic_bool | bool |
atomic_char | carbonizarse |
atomic_schar | char firmado |
atomic_uchar | char sin firmar |
atomic_int | En t |
atomic_uint | int sin firmar |
atomic_short | corto |
atomic_ushort | corto sin firmar |
atomic_long | largo |
atomic_ulong | largo sin firmar |
atomic_llong | largo largo |
atomic_ullong | sin firmar mucho tiempo |
atomic_char16_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_t |
Nuevamente, por ejemplo:
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
#define COUNT 10000
void inc(atomic<int> *p){
for(int i = 0; i < COUNT; i++){
(*p)++;
}
}
int main()
{
atomic<int> a{
0};
thread ta(inc, &a);
thread tb(inc, &a);
ta.join();
tb.join();
cout << " a = " << a << endl;
return 0;
}
std :: lock_guard
Tomemos primero un pequeño ejemplo:
mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();
En este código, el mutex m asegura que el acceso de la parte clave sharedVariable = getVar (); es secuencial.
Medios secuenciales: en este caso especial, cada hilo obtiene acceso a la parte clave en orden.
El código es simple, pero es propenso a bloquearse. Si la parte crítica arroja una excepción o el programador simplemente se olvida de desbloquear el mutex, se producirá un punto muerto.
Usando std :: lock_guard, podemos hacerlo de manera más elegante:
{
std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();
}
Es fácil. Pero, ¿qué son el corchete de apertura {y el corchete de cierre}?
Para garantizar que el ciclo de vida de std :: lock_guard solo sea válido en este {}.
En otras palabras, cuando el ciclo de vida abandona la zona crítica, su ciclo de vida finaliza.
Para ser precisos, en ese momento, se llamó al destructor de std :: lock_guard, y sí, se liberó el mutex. El proceso es completamente automático Además, si getVar () lanza una excepción cuando sharedVariable = getVar () también libera el mutex. Por supuesto, el alcance del cuerpo o bucle de la función también limita el ciclo de vida del objeto.
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
#define COUNT 10000
static mutex g_mutex;
void inc(int *p){
for(int i = 0; i < COUNT; i++){
lock_guard<mutex> lck(g_mutex);
(*p)++;
}
}
int main()
{
int a{
0};
thread ta(inc, &a);
thread tb(inc, &a);
ta.join();
tb.join();
cout << " a = " << a << endl;
return 0;
}
Además, también se puede utilizar unique_lock ().
Unique_lock es una plantilla de clase. En el trabajo, generalmente lock_guard (recomendado); lock_guard reemplaza lock () y unlock () de mutex; unique_lock es mucho más flexible que lock_guard, menos eficiente y ocupa un poco más de memoria. El uso específico de unique_lock () se explica por separado, por lo que no lo repetiré aquí.
std :: mutex
std :: mutex es el mutex más básico en C ++ 11. El objeto std :: mutex proporciona la característica de propiedad exclusiva, es decir, no admite el bloqueo recursivo de objetos std :: mutex, mientras que std :: recursive_lock El mutex El objeto se puede bloquear de forma recursiva.
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
#define COUNT 10000
static mutex g_mutex;
void inc(int *p){
for(int i = 0; i < COUNT; i++){
g_mutex.lock();
(*p)++;
g_mutex.unlock();
}
}
int main()
{
int a{
0};
thread ta(inc, &a);
thread tb(inc, &a);
ta.join();
tb.join();
cout << " a = " << a << endl;
return 0;
}
std :: condition_variable
Para notificaciones de eventos entre subprocesos, C ++ 11 proporciona una variable de condición class condition_variable (que puede considerarse como una encapsulación de pthread_cond_t). El uso de variables de condición permite que un subproceso espere notificaciones de otros subprocesos (esperar, esperar_para, esperar_hasta). , u otro El hilo envía notificaciones (notificar_uno, notificar_todos). La variable de condición debe usarse junto con el candado. En espera, debido a que hay desbloqueo y rebloqueo, debe usar un candado que se pueda desbloquear y bloquear manualmente mientras espera , como unique_lock, pero no se puede usar lock_guard, un ejemplo es el siguiente:
#include <thread>
#include <iostream>
#include <condition_variable>
# define THREAD_COUNT 10
using namespace std;
mutex m;
condition_variable cv;
int main(void){
thread** t = new thread*[THREAD_COUNT];
int i;
for(i = 0; i < THREAD_COUNT; i++){
t[i] = new thread( [](int index){
unique_lock<mutex> lck(m);
cv.wait_for(lck, chrono::hours(1000));
cout << index << endl;}, i );
this_thread::sleep_for( chrono::milliseconds(50) );
}
for(i = 0; i < THREAD_COUNT; i++){
lock_guard<mutex> _(m);
cv.notify_one();
}
for(i = 0; i < THREAD_COUNT; i++){
t[i]->join();
delete t[i];
}
delete t;
return 0;
}
Después de compilar y ejecutar el programa y generar los resultados, puede ver que el orden de las variables de condición no está garantizado, es decir, es posible que la que llama a esperar primero no se despierte primero.
std :: promesa / futuro
Promise / future se puede usar para llevar a cabo una interacción de datos simple entre subprocesos sin considerar el problema de los bloqueos. El subproceso A guarda los datos en una variable de promesa, y otro subproceso B puede obtenerlos a través de get_future () de esta variable de promesa. Valor, cuando El subproceso A aún no ha asignado un valor en la variable de promesa, el subproceso B también puede esperar la asignación de la variable de promesa:
#include <thread>
#include <iostream>
#include <future>
using namespace std;
promise<string> val;
int main(void){
thread ta([](){
future<string> fu = val.get_future();
cout << "waiting promise->future" << endl;
cout << fu.get() << endl;
});
thread tb([](){
this_thread::sleep_for( chrono::milliseconds(5000) );
val.set_value("promise is set");
});
ta.join();
tb.join();
return 0;
}
Una variable de futuro solo puede llamar a get () una vez. Si necesita llamar a get () varias veces, puede usar shared_future. También puede pasar excepciones entre hilos a través de promise / future.
std :: packaged_task
Si combina un objeto invocable con una promesa, es packaged_task, lo que puede simplificar aún más la operación:
#include <thread>
#include <iostream>
#include <mutex>
#include <future>
using namespace std;
static mutex g_mutex;
int main(void){
auto run = [=](int index){
{
lock_guard<mutex> lck(g_mutex);
cout << "tasklet " << index << endl;
}
this_thread::sleep_for( chrono::seconds(5) );
return index * 1000;
};
packaged_task<int(int)> pt1(run);
packaged_task<int(int)> pt2(run);
thread t1( [&](){
pt1(2);} );
thread t2( [&](){
pt2(3);} );
int f1 = pt1.get_future().get();
int f2 = pt2.get_future().get();
cout << "task result=" << f1 << endl;
cout << "task result=" << f2 << endl;
t1.join();
t2.join();
return 0;
}
std :: async
También puede intentar combinar una packaged_task con un hilo, que es la función async (). Utilice la función async () para iniciar el código de ejecución y devolver un objeto futuro para guardar el valor de retorno del código. No necesitamos crear y destruir explícitamente subprocesos, etc., pero la implementación de la biblioteca C ++ 11 decide cuándo crear y destruir subprocesos, y crear varios subprocesos, etc., los ejemplos son los siguientes:
#include <thread>
#include <iostream>
#include <mutex>
#include <future>
#include <vector>
# define COUNT 1000000
using namespace std;
static long do_sum(vector<long> *arr, size_t start, size_t count){
static mutex m;
long sum = 0;
for(size_t i = 0; i < count; i++){
sum += (*arr)[start + i];
}
{
lock_guard<mutex> lck(m);
cout << "thread " << this_thread::get_id() << ", count=" << count
<< ", start="<< start << ", sum=" << sum << endl;
}
return sum;
}
int main(void){
vector<long> data(COUNT);
for(size_t i = 0; i < COUNT; i++){
data[i] = random() & 0xff;
}
vector< future<long> > result;
size_t ptc = thread::hardware_concurrency() * 2;
for(size_t batch = 0; batch < ptc; batch++) {
size_t batch_each = COUNT / ptc;
if (batch == ptc - 1) {
batch_each = COUNT - (COUNT / ptc * batch);
}
result.push_back(async(do_sum, &data, batch * batch_each, batch_each));
}
long total = 0;
for(size_t batch = 0; batch < ptc; batch++) {
total += result[batch].get();
}
cout << "total=" << total << endl;
return 0;
}
En resumen, los anteriores son varios métodos de subprocesos múltiples diferentes, y los ejemplos utilizados son muy simples.
Para obtener detalles de los diferentes métodos específicos, aquí hay un enlace . Seguimiento, continúe aprendiendo.