Use o primitivo de sincronização std::mutex em C++

Jinku Hu 12 outubro 2023
Use o primitivo de sincronização std::mutex em C++

Este artigo demonstrará como usar a primitiva de sincronização std::mutex em C++.

Use std::mutex para proteger o acesso a dados compartilhados entre threads em C++

Geralmente, as primitivas de sincronização são ferramentas para o programador controlar com segurança o acesso a dados compartilhados em programas que utilizam simultaneidade.

Uma vez que a modificação não ordenada de localizações de memória compartilhada de vários threads produz resultados errôneos e comportamento imprevisível do programa, cabe ao programador garantir que o programa seja executado de maneira determinística. Sincronização e outros tópicos na programação simultânea são bastante complexos e muitas vezes exigem amplo conhecimento em várias camadas de características de software e hardware em sistemas de computação modernos.

Assim, assumiremos algum conhecimento prévio desses conceitos, ao mesmo tempo em que cobrimos uma parte muito pequena do tópico de sincronização neste artigo. Nomeadamente, iremos introduzir o conceito de exclusão mútua, também conhecido como mutex (frequentemente, o mesmo nome é dado aos nomes de objetos em linguagens de programação, por exemplo, std::mutex).

Um mutex é um tipo de mecanismo de bloqueio que pode envolver uma seção crítica do programa e garantir que o acesso a ela seja protegido. Quando dizemos que o recurso compartilhado está protegido, isso significa que se uma thread estiver executando a operação de gravação no objeto compartilhado, outras threads não funcionarão até que a thread anterior termine a operação.

Observe que um comportamento como esse pode não ser o ideal para alguns problemas, pois podem surgir contenção de recursos, falta de recursos ou outros problemas relacionados ao desempenho. Assim, alguns outros mecanismos abordam essas questões e oferecem características diferentes que vão além do escopo deste artigo.

No exemplo a seguir, mostramos o uso básico da classe std::mutex conforme fornecido pelo C++ STL. Observe que o suporte padrão para threading foi adicionado desde a versão C++ 11.

Primeiramente, precisamos construir um objeto std::mutex que então pode ser usado para controlar o acesso ao recurso compartilhado. std::mutex tem duas funções-membro centrais - lock e unlock. A operação lock é geralmente chamada antes do recurso compartilhado ser modificado e unlock é chamado após a modificação.

O código inserido entre essas chamadas é conhecido como seção crítica. Mesmo que a ordem anterior do layout do código esteja correta, C++ fornece outra classe de modelo útil - std::lock_guard, que pode desbloquear automaticamente o mutex fornecido quando o escopo é deixado. A principal razão para utilizar lock_guard em vez de usar diretamente as funções de membro lock e unlock é garantir que o mutex será desbloqueado em todos os caminhos de código, mesmo se as exceções forem levantadas. Portanto, nosso exemplo de código também usará o último método para demonstrar o uso de std::mutex.

O programa main é construído para criar dois recipientes vector de inteiros aleatórios e, em seguida, enviar o conteúdo de ambos para uma lista. A parte complicada é que queremos utilizar vários threads para adicionar elementos à lista.

Na verdade, invocamos a função generateNumbers com dois threads, mas eles operam em objetos diferentes e a sincronização é desnecessária. Depois de gerar os inteiros, a lista pode ser preenchida chamando a função addToList.

Observe que esta função começa com a construção lock_guard e então inclui as operações que precisam ser realizadas na lista. Nesse caso, ele apenas chama a função push_back no objeto de lista fornecido.

#include <chrono>
#include <iostream>
#include <list>
#include <mutex>
#include <string>
#include <thread>
#include <vector>

using std::cout;
using std::endl;
using std::list;
using std::string;
using std::vector;

std::mutex list1_mutex;

const int MAX = 1000;
const int NUMS_TO_GENERATE = 1000000;

void addToList(const int &num, list<int> &l) {
  std::lock_guard<std::mutex> guard(list1_mutex);
  l.push_back(num);
}

void generateNumbers(vector<int> &v) {
  for (int n = 0; n < NUMS_TO_GENERATE; ++n) {
    v.push_back(std::rand() % MAX);
  }
}

int main() {
  list<int> list1;
  vector<int> vec1;
  vector<int> vec2;

  std::thread t1(generateNumbers, std::ref(vec1));
  std::thread t2(generateNumbers, std::ref(vec2));
  t1.join();
  t2.join();

  cout << vec1.size() << ", " << vec2.size() << endl;

  for (int i = 0; i < NUMS_TO_GENERATE; ++i) {
    std::thread t3(addToList, vec1[i], std::ref(list1));
    std::thread t4(addToList, vec2[i], std::ref(list1));
    t3.join();
    t4.join();
  }

  cout << "list size = " << list1.size() << endl;

  return EXIT_SUCCESS;
}

Produção:

1000000, 1000000
list size = 2000000

No trecho de código anterior, optamos por criar dois encadeamentos separados em cada iteração do loop for e uni-los ao encadeamento principal no mesmo bucle. Este cenário é ineficiente, pois leva um tempo de execução precioso para criar e destruir threads, mas só o oferecemos para demonstrar o uso de mutex.

Normalmente, se houver necessidade de gerenciar vários threads durante algum tempo arbitrário no programa, o conceito de pool de threads é utilizado. A forma mais simples desse conceito criará um número fixo de threads no início da rotina de trabalho e, em seguida, começará a atribuir-lhes unidades de trabalho em forma de fila. Quando um thread completa sua unidade de trabalho, ele pode ser reutilizado para a próxima unidade de trabalho pendente. Lembre-se, porém, de que nosso código de driver foi projetado apenas para ser uma simulação simples de fluxo de trabalho multithread em um programa.

Autor: Jinku Hu
Jinku Hu avatar Jinku Hu avatar

Founder of DelftStack.com. Jinku has worked in the robotics and automotive industries for over 8 years. He sharpened his coding skills when he needed to do the automatic testing, data collection from remote servers and report creation from the endurance test. He is from an electrical/electronics engineering background but has expanded his interest to embedded electronics, embedded programming and front-/back-end programming.

LinkedIn Facebook