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++中全局变量和局部变量的区别

《C++中全局变量和局部变量的区别》本文主要介绍了C++中全局变量和局部变量的区别,全局变量和局部变量在作用域和生命周期上有显著的区别,下面就来介绍一下,感兴趣的可以了解一下... 目录一、全局变量定义生命周期存储位置代码示例输出二、局部变量定义生命周期存储位置代码示例输出三、全局变量和局部变量的区别作用域

C++中assign函数的使用

《C++中assign函数的使用》在C++标准模板库中,std::list等容器都提供了assign成员函数,它比操作符更灵活,支持多种初始化方式,下面就来介绍一下assign的用法,具有一定的参考价... 目录​1.assign的基本功能​​语法​2. 具体用法示例​​​(1) 填充n个相同值​​(2)

c++ 类成员变量默认初始值的实现

《c++类成员变量默认初始值的实现》本文主要介绍了c++类成员变量默认初始值,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录C++类成员变量初始化c++类的变量的初始化在C++中,如果使用类成员变量时未给定其初始值,那么它将被

C++中NULL与nullptr的区别小结

《C++中NULL与nullptr的区别小结》本文介绍了C++编程中NULL与nullptr的区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编... 目录C++98空值——NULLC++11空值——nullptr区别对比示例 C++98空值——NUL

C++ Log4cpp跨平台日志库的使用小结

《C++Log4cpp跨平台日志库的使用小结》Log4cpp是c++类库,本文详细介绍了C++日志库log4cpp的使用方法,及设置日志输出格式和优先级,具有一定的参考价值,感兴趣的可以了解一下... 目录一、介绍1. log4cpp的日志方式2.设置日志输出的格式3. 设置日志的输出优先级二、Window

从入门到精通C++11 <chrono> 库特性

《从入门到精通C++11<chrono>库特性》chrono库是C++11中一个非常强大和实用的库,它为时间处理提供了丰富的功能和类型安全的接口,通过本文的介绍,我们了解了chrono库的基本概念... 目录一、引言1.1 为什么需要<chrono>库1.2<chrono>库的基本概念二、时间段(Durat

C++20管道运算符的实现示例

《C++20管道运算符的实现示例》本文简要介绍C++20管道运算符的使用与实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录标准库的管道运算符使用自己实现类似的管道运算符我们不打算介绍太多,因为它实际属于c++20最为重要的

Visual Studio 2022 编译C++20代码的图文步骤

《VisualStudio2022编译C++20代码的图文步骤》在VisualStudio中启用C++20import功能,需设置语言标准为ISOC++20,开启扫描源查找模块依赖及实验性标... 默认创建Visual Studio桌面控制台项目代码包含C++20的import方法。右键项目的属性:

c++中的set容器介绍及操作大全

《c++中的set容器介绍及操作大全》:本文主要介绍c++中的set容器介绍及操作大全,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录​​一、核心特性​​️ ​​二、基本操作​​​​1. 初始化与赋值​​​​2. 增删查操作​​​​3. 遍历方

解析C++11 static_assert及与Boost库的关联从入门到精通

《解析C++11static_assert及与Boost库的关联从入门到精通》static_assert是C++中强大的编译时验证工具,它能够在编译阶段拦截不符合预期的类型或值,增强代码的健壮性,通... 目录一、背景知识:传统断言方法的局限性1.1 assert宏1.2 #error指令1.3 第三方解决