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++ 中的 if-constexpr语法和作用

《C++中的if-constexpr语法和作用》if-constexpr语法是C++17引入的新语法特性,也被称为常量if表达式或静态if(staticif),:本文主要介绍C++中的if-c... 目录1 if-constexpr 语法1.1 基本语法1.2 扩展说明1.2.1 条件表达式1.2.2 fa

C++中::SHCreateDirectoryEx函数使用方法

《C++中::SHCreateDirectoryEx函数使用方法》::SHCreateDirectoryEx用于创建多级目录,类似于mkdir-p命令,本文主要介绍了C++中::SHCreateDir... 目录1. 函数原型与依赖项2. 基本使用示例示例 1:创建单层目录示例 2:创建多级目录3. 关键注

C++从序列容器中删除元素的四种方法

《C++从序列容器中删除元素的四种方法》删除元素的方法在序列容器和关联容器之间是非常不同的,在序列容器中,vector和string是最常用的,但这里也会介绍deque和list以供全面了解,尽管在一... 目录一、简介二、移除给定位置的元素三、移除与某个值相等的元素3.1、序列容器vector、deque

C++常见容器获取头元素的方法大全

《C++常见容器获取头元素的方法大全》在C++编程中,容器是存储和管理数据集合的重要工具,不同的容器提供了不同的接口来访问和操作其中的元素,获取容器的头元素(即第一个元素)是常见的操作之一,本文将详细... 目录一、std::vector二、std::list三、std::deque四、std::forwa

C++字符串提取和分割的多种方法

《C++字符串提取和分割的多种方法》在C++编程中,字符串处理是一个常见的任务,尤其是在需要从字符串中提取特定数据时,本文将详细探讨如何使用C++标准库中的工具来提取和分割字符串,并分析不同方法的适用... 目录1. 字符串提取的基本方法1.1 使用 std::istringstream 和 >> 操作符示

C++原地删除有序数组重复项的N种方法

《C++原地删除有序数组重复项的N种方法》给定一个排序数组,你需要在原地删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度,不要使用额外的数组空间,你必须在原地修改输入数组并在使用O(... 目录一、问题二、问题分析三、算法实现四、问题变体:最多保留两次五、分析和代码实现5.1、问题分析5.

C++ 各种map特点对比分析

《C++各种map特点对比分析》文章比较了C++中不同类型的map(如std::map,std::unordered_map,std::multimap,std::unordered_multima... 目录特点比较C++ 示例代码 ​​​​​​代码解释特点比较1. std::map底层实现:基于红黑

C++中函数模板与类模板的简单使用及区别介绍

《C++中函数模板与类模板的简单使用及区别介绍》这篇文章介绍了C++中的模板机制,包括函数模板和类模板的概念、语法和实际应用,函数模板通过类型参数实现泛型操作,而类模板允许创建可处理多种数据类型的类,... 目录一、函数模板定义语法真实示例二、类模板三、关键区别四、注意事项 ‌在C++中,模板是实现泛型编程

利用Python和C++解析gltf文件的示例详解

《利用Python和C++解析gltf文件的示例详解》gltf,全称是GLTransmissionFormat,是一种开放的3D文件格式,Python和C++是两个非常强大的工具,下面我们就来看看如何... 目录什么是gltf文件选择语言的原因安装必要的库解析gltf文件的步骤1. 读取gltf文件2. 提

C++快速排序超详细讲解

《C++快速排序超详细讲解》快速排序是一种高效的排序算法,通过分治法将数组划分为两部分,递归排序,直到整个数组有序,通过代码解析和示例,详细解释了快速排序的工作原理和实现过程,需要的朋友可以参考下... 目录一、快速排序原理二、快速排序标准代码三、代码解析四、使用while循环的快速排序1.代码代码1.由快