文章目录
一、用互斥保护共享数据
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();
}
文章介绍了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支持递归加锁的场景。
1433

被折叠的 条评论
为什么被折叠?



