C++ 스레드 간에 데이터 공유

1. C++에서 뮤텍스 사용하기

C++ 표준 라이브러리는 뮤텍스에 대한 RAII 구문 템플릿 클래스 std::lack_guard를 제공합니다. 이는 구성 중에 잠긴 뮤텍스를 제공하고 파괴 중에 잠금을 해제하여 잠긴 뮤텍스를 보장합니다. 뮤텍스는 항상 적절하게 잠금 해제됩니다.

뮤텍스로 목록 보호

#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();
}

공유 데이터를 보호하기 위해 코드를 신중하게 구성합니다.
실수로 보호된 데이터에 대한 참조를 전달합니다.

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 在无保护的情况下访问保护数据
}

이 코드의 문제는 전혀 보호하지 않는다는 것입니다. 액세스 가능한 모든 데이터 구조 코드를 상호 배타적으로 표시할 뿐입니다. 함수 foo()에서 unprotected->do_something()을 호출하는 코드가 상호 배타적으로 표시되지 않았습니다.

이 경우 C++ 스레딩 라이브러리는 어떠한 도움도 제공할 수 없으며 올바른 뮤텍스를 사용하여 데이터를 보호하는 것은 프로그래머의 몫입니다.

낙관적인 관점에서 볼 때 여전히 갈 길이 있습니다. 함수 반환 값이든, 외부에서 볼 수 있는 메모리에 저장되었거나, 사용자 제공 함수.

2. 조건부 경쟁을 해결하기 위한 옵션

옵션 1: 참조를 전달
옵션 2: 예외를 발생시키지 않는 복사 생성자 또는 이동 생성자
옵션 3: 팝된 값에 대한 포인터 반환

다음은 예입니다.

#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. 교착 상태

std::lock - 한 번에 여러 개(2개 이상)의 뮤텍스를 잠글 수 있으며 부작용(교착 상태 위험)이 없습니다.

// 这里的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);
  }
};

교착 상태 회피
1. 중첩된 잠금을 피하고 std::lock을 사용하여 여러 뮤텍스를 잠급니다.
2. 잠금을 유지하는 동안 사용자 제공 코드를 호출하지 않습니다
. 3. 고정 순서를 사용하여 잠금을 획득합니다.

1. 계층적 잠금을 사용하여

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()는 원활하게 실행되지 않습니다.우선 other_mutex⑩을 잠그고 이 mutex의 레벨 값은 100⑦뿐입니다. 이는 초저수준 데이터(ultra-low-level data)가 보호되었음을 의미합니다. other_stuff()가 high_level_func()⑧을 호출하면 계층 구조가 위반됩니다.

2. 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과 std::lock_guard

std::unique_lock은 더 많은 공간을 차지하고 std::lock_guard보다 약간 느립니다. 유연성을 위해 지불해야 할 대가가 있으며 그 대가는 뮤텍스 없이 std::unique_lock 인스턴스를 허용하는 것입니다. 정보는 이미 저장되어 있고 업데이트됩니다.

1. 교환 작업에서 std::lock() 및 std::unique_lock 사용

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은 std::adopt_lock 매개변수로 추가 가능
① std::try_to_lock을 추가하면
불필요한 대기를 피할 수 있으며 현재 뮤텍스를 잠글 수 있는지 여부를 판단합니다. 잠글 수 없는 경우 다른 코드를 먼저 실행할 수 있습니다.

#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;
}

② 매개변수 std::defer_lock 추가는
일시적으로 잠그지 않고 수동으로 잠그는 것을 의미하지만 사용하기 전에 잠그는 것은 허용되지 않습니다. 일반적으로 unique_lock 멤버 함수와 함께 사용

#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;
}

위의 try_to_lock 파라미터와 같은 기능을 하는 멤버 함수인 try_lock도 있는데, 현재 잠금을 사용할 수 있는지 여부를 결정하고, 그렇지 않으면 다른 코드를 실행하고 false를 반환하고, 그렇다면 잠그고 true를 반환합니다. 코드는 다음과 같습니다. 다음과 같이:

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 {
    
    
			// 处理一些没有共享内存的代码
		}
	}
}

해제 함수는 unique_lock과 뮤텍스 개체 간의 연결을 해제하고 원래 뮤텍스 개체의 포인터를 반환합니다. 이전 뮤텍스가 잠긴 경우 나중에 수동으로 잠금을 해제해야 합니다. 코드는 다음과 같습니다.

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. 다른 도메인에서 뮤텍스 소유권 이전

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. 공유 데이터 보호를 위한 대체 설비

1. 조건부 레이스 처리

C++ 표준 라이브러리는 조건부 경쟁을 처리하기 위해 std::once_flag 및 std::call_once를 제공합니다.
뮤텍스를 잠그고 포인터를 명시적으로 확인하는 대신 각 스레드는 std::call_once만 사용하면 되고 std::call_once의 끝에서 포인터가 다른 스레드에 의해 초기화되었음을 안전하게 알 수 있습니다.
특히 초기화가 완료된 경우 std::call_once를 사용하면 명시적으로 뮤텍스를 사용하는 것보다 적은 리소스를 사용합니다.

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. std::call_once를 클래스 멤버의 지연 초기화로 사용(스레드 안전성)

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();
  }
};

다음은 전역 인스턴스가 하나만 필요한 std::call_once의 대안입니다.

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

멀티스레딩은 데이터 경쟁에 대한 걱정 없이 안전하게 get_my_class_instance()① 함수를 호출할 수 있습니다.

C++11 표준에서: 초기화 및 정의는 전적으로 하나의 스레드에서 발생하며 초기화가 완료되기 전에는 다른 스레드가 이를 처리할 수 없으며 조건부 경쟁은 초기화 시 종료되므로 나중에 수행하는 것보다 훨씬 낫습니다.

3. 동기화를 위해 std::mutex 인스턴스를 사용하는 대신 동기화를 위해 boost::shared_mutex를 사용하는 것이 좋습니다.

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. 중첩 잠금
C++ 표준 라이브러리는 std::recursive_mutex 클래스를 제공합니다.

중첩 잠금은 일반적으로 여러 스레드에서 동시에 액세스할 수 있는 클래스에서 사용되므로 멤버 데이터를 보호하기 위한 뮤텍스가 있습니다. 각 공용 멤버 함수는 뮤텍스를 잠근 다음 해당 함수를 완료한 다음 뮤텍스를 잠금 해제합니다.

C++에서 lock_guard와 unique_lock의 차이점

  1. unique_lock은 지연된 잠금을 실현할 수 있습니다. 즉, 먼저 unique_lock 개체를 생성한 다음 필요한 경우 잠금 기능을 호출합니다. lock_guard는 개체가 생성될 때 자동으로 잠금 작업을 수행합니다.
  2. unique_lock은 필요한 경우 잠금 해제 작업을 호출할 수 있으며 lock_guard는 개체 수명 주기가 끝난 후에만 자동으로 잠금을 해제할 수 있습니다.

Guess you like

Origin blog.csdn.net/weixin_43367756/article/details/126392870