一文搞懂可重入和线程安全

简介: 一文搞懂可重入和线程安全

关于线程安全的概念,在之前写线程的时候提过:

一个函数被称为线程安全的,当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。
如果一个函数不是线程安全的,我们就说它是线程不安全的

而可重入的概念,第一次见还是在游双的《Linux高性能服务器编程》一书中:

inet_ntoa函数将网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。
但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。代码清单揭示了其不可重入性:
char *szValue1 = inet_ntoa("1.2.3.4");
char *szValue2 = inet_ntoa("10.194.71.60");
printf("address 1: %s\n", szValue1);
printf("address 2: %s\n", szValue2);

运行这段代码,得到的结果是:

address1: 10.194.71.60
address2: 10.194.71.60

所以,可重入与线程安全到底是什么,又有什么区别呢?

这里以Qt官方文档为蓝本,翻译一下:

一个线程安全的函数可以被多个线程同时调用,即使调用使用共享数据,因为对共享数据的所有引用都是序列化的(串行的)

一个可重入函数也可以被多个线程同时调用,但前提是每个调用都使用自己的数据。

因此,线程安全的函数总是可重入的,但可重入的函数并不总是线程安全的。

c++类通常是可重入的,这是因为它们只访问自己的成员数据。任何线程都可以调用可重入类实例中的成员函数,只要没有其他线程可以同时调用该类的同一个实例上的成员函数。例如,下面的Counter类是可重入的:

class Counter
 {
  public:
      Counter() { n = 0; }
      void increment() { ++n; }
      void decrement() { --n; }
      int value() const { return n; }
  private:
      int n;
 };

但却不是线程安全的,如果多个线程同时试图修改成员变量n,结果是未定义的,因为++或–操作并不是原子的。实际上,它们通常会扩展到三条机器指令:

将变量的值装入寄存器
增加或减少寄存器的值
将寄存器的值存储回主存

如果线程A和线程B同时加载变量的旧值,增加它们的寄存器,并存储它,它们最终会相互覆盖,变量只增加一次!

而下面这个例子是线程安全的:

class Counter
{
  public:
      Counter() { n = 0; }
      void increment() { QMutexLocker locker(&mutex); ++n; }
      void decrement() { QMutexLocker locker(&mutex); --n; }
      int value() const { QMutexLocker locker(&mutex); return n; }
  private:
      mutable QMutex mutex;
      int n;
};

因为即使多个线程同时修改变量n,有了互斥锁的控制,可以保证变量修改的原子性。

再来看一段《Qt中的C++技术》一书中关于这两个概念的描述:

可重入类(reentrant class)的不同对象可以被自由使用,即使它们处于不同的线程,但是这种类的同一个对象却不能够被多个线程同时使用。
而线程安全(thread-safe)类的对象可以被自由使用,即使该类的同一个对象被多个线程同时使用。
当一个函数满足以下条件时,我们称它是线程安全的(thread-safe):当多个线程同时调用该函数时,即使在不同线程中该函数访问了同一块内存,
这些访问也会安排为顺序进行的,使得每个线程中该函数的执行结果是确定的。如果一个类的所有成员函数都是线程安全的,我们称这个类是线程安全的。
对于线程安全的类,我们可以在多个线程中同时访问该类的一个对象,不用担心该对象的数据会被破坏。
对于类,一个稍宽松的条件是可重入(reentrant):如果多个线程能够安全地访问一个类的不同对象,我们称这个类是可重入的。
一般情况下,如果一个类不访问全局对象或者其他共享数据,该类就是可重入的。
而具有以下形态的类则是不可重入的:具有静态数据成员;某个或者某些成员函数的内部定义了静态局部变量;
某个数据成员是指针,而该类多个对象的这个指针指向同一块内存。

这也就解释了为什么inet_ntoa函数是不可重入的。

参考资料

[1] 游双.Linux高性能服务器编程[M].北京:机械工业出版社,2013.

[2] 张波.Qt中的C++技术[M].北京:电子工业出版社,2012.7

相关文章
|
11月前
|
人工智能 安全 机器人
无代码革命:10分钟打造企业专属数据库查询AI机器人
随着数字化转型加速,企业对高效智能交互解决方案的需求日益增长。阿里云AppFlow推出的AI助手产品,借助创新网页集成技术,助力企业打造专业数据库查询助手。本文详细介绍通过三步流程将AI助手转化为数据库交互工具的核心优势与操作指南,包括全场景适配、智能渲染引擎及零代码配置等三大技术突破。同时提供Web集成与企业微信集成方案,帮助企业实现便捷部署与安全管理,提升内外部用户体验。
972 12
无代码革命:10分钟打造企业专属数据库查询AI机器人
|
关系型数据库 MySQL Linux
文章对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据自身需求选择最合适的安装方法
【10月更文挑战第8天】本文介绍了在 CentOS 7 中通过编译源码安装 MySQL 数据库的详细步骤,包括准备工作、下载源码、编译安装、配置 MySQL 服务及登录设置等。同时,文章对比了编译源码安装与使用 RPM 包安装的优缺点,帮助读者根据自身需求选择最合适的安装方法。
628 4
|
11月前
|
运维 Ubuntu Linux
Linux重置root用户密码
本文详细介绍了Linux系统中root密码重置的核心技能,涵盖主流发行版如RHEL、CentOS、Debian、Ubuntu、Arch、openSUSE等的实操方法。内容包括通过GRUB引导编辑、单用户模式和Live CD救援三种方式重置密码的具体步骤,适配物理机、虚拟机及云服务器环境。文章分步解析了启动拦截、权限获取和密码重置三大阶段,并提供各发行版的实际操作代码示例,帮助管理员快速解决忘记root密码的问题。
|
存储 安全 Linux
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
1168 0
【C/C++ 可重入函数与不可重入函数】理解C/C++ 中函数的可重入性以及与线程安全性的关系
|
网络协议 Linux API
Linux网络编程:shutdown() 与 close() 函数详解:剖析 shutdown()、close() 函数的实现原理、参数说明和使用技巧
Linux网络编程:shutdown() 与 close() 函数详解:剖析 shutdown()、close() 函数的实现原理、参数说明和使用技巧
2186 0
|
存储 Java 应用服务中间件
Java规则引擎Drools急速入门
Java规则引擎Drools急速入门
11204 0
Java规则引擎Drools急速入门
|
移动开发 网络协议 算法
TCP中的粘包、拆包问题产生原因及解决方法
TCP中的粘包、拆包问题产生原因及解决方法
2152 0
TCP中的粘包、拆包问题产生原因及解决方法
|
测试技术 Python
分支覆盖 (Branch Coverage)
分支覆盖 (Branch Coverage) 是一种软件测试覆盖率评估方法,能够测量代码中每个分支的执行情况,即代码中每个条件语句 (if-else 语句) 的所有可能分支是否都被执行过。
4814 1
|
安全 JavaScript 前端开发
在Linux中生成随机数的各种方法
在计算机编程中,生成随机数是一项常见的任务,用于模拟实际情况、加密、游戏设计等。Linux系统提供了多种方法来生成随机数。本文将介绍Linux环境下生成随机数的几种常用方法,包括伪随机数生成器、/dev/random 和 /dev/urandom 设备、以及使用编程语言生成随机数。
1683 3
|
网络协议
通俗易懂理解三次握手、四次挥手(TCP)
这篇文章用通俗的语言解释了TCP协议中的三次握手和四次挥手过程,通过比喻和详细的状态变化描述,帮助读者理解建立和断开连接的原理和原因。
通俗易懂理解三次握手、四次挥手(TCP)