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

2024-08-31 05:28

本文主要是介绍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副本的生成,导致其引用计数原子递增,从而保证在使用该副本期间,它所管理的对象不会被释放。

这篇关于Modern C++——无所有权指针的安全性保障的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1123015

相关文章

C++ move 的作用详解及陷阱最佳实践

《C++move的作用详解及陷阱最佳实践》文章详细介绍了C++中的`std::move`函数的作用,包括为什么需要它、它的本质、典型使用场景、以及一些常见陷阱和最佳实践,感兴趣的朋友跟随小编一起看... 目录C++ move 的作用详解一、一句话总结二、为什么需要 move?C++98/03 的痛点⚡C++

详解C++ 存储二进制数据容器的几种方法

《详解C++存储二进制数据容器的几种方法》本文主要介绍了详解C++存储二进制数据容器,包括std::vector、std::array、std::string、std::bitset和std::ve... 目录1.std::vector<uint8_t>(最常用)特点:适用场景:示例:2.std::arra

C++构造函数中explicit详解

《C++构造函数中explicit详解》explicit关键字用于修饰单参数构造函数或可以看作单参数的构造函数,阻止编译器进行隐式类型转换或拷贝初始化,本文就来介绍explicit的使用,感兴趣的可以... 目录1. 什么是explicit2. 隐式转换的问题3.explicit的使用示例基本用法多参数构造

C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解

《C++,C#,Rust,Go,Java,Python,JavaScript的性能对比全面讲解》:本文主要介绍C++,C#,Rust,Go,Java,Python,JavaScript性能对比全面... 目录编程语言性能对比、核心优势与最佳使用场景性能对比表格C++C#RustGoJavapythonjav

C++打印 vector的几种方法小结

《C++打印vector的几种方法小结》本文介绍了C++中遍历vector的几种方法,包括使用迭代器、auto关键字、typedef、计数器以及C++11引入的范围基础循环,具有一定的参考价值,感兴... 目录1. 使用迭代器2. 使用 auto (C++11) / typedef / type alias

C++ scoped_ptr 和 unique_ptr对比分析

《C++scoped_ptr和unique_ptr对比分析》本文介绍了C++中的`scoped_ptr`和`unique_ptr`,详细比较了它们的特性、使用场景以及现代C++推荐的使用`uni... 目录1. scoped_ptr基本特性主要特点2. unique_ptr基本用法3. 主要区别对比4. u

C++11中的包装器实战案例

《C++11中的包装器实战案例》本文给大家介绍C++11中的包装器实战案例,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录引言1.std::function1.1.什么是std::function1.2.核心用法1.2.1.包装普通函数1.2.

C++多线程开发环境配置方法

《C++多线程开发环境配置方法》文章详细介绍了如何在Windows上安装MinGW-w64和VSCode,并配置环境变量和编译任务,使用VSCode创建一个C++多线程测试项目,并通过配置tasks.... 目录下载安装 MinGW-w64下载安装VS code创建测试项目配置编译任务创建 tasks.js

C++ 多态性实战之何时使用 virtual 和 override的问题解析

《C++多态性实战之何时使用virtual和override的问题解析》在面向对象编程中,多态是一个核心概念,很多开发者在遇到override编译错误时,不清楚是否需要将基类函数声明为virt... 目录C++ 多态性实战:何时使用 virtual 和 override?引言问题场景判断是否需要多态的三个关

C++简单日志系统实现代码示例

《C++简单日志系统实现代码示例》日志系统是成熟软件中的一个重要组成部分,其记录软件的使用和运行行为,方便事后进行故障分析、数据统计等,:本文主要介绍C++简单日志系统实现的相关资料,文中通过代码... 目录前言Util.hppLevel.hppLogMsg.hppFormat.hppSink.hppBuf