Compartilhamento de dados entre threads C++

1. Usando mutexes em C++

A biblioteca padrão C++ fornece uma classe de modelo de sintaxe RAII std::lack_guard para mutexes, que fornecerá mutexes bloqueados durante a construção e os desbloqueará durante a destruição, garantindo assim um mutex bloqueado O mutex é sempre devidamente desbloqueado

Proteger a lista com um mutex

#include <list>
#include <mutex>
#include <algorithm>

std::list<int> some_list;    // 1
std::mutex some_mutex;    // 2

void add_to_list(int new_value)
{
    
    
  std::lock_guard<std::mutex> guard(some_mutex);    // 3
  some_list.push_back(new_value);
}

bool list_contains(int value_to_find)
{
    
    
  std::lock_guard<std::mutex> guard(some_mutex);    // 4
  return std::find(some_list.begin(),some_list.end(),value_to_find) != some_list.end();
}

Organizar cuidadosamente o código para proteger os dados compartilhados
Passar inadvertidamente referências a dados protegidos

class some_data
{
    
    
  int a;
  std::string b;
public:
  void do_something();
};

class data_wrapper
{
    
    
private:
  some_data data;
  std::mutex m;
public:
  template<typename Function>
  void process_data(Function func)
  {
    
    
    std::lock_guard<std::mutex> l(m);
    func(data);    // 1 传递“保护”数据给用户函数
  }
};

some_data* unprotected;

void malicious_function(some_data& protected_data)
{
    
    
  unprotected=&protected_data;
}

data_wrapper x;
void foo()
{
    
    
  x.process_data(malicious_function);    // 2 传递一个恶意函数
  unprotected->do_something();    // 3 在无保护的情况下访问保护数据
}

O problema com esse código é que ele não protege nada: ele apenas marca todo o código de estrutura de dados acessível como mutuamente exclusivo. O código que chama unprotected->do_something() na função foo() falhou ao ser marcado como mutuamente exclusivo.

Nesse caso, a biblioteca de encadeamento C++ não pode fornecer nenhuma ajuda e cabe ao programador usar o mutex correto para proteger os dados.

Do ponto de vista otimista, ainda há um caminho a percorrer: Nunca passe ponteiros ou referências a dados protegidos fora do escopo do mutex, seja um valor de retorno de função, armazenado em memória visível externamente, ou passado como argumento para uma função fornecida pelo usuário.

2. Opções para resolver a concorrência condicional

Opção 1: Passar uma referência
Opção 2: Copiar construtor ou mover construtor que não gera exceção
Opção 3: Retornar um ponteiro para o valor exibido

Aqui está um exemplo

#include <exception>
#include <memory>
#include <mutex>
#include <stack>

struct empty_stack: std::exception
{
    
    
  const char* what() const throw() {
    
    };
};

template<typename T>
class threadsafe_stack
{
    
    
private:
  std::stack<T> data;
  mutable std::mutex m;

public:
  threadsafe_stack(){
    
    }

  threadsafe_stack(const threadsafe_stack& other)
  {
    
    
    std::lock_guard<std::mutex> lock(other.m);
    data = other.data; // 1 在构造函数体中的执行拷贝
  }

  threadsafe_stack& operator=(const threadsafe_stack&) = delete;

  void push(T new_value)
  {
    
    
    std::lock_guard<std::mutex> lock(m);
    data.push(new_value);
  }

  std::shared_ptr<T> pop()
  {
    
    
    std::lock_guard<std::mutex> lock(m);
    if(data.empty()) throw empty_stack(); // 在调用pop前,检查栈是否为空

    std::shared_ptr<T> const res(std::make_shared<T>(data.top())); // 在修改堆栈前,分配出返回值
    data.pop();
    return res;
  }

  void pop(T& value)
  {
    
    
    std::lock_guard<std::mutex> lock(m);
    if(data.empty()) throw empty_stack();

    value=data.top();
    data.pop();
  }

  bool empty() const
  {
    
    
    std::lock_guard<std::mutex> lock(m);
    return data.empty();
  }
};

3. Impasse

std::lock - pode bloquear vários (mais de dois) mutexes de uma só vez e não tem efeitos colaterais (risco de deadlock).

// 这里的std::lock()需要包含<mutex>头文件
class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
    
    
private:
  some_big_object some_detail;
  std::mutex m;
public:
  X(some_big_object const& sd):some_detail(sd){
    
    }

  friend void swap(X& lhs, X& rhs)
  {
    
    
    if(&lhs==&rhs)
      return;
    std::lock(lhs.m,rhs.m); // 1 std::lock不会自动解锁,需要给每个锁加上std::lock_guard
    std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock); // 2
    //提供std::adopt_lock参数除了表示std::lock_guard对象已经上锁外,还表示现成的锁,而非尝试创建新的锁
    std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock); // 3
    swap(lhs.some_detail,rhs.some_detail);
  }
};

Prevenção de deadlock
1. Evite bloqueios aninhados, bloqueie vários mutexes com std::lock
2. Evite chamar o código fornecido pelo usuário enquanto mantém um bloqueio
3. Use uma ordem fixa para adquirir bloqueios

1. Use bloqueios hierárquicos para evitar

hierarchical_mutex high_level_mutex(10000); // 1
hierarchical_mutex low_level_mutex(5000);  // 2

int do_low_level_stuff();

int low_level_func()
{
    
    
  std::lock_guard<hierarchical_mutex> lk(low_level_mutex); // 3
  return do_low_level_stuff();
}

void high_level_stuff(int some_param);

void high_level_func()
{
    
    
  std::lock_guard<hierarchical_mutex> lk(high_level_mutex); // 4
  high_level_stuff(low_level_func()); // 5
}

void thread_a()  // 6
{
    
    
  high_level_func();
}

hierarchical_mutex other_mutex(100); // 7
void do_other_stuff();

void other_stuff()
{
    
    
  high_level_func();  // 8
  do_other_stuff();
}

void thread_b() // 9
{
    
    
  std::lock_guard<hierarchical_mutex> lk(other_mutex); // 10
  other_stuff();
}

thread_b() não funcionará sem problemas. Primeiro, ele bloqueia other_mutex⑩, e o valor do nível deste mutex é apenas 100⑦. Isso significa que os dados de nível ultrabaixo (dados de nível ultrabaixo) foram protegidos. Quando other_stuff() chama high_level_func()⑧, a hierarquia é violada.

2. Use a implementação do nível mutex hierarchical_mutex

class hierarchical_mutex
{
    
    
  std::mutex internal_mutex;

  unsigned long const hierarchy_value;
  unsigned long previous_hierarchy_value;

  static thread_local unsigned long this_thread_hierarchy_value;  // 1 被初始化为最大值

  void check_for_hierarchy_violation()
  {
    
    
    if(this_thread_hierarchy_value <= hierarchy_value)  // 2 第二个互斥量的层级值必须小于已经持有互斥量检查函数才能通过
    {
    
     
      throw std::logic_error(“mutex hierarchy violated”);
    }
  }

  void update_hierarchy_value()
  {
    
    
    previous_hierarchy_value=this_thread_hierarchy_value;  // 3
    this_thread_hierarchy_value=hierarchy_value;
  }

public:
  explicit hierarchical_mutex(unsigned long value):
      hierarchy_value(value),
      previous_hierarchy_value(0)
  {
    
    }

  void lock()
  {
    
    
    check_for_hierarchy_violation();
    internal_mutex.lock();  // 4
    update_hierarchy_value();  // 5
  }

  void unlock()
  {
    
    
    this_thread_hierarchy_value=previous_hierarchy_value;  // 6
    internal_mutex.unlock();
  }
/*try_lock()与lock()的功能相似,除了在调用internal_mutex的try_lock()⑦失败时,不能持有对应锁,所以不必更新层级值,并直接返回false。
虽然是运行时检测,但是它没有时间依赖性——不必去等待那些导致死锁出现的罕见条件。同时,设计过程需要去拆分应用,互斥量在这样的情况下可以消除可能导致死锁的可能性。这样的设计练习很有必要去做一下,即使你之后没有去做,代码也会在运行时进行检查。*/

  bool try_lock()
  {
    
    
    check_for_hierarchy_violation();
    if(!internal_mutex.try_lock())  // 7
      return false;
    update_hierarchy_value();
    return true;
  }
};
thread_local unsigned long
     hierarchical_mutex::this_thread_hierarchy_value(ULONG_MAX);  // 7

4. std::unique_lock

std::unique_lock e std::lock_guard

std::unique_lock ocupa mais espaço e é um pouco mais lento que std::lock_guard. Há um preço a pagar pela flexibilidade, e esse preço é permitir instâncias std::unique_lock sem um mutex: as informações já estão armazenadas e atualizadas.

1. O uso de std::lock() e std::unique_lock na operação de troca

class some_big_object;
void swap(some_big_object& lhs,some_big_object& rhs);
class X
{
    
    
private:
  some_big_object some_detail;
  std::mutex m;
public:
  X(some_big_object const& sd):some_detail(sd){
    
    }
  friend void swap(X& lhs, X& rhs)
  {
    
    
    if(&lhs==&rhs)
      return;
    std::unique_lock<std::mutex> lock_a(lhs.m,std::defer_lock); // 1 
    std::unique_lock<std::mutex> lock_b(rhs.m,std::defer_lock); // 1 std::def_lock 留下未上锁的互斥量
    std::lock(lock_a,lock_b); // 2 互斥量在这里上锁
    swap(lhs.some_detail,rhs.some_detail);
  }
};

2. Unique_lock pode ser adicionado com o parâmetro std::adopt_lock
① Adicionar std::try_to_lock
pode evitar uma espera desnecessária e julgará se o mutex atual pode ser bloqueado. Se não puder ser bloqueado, você pode executar outros códigos primeiro

#include <iostream>
#include <mutex>

std::mutex mlock;

void work1(int& s) {
    
    
	for (int i = 1; i <= 5000; i++) {
    
    
		std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
		if (munique.owns_lock() == true) {
    
    
			s += i;
		}
		else {
    
    
			// 执行一些没有共享内存的代码
		}
	}
}

void work2(int& s) {
    
    
	for (int i = 5001; i <= 10000; i++) {
    
    
		std::unique_lock<std::mutex> munique(mlock, std::try_to_lock);
		if (munique.owns_lock() == true) {
    
    
			s += i;
		}
		else {
    
    
			// 执行一些没有共享内存的代码
		}
	}
}

int main()
{
    
    
	int ans = 0;
	std::thread t1(work1, std::ref(ans));
	std::thread t2(work2, std::ref(ans));
	t1.join();
	t2.join();
	std::cout << ans << std::endl;
	return 0;
}

② Adicionar o parâmetro std::defer_lock
significa não bloqueá-lo temporariamente e depois bloqueá-lo manualmente, mas não é permitido bloqueá-lo antes de usá-lo. Geralmente usado com funções de membro unique_lock para usar

#include <iostream>
#include <mutex>

std::mutex mlock;

void work1(int& s) {
    
    
	for (int i = 1; i <= 5000; i++) {
    
    
		std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
		munique.lock();
		s += i;
		munique.unlock();         // 这里可以不用unlock,可以通过unique_lock的析构函数unlock
	}
}

void work2(int& s) {
    
    
	for (int i = 5001; i <= 10000; i++) {
    
    
		std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
		munique.lock();
		s += i;
		munique.unlock();
	}
}

int main()
{
    
    
	int ans = 0;
	std::thread t1(work1, std::ref(ans));
	std::thread t2(work2, std::ref(ans));
	t1.join();
	t2.join();
	std::cout << ans << std::endl;
	return 0;
}

Há também uma função de membro try_lock, que tem a mesma função do parâmetro try_to_lock acima. Ela determina se o bloqueio atual pode ser usado. Se não, execute outros códigos e retorne false. Se sim, bloqueie e retorne true. O código é do seguinte modo:

void work1(int& s) {
    
    
	for (int i = 1; i <= 5000; i++) {
    
    
		std::unique_lock<std::mutex> munique(mlock, std::defer_lock);
		if (munique.try_lock() == true) {
    
    
			s += i;
		}
		else {
    
    
			// 处理一些没有共享内存的代码
		}
	}
}

A função release libera a conexão entre o unique_lock e o objeto mutex e retorna o ponteiro do objeto mutex original. Se o mutex anterior foi bloqueado, você precisa desbloqueá-lo manualmente mais tarde, o código é o seguinte:

void work1(int& s) {
    
    
	for (int i = 1; i <= 5000; i++) {
    
    
		std::unique_lock<std::mutex> munique(mlock);   // 这里是自动lock
		std::mutex *m = munique.release();
		s += i;
		m->unlock();
	}
}

3. Transferência de propriedade mutex em diferentes domínios

std::unique_lock<std::mutex> get_lock()
{
    
    
  extern std::mutex some_mutex;
  std::unique_lock<std::mutex> lk(some_mutex);
  prepare_data();
  return lk;  // 1
}
void process_data()
{
    
    
  std::unique_lock<std::mutex> lk(get_lock());  // 2
  do_something();
}

5. Instalações alternativas para proteção de dados compartilhados

1. Lidando com corridas condicionais

A biblioteca padrão C++ fornece std::once_flag e std::call_once para manipular a competição condicional.
Em vez de bloquear o mutex e verificar o ponteiro explicitamente, cada thread só precisa usar std::call_once e, no final de std::call_once, pode saber com segurança que o ponteiro foi inicializado por outros threads.
O uso de std::call_once consome menos recursos do que o uso explícito de um mutex, especialmente quando a inicialização é concluída.

std::shared_ptr<some_resource> resource_ptr;
std::once_flag resource_flag;  // 1

void init_resource()
{
    
    
  resource_ptr.reset(new some_resource);
}

void foo()
{
    
    
  std::call_once(resource_flag,init_resource);  // 可以完整的进行一次初始化
  resource_ptr->do_something(); //some_resource初始化完成,可以使用resource_ptr
}

2. Use std::call_once como inicialização preguiçosa de membros de classe (segurança de thread)

class X
{
    
    
private:
  connection_info connection_details;
  connection_handle connection;
  std::once_flag connection_init_flag;

  void open_connection()
  {
    
    
    connection=connection_manager.open(connection_details);
  }
public:
  X(connection_info const& connection_details_):
      connection_details(connection_details_)
  {
    
    }
  void send_data(data_packet const& data)  // 1
  {
    
    
    std::call_once(connection_init_flag,&X::open_connection,this);  // 2
    connection.send_data(data);
  }
  data_packet receive_data()  // 3
  {
    
    
    std::call_once(connection_init_flag,&X::open_connection,this);  // 2
    return connection.receive_data();
  }
};

Aqui está uma alternativa para std::call_once onde apenas uma instância global é necessária

class my_class;
my_class& get_my_class_instance()
{
    
    
  static my_class instance;  // 线程安全的初始化过程
  return instance;
}

Multithreading pode chamar com segurança a função get_my_class_instance()① sem se preocupar com a competição de dados.

No padrão C++11: a inicialização e a definição ocorrem inteiramente em um thread, e nenhum outro thread pode processá-lo antes que a inicialização seja concluída e a competição condicional termina na inicialização, o que é muito melhor do que fazê-lo posteriormente

3. Em vez de usar a instância std::mutex para sincronização, é melhor usar boost::shared_mutex para sincronização

Proteja estruturas de dados usando boost::shared_mutex

#include <map>
#include <string>
#include <mutex>
#include <boost/thread/shared_mutex.hpp>

class dns_entry;

class dns_cache
{
    
    
  std::map<std::string,dns_entry> entries;
  mutable boost::shared_mutex entry_mutex;
public:
  dns_entry find_entry(std::string const& domain) const
  {
    
    
    boost::shared_lock<boost::shared_mutex> lk(entry_mutex);  // 1 保护其共享和只读权限
    std::map<std::string,dns_entry>::const_iterator const it=
       entries.find(domain);
    return (it==entries.end())?dns_entry():it->second;
  }
  void update_or_add_entry(std::string const& domain,
                           dns_entry const& dns_details)
  {
    
    
    std::lock_guard<boost::shared_mutex> lk(entry_mutex);  // 2 独占访问权限
    entries[domain]=dns_details;
  }
};

4.
A biblioteca padrão C++ de bloqueio aninhado fornece a classe std::recursive_mutex

Os bloqueios aninhados geralmente são usados ​​em classes que podem ser acessadas simultaneamente por vários encadeamentos, portanto, eles têm um mutex para proteger os dados de seus membros. Cada função de membro público bloqueará o mutex, concluirá a função correspondente e desbloqueará o mutex

A diferença entre lock_guard e unique_lock em c++

  1. unique_lock pode realizar o bloqueio atrasado, ou seja, gerar o objeto unique_lock primeiro e, em seguida, chamar a função de bloqueio quando necessário, lock_guard executará automaticamente a operação de bloqueio quando o objeto for criado;
  2. unique_lock pode chamar a operação de desbloqueio quando necessário e lock_guard só pode desbloquear automaticamente após o fim do ciclo de vida do objeto;

Acho que você gosta

Origin blog.csdn.net/weixin_43367756/article/details/126392870
Recomendado
Clasificación