Consulte o blog https://blog.csdn.net/hai008007/article/details/80246437 para organizá-lo e modificá-lo.
A interação de dados entre vários threads no mesmo processo é inevitável. Filas e dados compartilhados são maneiras comuns de obter interação de dados entre vários threads. A fila encapsulada é relativamente menos propensa a erros de usar, e os dados compartilhados são os mais básicos e propensos a erros. , porque causará contenção de dados, ou seja, mais de um thread tenta pegar um recurso ao mesmo tempo, como lendo e gravando um bloco de memória, conforme mostrado no exemplo a seguir:
#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;
}
O exemplo acima é uma troca de dados simples.Pode ser visto que duas threads leem e escrevem o endereço de memória de & a ao mesmo tempo. Superficialmente, depois que as duas threads são executadas, o valor final deve ser COUNT * 2, mas na verdade não é o caso, porque pode haver duas threads que precisam acessar este pedaço de memória ao mesmo tempo para a operação , e a discussão será interrompida. Para resolver este problema, para tipos básicos simples de dados, como caracteres, inteiros, ponteiros, etc., C ++ fornece a classe de modelo atômica atômica, e para objetos complexos, fornece o mecanismo de bloqueio mais comumente usado, como mutex de classe de exclusão mútua , lock_guard lock da porta, lock exclusivo unique_lock, variável de condição condition_variable, etc.
std :: atomic
Para encadeamentos, os tipos atômicos pertencem aos dados do "tipo de recurso", o que significa que vários encadeamentos geralmente podem acessar apenas uma cópia de um único tipo atômico.
Portanto, no C ++ 11, os tipos atômicos só podem ser construídos a partir de seus tipos de parâmetro de modelo.
O padrão não permite que tipos atômicos realizem operações como construção de cópia, construção de movimentação e uso de operator =. Na verdade, operações como construção de cópia, construção de movimentação e operator = de modelos de classe atômica são excluídas por padrão.
No entanto, é possível construir variáveis do tipo de parâmetro de modelo T a partir das variáveis do tipo atômico. Isso ocorre porque o modelo de classe atômica sempre define a função de conversão de tipo de <T> atômico para T. Quando necessário, o compilador irá implicitamente Conclua a conversão do tipo atômico para o tipo de parâmetro de modelo correspondente.
O arquivo de cabeçalho C11 <cstdatomic> simplesmente define o tipo atômico correspondente ao tipo embutido
atômico | Tipos de |
---|---|
atomic_bool | bool |
atomic_char | Caracteres |
atomic_schar | char assinado |
atomic_uchar | caracter não identifcado |
atomic_int | int |
atomic_uint | int sem sinal |
atomic_short | baixo |
atomic_ushort | curto sem sinal |
atomic_long | grande |
atomic_ulong | longo sem sinal |
atomic_llong | longo longo |
atomic_ullong | longo sem sinal |
atomic_char16_t | char16_t |
atomic_char32_t | char32_t |
atomic_wchar_t | wchar_t |
Novamente, por exemplo:
#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
Vamos dar um pequeno exemplo primeiro:
mutex m;
m.lock();
sharedVariable= getVar();
m.unlock();
Neste código, o mutex m garante que o acesso da parte da chave sharedVariable = getVar (); seja sequencial.
Sequencial significa: neste caso especial, cada thread obtém acesso à parte principal em ordem.
O código é simples, mas está sujeito a deadlock. Se a parte crítica lançar uma exceção ou o programador simplesmente esquecer de desbloquear o mutex, ocorrerá um deadlock.
Usando std :: lock_guard, podemos fazer isso de forma mais elegante:
{
std::mutex m,
std::lock_guard<std::mutex> lockGuard(m);
sharedVariable= getVar();
}
É fácil. Mas quais são os colchetes de abertura {e os colchetes de fechamento}?
Para garantir que o ciclo de vida de std :: lock_guard seja válido apenas neste {}.
Em outras palavras, quando o ciclo de vida deixa a zona crítica, seu ciclo de vida termina.
Para ser mais preciso, naquele momento, o destruidor de std :: lock_guard foi chamado e, sim, o mutex foi lançado. O processo é totalmente automático. Além disso, se getVar () lança uma exceção quando sharedVariable = getVar () também libera o mutex. Obviamente, o escopo do corpo ou loop da função também limita o ciclo de vida do 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;
}
Além disso, unique_lock () também pode ser usado.
Unique_lock é um modelo de classe. No trabalho, geralmente lock_guard (recomendado); lock_guard substitui lock () e unlock () do mutex; unique_lock é muito mais flexível do que lock_guard, menos eficiente e ocupa um pouco mais de memória. O uso específico de unique_lock () é explicado separadamente, portanto, não o repetirei aqui.
std :: mutex
std :: mutex é o mutex mais básico em C ++ 11. O objeto std :: mutex fornece a característica de propriedade exclusiva - isto é, ele não suporta o bloqueio recursivo de objetos std :: mutex, enquanto std :: recursive_lock O mutex objeto pode ser bloqueado recursivamente.
#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 notificações de eventos entre threads, C ++ 11 fornece uma classe de variável de condição condition_variable (que pode ser considerada como um encapsulamento de pthread_cond_t). O uso de variáveis de condição permite que uma thread aguarde notificações de outras threads (wait, wait_for, wait_until) , ou para outro O thread envia notificações (notificar_one, notificar_all). A variável de condição deve ser usada em conjunto com o bloqueio. Ao aguardar, porque há desbloqueio e relocking, você deve usar um bloqueio que pode ser desbloqueado manualmente e bloqueado enquanto espera , como unique_lock, mas não pode ser usado lock_guard, um exemplo é o seguinte:
#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;
}
Depois de compilar e executar o programa e gerar os resultados, você pode ver que a ordem das variáveis de condição não é garantida, ou seja, aquela que chama a espera primeiro pode não ser ativada primeiro.
std :: promessa / futuro
Promise / future pode ser usado para realizar interação de dados simples entre threads sem considerar o problema de bloqueios. Thread A salva os dados em uma variável de promessa e outro thread B pode obtê-los por meio de get_future () desta variável de promessa. Valor, quando o encadeamento A ainda não atribuiu um valor na variável de promessa, o encadeamento B também pode esperar pela atribuição da variável de promessa:
#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;
}
Uma variável futura só pode chamar get () uma vez. Se você precisar chamar get () várias vezes, pode usar shared_future. Você também pode passar exceções entre threads por meio de promessa / futuro.
std :: packaged_task
Se você combinar um objeto que pode ser chamado com uma promessa, é packaged_task, o que pode simplificar ainda mais a operação:
#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
Você também pode tentar combinar um packaged_task com um thread, que é a função async (). Use a função async () para iniciar o código de execução e retornar um objeto futuro para salvar o valor de retorno do código. Não precisamos criar e destruir threads explicitamente, etc., mas a implementação da biblioteca C ++ 11 decide quando criar e destruir threads e Criar vários threads, etc., os exemplos são os seguintes:
#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;
}
Em resumo, o acima são vários métodos de multithreading diferentes, e os exemplos usados são apenas muito simples.
Para obter detalhes sobre os diferentes métodos específicos, aqui está um link . Acompanhamento, continue a aprender.