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


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

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

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

    inet_ntoa函数将网络字节序整数表示的IPv4地址转化为用点分十进制字符串表示的IPv4地址。
    但需要注意的是,该函数内部用一个静态变量存储转化结果,函数的返回值指向该静态内存,因此inet_ntoa是不可重入的。代码清单揭示了其不可重入性:
    
    • 1
    • 2
    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);
    
    • 1
    • 2
    • 3
    • 4

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

    address1: 10.194.71.60
    address2: 10.194.71.60
    
    • 1
    • 2

    所以,可重入与线程安全到底是什么,又有什么区别呢?
    这里以Qt官方文档为蓝本,翻译一下:
    在这里插入图片描述
    一个线程安全的函数可以被多个线程同时调用,即使调用使用共享数据,因为对共享数据的所有引用都是序列化的(串行的)
    一个可重入函数也可以被多个线程同时调用,但前提是每个调用都使用自己的数据。
    因此,线程安全的函数总是可重入的,但可重入的函数并不总是线程安全的。
    c++类通常是可重入的,这是因为它们只访问自己的成员数据。任何线程都可以调用可重入类实例中的成员函数,只要没有其他线程可以同时调用该类的同一个实例上的成员函数。例如,下面的Counter类是可重入的:

     class Counter
     {
      public:
          Counter() { n = 0; }
    
          void increment() { ++n; }
          void decrement() { --n; }
          int value() const { return n; }
    
      private:
          int n;
     };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    如果线程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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

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

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

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

    参考资料
    [1] 游双.Linux高性能服务器编程[M].北京:机械工业出版社,2013.
    [2] 张波.Qt中的C++技术[M].北京:电子工业出版社,2012.7

  • 相关阅读:
    Redis 中的过期删除策略和内存淘汰机制
    【Java高级技术】单元测试——概述和快速入门
    苹果电脑快捷键
    npm run dev报filemanager-webpack-plugin相关错误
    reactNative导入excel文件
    虚拟机桥接模式下没有无线网卡选项
    【MindSpore功能】昇腾310推理卡上跑MindSpore.ops中的算子,执行失败
    分布式文件系统
    Java 接口
    c++11 多线程支持 (std::async)
  • 原文地址:https://blog.csdn.net/gaoyuelon/article/details/126672977