多线程编程中的有状态(stateful)和无状态(stateless)以及可重入、不可重入

本文详细解析了线程安全与可重入的概念,包括无状态与有状态、可重入与不可重入的区别,并探讨了如何编写线程安全的函数及常见线程不安全函数的类型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    要写出线程安全的类和函数,有状态、无状态,可重入、不可重入这四个概念绕不开。什么是线程安全的类和函数呢?就是可以被多个线程调用而不会出现数据的错乱的类的对象和函数。导致线程不安全的根本原因是函数或类对象中使用了共享数据(类静态成员变量、全局变量等),由于没有对这些共享数据进行同步操作而导致数据错乱。根据这个原因就把函数或类分成下边两种情况:

1.无状态的

    这类函数和类对象不包含任何其他作用域中的变量或者对象,计算过程中的临时状态仅存在于自己的线程栈上的局部变量中,并且只能由当前的线程访问,不会受到其他线程的影响,这些无状态的类对象和函数时线程安全的。那么相对的就属于有状态的。

 非线程安全例子:

       1: 函数中引用了全局可变变量或者对象。并且函数的运行状态依赖该变量当前状态的影响,此时每次的运行结果是不确定的,受到其他线程的影响,是非线程安全的。

       2: 函数中引用或者返回了全局静态变量,局部静态变量,类静态变量,因为静态变量只会初始化一次,他的状态会受到其他线程的影响。

       3: 如果一个类中含有可变的静态变量,那么此类也是非线程安全的,当我们在不同的线程中定义了该类的对象时(其实不用定义该类的对象也可),如果在线程中改变了该类的静态成员变量,那么也会造成数据的不一致。

      4:调用了线程不安全的函数,那么该函数挥着类也是非线程安全的。

二  可重入函数

 

     当一个函数在被一个线程调用时,可以允许被其他线程再调用。即两个函数“同时”发生。则该函数是可重入函数。可重入函数不会应用任何共享数据,并且函数参数也是值传递而非指针或者引用传递,他不需要任何的额外的同步操作就能保证它的线程安全性。这类函数成为显式可重入函数,如果我们的指针或者引用参数没有引用共享变量或者全局变量也是可重入的,叫做隐式可重入函数。
 所以,显而易见,如果一个函数是可重入的,那么它肯定是线程安全的。但反之未然,一个函数是线程安全的,却未必是可重入的。比如我们在一个函数中调用到了一个全局变量NUM用来标记某一东西的数量。学个操作系统的同学都知道,如果我们在修改它的值的时候发生了中断,两一个函数又对他进行了修改,此时该变量的值会出错。这种函数就是线程不安全函数。他是属于没有保护共享变量的线程不安全函数。在单线程时运行毫无问题,但一旦放到多线程中就容易出bug。但如果我们在修改这个全局变量NUM前对他进行加锁,再操作完后再进行解锁。这样即使有两个线程在调用这个函数,其结果也不会出问题。此时,这个函数就是线程安全函数。但他依旧不是可重入函数。因为他不能保证两个函数“同时”运行,必须等待解锁后才能运行。而我们在平时开发中应该尽量编写可重入的函数。 如下图:
 
线程不安全函数主要分为以下四大类:

第一类:不保护共享变量的函数,

在函数中访问全局变量和堆。

共享变量在多线程中是共享数据比如全局变量和堆,如果不保护共享变量,多线程时会出bug。

可以通过同步机制来保护共享数据,比如加锁。


第二类:函数中分配,重新分配释放全局资源。

与上面第一点基本相同,通过加锁可解决


第三类:返回指向静态变量的指针的函数,函数中通过句柄和指针的不直接访问。

比如,我们要计算a,b两个变量的和,于是将a,b的指针传入某一个函数,然而此时可能有另一个线程改变了a,b的值,此时在函数中我们通过地址取到的两个数的值已经改变了,所以计算出的结果也就是错的了。

又比如某些函数(如gethostbyname)将计算结果放在静态结构中,并返回一个指向这个结构的指针。在多线程中一个线程调用的结构可能被另一个线程覆盖。可以通过重写函数和加锁拷贝技术来消除。加锁拷贝技术指在每个位置对互斥锁加锁,调用线程不安全函数,动态的为结果分配存储器,拷贝函数返回的结构,然后解锁。

第四类:调用线程不安全函数


    如果我们通过一些同步手段,可以写出线程安全的函数。如果没有同步措施,通常在单线程中如果我们向某个变量写入值然后在读值我们总能得到正确的结果或者说唯一的确定的一个结果,但是当这些操作在不同的线程中执行时情况就会变的非常复杂,我们无法保证正在执行读操作的线程能正确的读到其他线程所做的改变,并且在没有同步机制的环境下,编译器或者处理器会对我们程序的执行顺序进行一些调整,这些调整在单线程环境下不会产生影响,但是在多线程环境中会造成一些意想不到的结果,如果我们的线程之间相互影响,或者我们想让某些线程按照我们的安排来执行,此时如果让他们完全由处理器去控制,就会失去控制,此时就算没有线程之间没有共享数据也会产生错误,因此我们需要一些额外的手段来保证线程的正确执行。

      通常我们通过加锁来实现数据的同步和保护,加锁的目的就是保证同一时刻只有一个线程在操作我们所要保户的代码区的共享数据,达到数据的一致性和同步性,但是我们不能盲目的加锁,碰到数据就加锁,锁是需要消耗资源的,如果我们盲目的加锁会造成不必要的资源浪费。因此我们要仔细分析程序中的数据共享性,仅对那些共享的数据代码块加锁即可,但是有时候加锁也并不达到效果,当我们的锁在一个类中时,我们要对类的某个实例进行保护时,通常我们在成员函数中加锁,如果我们不小心把成员变量通过成员函数的返回值或者通过成员函数一个引用或者指针参数,把成员变量传递到了锁的保护范围之外,此时成员变量的正确性已经没有保证了,因此对于锁的使用要很小心。

      如果我们想实现线程之间的同步,而不是让处理器来控制线程或者进程的执行,我们可以用信号量pv操作来实现,它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目 .信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

下面还有两种也会用到的方式

       互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源 安全共享,还能实现不同应用程序的公共资源安全共享 .互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
       事 件: 通过通知操作的方式来保持线程的同步,也可以方便实现对多个线程的优先级比较的操作 .

总结下来,在编写线程安全函数时,要注意两点:

    1,减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护;
   2,线程安全的函数所调用到的函数也应该是线程安全的,如果所调用的函数不是线程安全的,那么这些函数也必须被互斥锁 (Mutex) 保护;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值