欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 建筑 > Modern C++——无所有权指针的安全性保障

Modern C++——无所有权指针的安全性保障

2024/10/26 15:22:59 来源:https://blog.csdn.net/breaksoftware/article/details/141559682  浏览:    关键词:Modern C++——无所有权指针的安全性保障

大纲

  • C++98中的实现
  • Modern C++的实现
  • 总结

C++98中的实现

在C++98标准中,当涉及到多线程环境时,对共享资源的访问需要特别小心以避免数据竞争(data race)问题。数据竞争是指两个或多个线程同时访问同一内存位置,且至少有一个线程正在修改该内存位置,同时又没有适当的同步机制(如互斥锁、信号量等)来协调这些访问的情况。

在C++98中,标准库本身并不直接支持多线程编程(尽管许多编译器和平台提供了扩展支持),但是理解和遵循多线程编程的最佳实践仍然是非常重要的。对于指针来说,当我们在多线程环境中释放了一个指针所指向的内存后,如果之后不将该指针设置为NULL(或现代C++中的nullptr,但C++98标准中尚未引入nullptr),并且没有适当的同步机制来保护这个指针的访问,就可能发生数据竞争。

这里是一个简化的例子来说明这个问题:

假设有两个线程,线程A和线程B,它们共享一个指针ptr

// 假设这是全局或共享的指针
int* ptr = new int(10);
// 线程A的函数
void threadAFunc() {// 假设这里线程A执行了一些操作,然后释放了ptr指向的内存delete ptr;ptr = NULL; 
}
// 线程B的函数
void threadBFunc() {// 线程B尝试访问ptr指向的内存if (ptr != NULL) { // 这里的检查在ptr已经被释放且未设为NULL时是无用的// 假设此时ptr被设置为NULL了,后续使用就会出错int* temp = ptr;int value = *temp; // 尝试解引用悬空指针,导致未定义行为}
}

因为是多线程执行,又没有使用同步机制,导致各个线程的执行流程不能保证。

当发生如下执行顺序时,上述代码就会发生Data Race问题:

  1. threadAFunc的delete ptr
  2. threadBFunc的 if (ptr != NULL)。测试该指针所指空间已经为threadAFunc释放,但是ptr = NULL还没来的及执行。
  3. threadAFunc的ptr = NULL。(这一步可能有可无,因为指针已经释放,后续threadBFunc执行一定会出错)
  4. threadBFunc的int* temp = ptr;int value = *temp;

所以上述代码的问题是delete ptr;ptr = NULL;这两步如何“原子化”。我们可以通过锁的方式来实现

// 假设这是全局或共享的指针
int* ptr = new int(10);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;// 线程A的函数
void* threadAFunc(void* arg) {pthread_mutex_lock(&mutex);// 假设这里线程A执行了一些操作,然后释放了ptr指向的内存delete ptr;ptr = NULL;pthread_mutex_unlock(&mutex);return NULL;
}// 线程B的函数
void* threadBFunc(void* arg) {pthread_mutex_lock(&mutex);// 线程B尝试访问ptr指向的内存if (ptr != NULL) {int* temp = ptr;int value = *temp; // 尝试解引用悬空指针,导致未定义行为std::cout << "Value: " << value << std::endl;}pthread_mutex_unlock(&mutex);return NULL;
}

Modern C++的实现

上述代码看起来很有C的风格。而且从形式上,ptr 和mutex 是强绑定的。这种设计非常容易因为使用不当导致问题出现(比如某次使用ptr忘记使用mutex来保护)。

Modern C++则引入了shared_ptr和weak_ptr来解决这个问题。

我们在《Modern C++——共享所有权指针保证内部对象析构安全的原因分析》中分析了在多线程下,shared_ptr释放内部对象需要依赖编译器的构造和析构排布来保证安全。但是有些场景下,我们需要让对象提前失效(如上述代码),而不是依赖对象的析构。这个时候weak_ptr就派上用场了。

void threadAFunc(std::shared_ptr<Data>& data) {std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些延迟data.reset(); // 销毁数据(实际不一定在此销毁)std::cout << "Data has been reset\n";
}void threadBFunc(std::weak_ptr<Data> weakData) {std::this_thread::sleep_for(std::chrono::seconds(2)); // 模拟一些延迟if (auto data = weakData.lock()) {data->process();} else {std::cout << "Data no longer exists\n";}
}int main() {std::shared_ptr<Data> data = std::make_shared<Data>();std::weak_ptr<Data> weakData = data;std::thread threadA(threadAFunc, std::ref(data));std::thread threadB(threadBFunc, weakData);threadA.join();threadB.join();return 0;
}

threadAFunc使用shared_ptr的引用,所以并不会增加它的引用计数。这样在threadAFunc中,reset操作会释放其内部管理的对象。(实际并不一定是在threadAFunc中释放的。后续会对此进行分析。)

threadBFunc中,如果weak_ptr管理的对象指针没有被释放,则lock操作会获得一个shared_ptr;否则将返回一个空指针。

现在的问题是if (auto data = weakData.lock())data->process();之间,threadAFunc会不会进行释放操作。

  1. threadBFunc的if (auto data = weakData.lock())
  2. threadAFunc的data.reset()
  3. threadBFunc的data->process()

这个问题的答案是:上述流程完全正常,且不会出现多线程安全问题。因为threadAFunc虽然执行了reset,但是并没有释放管理的对象。

这是因为weak_ptr和shared_ptr在底层共享了一个计数器指针。而weak_ptr的lock操作实际没有做任何锁的操作,而是返回了一个shared_ptr的副本。

      shared_ptr<_Tp>lock() const noexcept{ return shared_ptr<_Tp>(*this, std::nothrow); }};

这个副本的生成会导致weak_ptr和shared_ptr在底层共享的计数器原子递增。这样即使后续shared_ptr::reset被调用,实际它只是将计数器减少到1,而没有减少到0,于是reset操作并不会真正释放对象。

	  if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) // 前值不是1,所以不会进入[[__unlikely__]]{_M_release_last_use_cold();return;}

总结

  • weak_ptr::lock没有任何原子操作,只是会生成一个shared_ptr副本。
  • 因为shared_ptr副本的生成,导致其引用计数原子递增,从而保证在使用该副本期间,它所管理的对象不会被释放。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com