Ir para o conteúdo

Última atualização:2026-10-12

Introdução a multi-thread em C++

Todo o código deste artigo está no repositório de C++那些事.

https://github.com/Light-City/CPlusPlusThings

1.Entendendo threads e processos

O thread é a unidade básica de escalonamento; o processo é a unidade básica de alocação de recursos. Um programa pode ser entendido como um processo, e o processo contém vários threads.

Por exemplo: o navegador é um processo, e cada aba aberta é um thread.

Diferenças simples entre os dois:

  • Espaço de endereçamento e outros recursos: processos são independentes entre si, enquanto os threads de um mesmo processo os compartilham. Os threads de um processo não são visíveis para outros processos.
  • Comunicação: processos se comunicam via IPC; threads podem ler e escrever diretamente no segmento de dados do processo (como variáveis globais) para se comunicar — com o auxílio de mecanismos de sincronização e exclusão mútua, para garantir a consistência dos dados.
  • Escalonamento e troca de contexto: a troca de contexto entre threads é muito mais rápida que a troca de contexto entre processos.
  • Em um SO multi-thread, o processo não é uma entidade executável.

Quanto à comunicação via IPC e à comunicação entre threads, haverá um novo artigo depois.

2.Cinco maneiras de criar threads

  • Ponteiro de função
  • Função Lambda
  • Functor (functor)
  • Função-membro não estática
  • Função-membro estática

2.1 Ponteiro de função

// 1.函数指针
void fun(int x) {
    while (x-- > 0) {
        cout << x << endl;
    }
}
// 调用
std::thread t1(fun, 10);
t1.join();

2.2 Função Lambda

// 注意:如果我们创建多线程 并不会保证哪一个先开始
int main() {
    // 2.Lambda函数
    auto fun = [](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    };
//    std::1.thread t1(fun, 10);
    // 也可以写成下面:
    std::thread t1_1([](int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }, 11);
//    std::1.thread t2(fun, 10);
//    t1.join();
    t1_1.join();
//    t2.join();
    return 0;
}

2.3 Functor

// 3.functor (Funciton Object)
class Base {
public:
    void operator()(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(Base(), 10);
t.join();

2.4 Função-membro não estática

// 4.Non-static member function
class Base {
public:
    void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(&Base::fun,&b, 10);
t.join();

2.5 Função-membro estática

// 4.Non-static member function
class Base {
public:
    static void fun(int x) {
        while (x-- > 0) {
            cout << x << endl;
        }
    }
};
// 调用
thread t(&Base::fun, 10);
t.join();

3.join e detach

3.1 join

  • Assim que o thread começa, se quisermos esperar sua conclusão, precisamos chamar join() nesse objeto
  • Um join duplo fará o programa terminar
  • Antes do join, devemos verificar com joinable() se ele pode sofrer join
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.join();
    if (t1.joinable()) {
        t1.join();
    }
    cout << "main() after" << endl;
    return 0;
}

3.2 detach

  • Isso é usado para separar do thread pai o thread recém-criado
  • Antes de separar o thread, verifique sempre se ele é joinable; caso contrário, pode ocorrer uma dupla separação, e um detach() duplo fará o programa terminar
  • Se temos um thread separado e a função main está retornando, a execução do thread separado será suspensa
void run(int count) {
    while (count-- > 0) {
        cout << count << endl;
    }
    std::this_thread::sleep_for(chrono::seconds(3));
}

int main() {
    thread t1(run, 10);
    cout << "main()" << endl;
    t1.detach();
    if(t1.joinable())
        t1.detach();
    cout << "main() after" << endl;
    return 0;

4.Seção crítica e mutex

4.1 O que é uma seção crítica (Critical Section)?

Uma seção crítica é um trecho de código que, para o programa funcionar corretamente, só pode ser executado por um thread por vez. Se dois threads (ou processos) executarem simultaneamente o código dentro da seção crítica, o programa pode deixar de ter o comportamento correto.

4.2 Apenas incrementar uma variável é uma seção crítica?

Pode ser.

O processo de incrementar uma variável (i++) tem três etapas:

  • Copiar o conteúdo da memória para um registrador da CPU. load
  • Incrementar esse valor na CPU. increment
  • Armazenar o novo valor na memória. store

Se essa posição de memória (por exemplo, a variável i abaixo) puder ser acessada por apenas um thread, não haverá disputa nem seção crítica associada a i. Mas a variável sum é uma variável global, que pode ser acessada por dois threads. Os dois threads podem tentar incrementá-la ao mesmo tempo.

#include <iostream>
#include <mutex>
#include <thread>

using namespace std;

int sum = 0; //shared

mutex m;

void *countgold() {
    int i; //local to each thread
    for (i = 0; i < 10000000; i++) {
        sum += 1;
    }
    return NULL;
}

int main() {
    thread t1(countgold);
    thread t2(countgold);

    //Wait for both threads to finish
    t1.join();
    t2.join();

    cout << "sum = " << sum << endl;
    return 0;
}
A saída típica do código acima é sum igual a 20000000. Como existe uma condição de corrida, cada execução do programa imprime um total diferente. O código não impede que dois threads leiam e escrevam o total simultaneamente. Por exemplo, os dois threads copiam o valor atual de sum para a CPU em que cada um executa (digamos 123). Os dois threads incrementam cada um a sua cópia em um. Os dois threads gravam o valor de volta (124). Se os threads tivessem acessado o total em momentos diferentes, a contagem seria 125.

4.3 Como garantir que apenas um thread por vez acesse a variável global?

Se um thread está atualmente na seção crítica, queremos que o outro espere até o primeiro terminar. Para isso, podemos usar um mutex (abreviação de mutual exclusion, exclusão mútua).

Metáfora visual do mutex:

bg2013042408

Uma maneira simples de impedir a entrada de outros é colocar uma tranca na porta. A primeira pessoa a chegar tranca a porta; quem chega depois vê a porta trancada e espera na fila até a tranca abrir para entrar. Isso se chama "mutex" (Mutual exclusion, abreviado Mutex), e evita que vários threads leiam e escrevam simultaneamente a mesma região de memória.

m.lock();
sum += 1;
m.unlock();
Com o código acima, a saída já sai correta: sum = 20000000.

Referências

http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html?utm_source=com.ideashower.readitlater.pro&utm_medium=social&utm_oi=35626384621568

https://www.youtube.com/watch?v=eZ8yKZo-PGw&list=PLk6CEY9XxSIAeK-EAh3hB4fgNvYkYmghp&index=4

Tópicos relacionados

Comentários