条件竞争
- 在即将析构一个对象时,从何而知此刻是否有别的线程正在执行该对象的成员函数?
- 如何保证在执行成员函数期间,对象不会在另一个线程被析构?
- 在调用某个对象的成员函数之前,如何得知这个对象还活着?它的析构函数会不会碰巧执行到一半?
线程安全类的定义
- 多个线程同时访问时,其表现出正确的行为。
- 无论操作系统如何调度这些线程,无论这些线程的执行顺序如何交织(interleaving)。
- 调用端代码无须额外的同步或其他协调动作。
创建类线程安全
安全的构造函数
- 不要在构造函数中注册任何回调
- 不要把this传递给其他对象
- 即使是最后一行也不能(派生类会先执行基类的构造函数,会导致实际上执行到最后一行,对象仍然还是个半成品)
一定要注册回调的话,使用二段式构造,创建新的init函数,由调用方来执行注册
安全的析构函数
很困难,没有办法任何办法保证。只能从使用角度,在析构的时候,没有任何一个人去使用类的成员函数来防止该问题。
安全的swap函数
一个函数如果要锁住相同类型的多个对象,为了保证始终按相同的顺序加锁,我们可以比较mutex对象的地址,始终先加锁地址较小的mutex。
当前c++11已经内置方法实现。
// 赋值操作符
Counter& Counter::operator=(const Counter& rhs) {
if (this == &rhs)
return *this;
// 锁定两个互斥锁,确保以相同的顺序获取锁,避免死锁
std::scoped_lock lock(mutex_, rhs.mutex_);
value_ = rhs.value_;
return *this;
}
// 获取当前值
int64_t Counter::value() const {
MutexLockGuard lock(mutex_);
return value_;
}
// 获取当前值并自增
int64_t Counter::getAndIncrease() {
MutexLockGuard lock(mutex_);
int64_t ret = value_++;
return ret;
}
使用智能指针加锁场景
当多个线程同时访问并修改同一个std::shared_ptr实例(例如,重置指针、增加或减少引用计数)时,需要加锁以防止数据竞争。
例如:
std::shared_ptr<int> ptr = std::make_shared<int>(10);
std::mutex mtx;
// Thread 1
{
std::lock_guard<std::mutex> lock(mtx);
ptr.reset(new int(20));
}
// Thread 2
{
std::lock_guard<std::mutex> lock(mtx);
ptr = std::make_shared<int>(30);
}
什么时候不需要加锁
- 局部 std::shared_ptr 实例:如果 std::shared_ptr 实例是局部的,并且不在多个线程之间共享,则不需要加锁。
- 只读访问:如果 std::shared_ptr 实例在多个线程中只读访问(每个线程只读取指向的对象,而不修改它),则不需要加锁,因为读取操作是线程安全的。
尽量减小临界区
排除一些IO操作,比如构造和析构函数。
要将globalPtr
原来指向的Foo
对象的销毁行为移出临界区,你可以使用一个临时的 shared_ptr
来保存 globalPtr
的旧值,并在临界区外进行销毁。以下是修改后的 write
函数:
void write()
{
std::shared_ptr<Foo> newPtr(new Foo); // 注意,对象的创建在临界区之外
std::shared_ptr<Foo> oldPtr; // 临时的shared_ptr,用于保存旧值
{
MutexLockGuard lock(mutex);
oldPtr = globalPtr; // 保存旧值
globalPtr = newPtr; // 更新globalPtr
} // 离开临界区
// oldPtr 在这里离开作用域,原来的 Foo 对象会在这里销毁
doit(newPtr);
}
在这个实现中,oldPtr
被用来保存 globalPtr
的旧值,而旧值的销毁行为发生在临界区之外。这确保了临界区内只进行必要的指针交换操作,而不涉及可能的对象销毁,从而提高了临界区的效率和安全性。