二.共享数据的保护

文章介绍了C++中保护共享数据的方法,如使用std::mutex和std::lock_guard进行互斥访问,防止死锁的策略,包括std::lock、std::adopt_lock以及C++17的std::scoped_lock。还提到了std::unique_lock的灵活性和std::call_once用于初始化过程的线程安全,以及std::shared_mutex用于保护稀少更新的数据和std::recursive_mutex支持递归加锁的场景。


一、用互斥保护共享数据

1.std::mutex调用lock()加锁,unlock()解锁

2.std::lock_guard利用RAII机制保证发生异常时也能正常unlock

#include <list>
#include <mutex>
#include <algorithm>
#include <iostream>
#include <thread>
std::list<int> some_list;
std::mutex some_mutex;

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

int main()
{
    std::thread t(add_to_list,42); 
    list_contains(42);
    t.join();
    std::cin.get();
}

二、防范死锁

1.std::lock同时锁住多个互斥

2.std::adopt_lock指明互斥已被锁住,不得在构造函数内试图另行加锁

代码如下(示例):

#include <mutex>

class some_big_object
{};

void swap(some_big_object& lhs,some_big_object& rhs)
{}

class X
{
private:
    some_big_object some_detail;
    mutable 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);
        std::lock_guard<std::mutex> lock_a(lhs.m,std::adopt_lock);
        std::lock_guard<std::mutex> lock_b(rhs.m,std::adopt_lock);
        swap(lhs.some_detail,rhs.some_detail);
    }
};

int main()
{}

3.C++17实现了std::scoped_lock模板,是std::lock模板的RAII实现

    friend void swap(X& lhs, X& rhs)
    {
        if(&lhs==&rhs)
            return;
        std::scoped_lock(lhs.m,rhs.m);
        swap(lhs.some_detail,rhs.some_detail);
    }

4.防范死锁的准则

1.避免嵌套锁

假如已经持有锁就应该避免获取第二个锁,万一有确有需求获取多个锁,应采用std::lock模板,借单独的调用动作一次获取全部锁来避免死锁。

2.一旦持锁,就须避免调用用户提供的程序接口

我们已经持有锁,再去调用用户实现的接口,恰好用户接口内部试图获取锁,就可能发生嵌套锁,不过有时这样是在所难免,所以我们又需要另一个新的准则。

3.依从固定顺序获取锁

如果多个锁是必要的,又无法通过std::lock模板在一部操作中完成,我们只能退而求其次,在每个线程内都依从固定的顺序获取这些锁。

5.std::unique_lock灵活加锁

与std::lock_guard比较,std::unique_lock即能很好利用RAII机制,又更加的灵活,可以根据需要,在std::unique_lock对象构造时对mutex对象加锁,也可以在std::unique_lock构造时使mutex处于无锁状态,之后调用std::unique_lock对象的lock()函数择机加锁,也可以接管已经加过锁的mutex,且允许在std::unique_lock对象销毁前调用std::unique_lock的成员函数unlock()解锁。

1.std::unique_lock第二个参数使用默认值

std::mutex m;
void f()
{
 std::unique_lock<std::mutex> lk(m);//构造时加锁
 //.....处理共享数据的事务
 lk.unlock();//解锁
 //.....处理非共享数据的事务
 lk.lock();//加锁
 //.....处理共享数据的事务
}

2.std::unique_lock第二个参数传入std::adopt_lock指明互斥已经lock过,std::unique_lock据此接收锁的归属权,不得在构造函数中试图另行加锁。

std::mutex m;
void f()
{
 m.lock();//互斥已经加锁
 std::unique_lock<std::mutex> lk(m,std::adopt_lock);//指明互斥已经加过锁
 //.....
 lk.unlock();
}

3.std::unique_lock第二个参数传入std::defer_lock指明互斥处于无锁状态,等以后有需要的时候再调用std::unique_lock对象的lock()加锁

std::mutex m;
void f()
{
 std::unique_lock<std::mutex> lk(m,std::defer_lock);//构造时无锁状态
 lk.lock();//延后加锁
 //...
 lk.unlock();
 //....
}

4.在不同作用域转移互斥所有权,std::unique_lock可移动不可复制

准许函数锁定互斥,然后将互斥所有权转移给函数调用者,好让他在一个锁的作用下执行其它操作。

std::mutex m;
std::unique_lock<std::mutex> f()
{
	std::unique_lock<std::mutex> lk(m);
	return lk;
}
void hold()
{
	std::unique_lock<std::mutex> lk(f());
    std::cout << __FUNCTION__ << std::endl; 
}

6.按适当粒度加锁

只在必要的操作上加锁,避免必要操作外加锁

三.保护共享数据的其它工具

1.在初始化过程中保护共享数据,std::call_once()

1.类的数据成员线程安全的延迟初始化

#include <mutex>

struct connection_info
{};

struct data_packet
{};

struct connection_handle
{
    void send_data(data_packet const&)
    {}
    data_packet receive_data()
    {
        return data_packet();
    }
};

struct remote_connection_manager
{
    connection_handle open(connection_info const&)
    {
        return connection_handle();
    }
} connection_manager;


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)
    {
        std::call_once(connection_init_flag,&X::open_connection,this);
        connection.send_data(data);
    }
    data_packet receive_data()
    {
        std::call_once(connection_init_flag,&X::open_connection,this);
        return connection.receive_data();
    }
};

int main()
{}

2.单例模式

1.new生成对象指针,使用std::call_once保证只执行一次初始化
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>
#include <chrono> 

class X
{
private:
	X() {};
	static void init() 
	{
		static huishou hs;
		x = new X();
		std::cout << "调用std::call_once的线程ID: " << std::this_thread::get_id() << std::endl;
	}
public:
	static X* getMe()
	{
		std::call_once(X::init_flag,X::init);
		return x;
	}
	void print()
	{
		std::lock_guard<std::mutex> lk(m);
		std::cout << "单例对象地址:"<< this << " 当前线程ID: " << std::this_thread::get_id() << std::endl;
	}

private:
	class huishou
	{
	public:
		huishou() {};
		~huishou() {
			delete X::x;
		};

	};
	static X* x;
	static std::once_flag init_flag;
    std::mutex m;	
};

X* X::x = nullptr;
std::once_flag X::init_flag;

void f()
{
	X* x = X::getMe();
	x->print();
}
int main()
{
	std::thread t1(f);
	std::thread t2(f);
	std::thread t3(f);
	std::thread t4(f);
	t1.join();
	t2.join();
	t3.join();
	t4.join();
}
2.C++11静态局部变量初始化,只会在某一个线程单独发生
#include <iostream>
#include <thread>
#include <memory>
#include <mutex>
#include <chrono> 

class X
{
private:
	X() {};
public:
	static X& getMe()
	{
		static X x;
		return x;
	}
	void print()
	{
		std::lock_guard<std::mutex> lk(m);
		std::cout << "单例对象地址:"<< this << " 当前线程ID: " << std::this_thread::get_id() << std::endl;
	}
private:
	std::mutex m;	
};

void f()
{
	X& x = X::getMe();
	x.print();
}
int main()
{
	std::thread t1(f);
	std::thread t2(f);
	std::thread t3(f);
	std::thread t4(f);
	t1.join();
	t2.join();
	t3.join();
	t4.join();
}

2.C++17用std::shared_mutex保护甚少更新的数据

因为程序性能由下列因素共同决定:处理器的数目,还有读、写线程上的相对工作负荷。多线程令复杂度增加,为了确保性能可以同样随之提升,一个重要的方法是在目标系统上对代码进行性能剖析。
除了std:mutex,我们也可以利用std::shared_mutex 的实例施加同步操作。更新操作可用 std::lock_ guard<std::shared_mutex>和 std:unique lock<std:shared_mutex>锁定,代替对应的std:mutex特化。它们与 std::mutex一样,都保证了访问的排他性质。对于那些无须更新数据结构的线程,可以另行改用共享锁std:shared lock<std.:shared mutex>实现共享访问。C++14 引入了共享锁的类模板,其工作原理是RAII过程,使用方式则与std:unique_ lock 相同,只不过多个线程能够同时锁住同一个 std.:shared_mutex。共享锁仅有一个限制,即假设它已被某些线程所持有,若别的线程试图获取排他锁,就会发生阻塞,直到那些线程全都释放该共享锁。反之,如果任一线程持有排他锁,那么其他线程全都无法获取共享锁或排他锁,直到持锁线程将排他锁释放为止。

#include <iostream>
#include <mutex>  // 对于 std::unique_lock
#include <shared_mutex>
#include <thread>
 
class ThreadSafeCounter {
 public:
  ThreadSafeCounter() = default;
 
  // 多个线程/读者能同时读计数器的值。
  unsigned int get() const {
    std::shared_lock<std::shared_mutex> lock(mutex_);
    return value_;
  }
 
  // 只有一个线程/写者能增加/写线程的值。
  void increment() {
    std::unique_lock<std::shared_mutex> lock(mutex_);
    value_++;
  }
 
  // 只有一个线程/写者能重置/写线程的值。
  void reset() {
    std::unique_lock<std::shared_mutex> lock(mutex_);
    value_ = 0;
  }
 
 private:
  mutable std::shared_mutex mutex_;
  unsigned int value_ = 0;
};
 
int main() {
  ThreadSafeCounter counter;
 
  auto increment_and_print = [&counter]() {
    for (int i = 0; i < 3; i++) {
      counter.increment();
      std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';
 
      // 注意:写入 std::cout 实际上也要由另一互斥同步。省略它以保持示例简洁。
    }
  };
 
  std::thread thread1(increment_and_print);
  std::thread thread2(increment_and_print);
 
  thread1.join();
  thread2.join();
}

3.使用std:recursive_mutex递归加锁

假如线程已经持有某个std:mutex 实例,试图再次对其重新加锁就会出错,将导致未定义行为。但在某些场景中,确有需要让线程在同一互斥上多次重复加锁,而无须解锁。C++标准库为此提供了std:recursive_mutex其工作方式与std::mutex 相似,不同之处是,其允许同一线程对某互斥的同一实例多次加锁。我们必须先释放全部的锁,才可以让另一个线程锁住该互斥。例如若我们对它调用了3次lock(),就必须调用3次unlock()。只要正确地使用std:lock guard<std.recursive_ mutex>和 std::unique lock<std:recursive_mutex>它们便会处理好递归锁的余下细节。

#include <iostream>
#include <thread>
#include <mutex>
 
class X {
    std::recursive_mutex m;
    std::string shared;
  public:
    void fun1() {
      std::lock_guard<std::recursive_mutex> lk(m);
      shared = "fun1";
      std::cout << "in fun1, shared variable is now " << shared << '\n';
    }
    void fun2() {
      std::lock_guard<std::recursive_mutex> lk(m);
      shared = "fun2";
      std::cout << "in fun2, shared variable is now " << shared << '\n';
      fun1(); // 递归锁在此处变得有用
      std::cout << "back in fun2, shared variable is " << shared << '\n';
    };
};
 
int main() 
{
    X x;
    std::thread t1(&X::fun1, &x);
    std::thread t2(&X::fun2, &x);
    t1.join();
    t2.join();
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值