概念
在函数中如果我们使用静态变量了,导致产生中断调用别的函数的 过程中可能还会调用这个函数,于是原来的 静态变量被在这里改变了,然后返回主体函数,用着的那个静态变量就被改变了,导致错误。这类函数我们称为不可重入函数。
如果是在函数体内 动态申请内存的话,即便 新的线程调用这个函数也没事,因为新的线程使用的是新的函数的 新申请的动态内存(静态变量只有一份,所以 多线程对于函数体内的静态变量改变 会有无法修复的结果),所以这类函数就是可重入函数。
可重入函数主要用于多任务环境中,简单来说就是可以被中断的函数,即在这个函数执行的任何时刻中断它,转入OS调度下去执行另外一段代码,返回控制时不会出现什么错误;也意味着它除了使用自己栈上的变量以外不依赖于任何环境(包括static),这样的函数就是 purecode(纯代码)可重入,可以允许有该函数的多个副本在运行,由于它们使用的是分离的栈,所以不会互相干扰。而不可重入的函数由于使用了一些系统资源,比如全局变量区、中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。
信号与可重入
信号作为一种软中断,能够被进程给捕获,因而也就中断进程的正常执行,转而去执行信号处理程序,最后再返回到原进程继续正常执行。
然而,当进程正在执行malloc()动态内存分配时,信号产生从而转入到信号处理程序,但当信号处理程序中也用到了malloc()函数时,问题就出来了?
因为malloc()通常维护一个所有已分配内存链表,当信号发生时,进程可能正在修改链表指针,这时在信号处理程序中将又一次修改链表。
因此,在进行上层应用程序设计过程中我们就必须明确哪些函数是可重入性函数(reentrant functions)。可重入性函数通常也一定能够在信号处理程序(signal handler)中被调用。
可重入与不可重入函数
可重入函数(即可以被中断的函数)可以被一个以上的任务调用,而不担心数据破坏。
可重入函数在任何时候都可以被中断,而一段时间之后又可以恢复运行,而相应的数据不会破坏或者丢失。
要确保函数线程安全,主要需要考虑的是线程之间的共享变量。
属于同一进程的不同线程会共享进程内存空间中的全局区和堆,而私有的线程空间则主要包括栈和寄存器。
因此,对于同一进程的不同线程来说,每个线程的局部变量都是私有的,而全局变量、局部静态变量、分配于堆的变量都是共享的。
在对这些共享变量进行访问时,如果要保证线程安全,则必须通过加锁的方式。
要确保函数可重入,需满足以下几个条件:
1、不在函数内部使用静态或全局数据
2、不返回静态或全局数据,所有数据都由函数的调用者提供。
3、使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
4、不调用不可重入函数。
可重入函数使用的变量有两种情况:
1.使用局部变量,变量保存在CPU寄存器中或者堆栈中;
2.使用全局变量,但是这时候要注意保护全局变量(防止任务中断后被其它任务改变变量)。
可重入与线程安全并不等同,一般说来,可重入的函数一定是线程安全的,但反过来不一定成立。线程安全函数包括了可重入的函数,但不全是可重入函数;
比如:strtok函数是既不可重入的,也不是线程安全的;加锁的strtok不是可重入的,但线程安全;而strtok_r既是可重入的,也是线程安全的。
如果我们的线程函数不是线程安全的,那在多线程调用的情况下,可能导致的后果是显而易见的——共享变量的值由于不同线程的访问,可能发生不可预料的变化,进而导致程序的错误,甚至崩溃。
满足下面条件之一的多数是不可重入函数:
(1)函数内使用了静态数据结构;
(2)函数内使用了malloc()或者free()函数的。
(3)函数内调用了标准的I/O函数的。
(4)进行了浮点运算.许多的处理器/编译器中,浮点一般都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现。
malloc/free是不可重入的,它们使用了全局变量来指向空闲区;标准I/O库的很多实现都使用了全局数据结构;
许多的处理器/编译器中,浮点一般都是不可重入的 (浮点运算大多使用协处理器或者软件模拟来实现)。
线程安全性
线程安全性的概念
线程安全性是指当多个线程同时访问同一份共享数据时,不会引发任何数据不一致或死锁等问题。在多线程环境下,由于多个线程同时对共享数据进行操作,会出现数据竞争的问题,线程安全性就是要确保程序在各种情况下都能正确处理数据竞争问题。
线程安全性与可重入性的关系
可重入函数是指一个函数可以在同一线程中嵌套调用,且不会产生任何副作用。可重入函数一般是线程安全的,因为它们不会使用全局变量或静态变量等共享数据,每次调用时都会使用自己的栈空间,因此不会出现数据竞争的问题。
线程安全性与不可重入性的关系
不可重入函数是指一个函数不能在同一线程中嵌套调用,否则会产生副作用。不可重入函数一般是线程不安全的,因为它们可能会使用全局变量或静态变量等共享数据,同时也可能会使用一些不可重入的系统调用,导致数据竞争问题,从而引发线程不安全的情况。
实例分析
可重入函数的实例分析
可重入函数的一个例子是strtok_s()函数,它是strtok()函数的线程安全版本。该函数可以将一个字符串分割成多个子串,每次调用返回一个子串,直到全部子串都返回完为止。
该函数是可重入的,因为它使用了一个指向保存上一次调用状态的指针,而不是使用静态变量来保存状态,因此可以在多线程环境下安全地使用。
char* strtok_s(char* str, const char* delim, char** context);
不可重入函数的实例分析
不可重入函数的一个例子是asctime()函数,它将一个时间结构体转换成一个字符串表示。该函数不可重入,因为它使用了一个静态变量来保存转换后的字符串,如果在同一线程中嵌套调用该函数,会导致上一次调用的字符串被覆盖,从而产生副作用,引发线程不安全的情况。
char* asctime(const struct tm* timeptr);
不可重入函数的缺陷:C++ 引入了一些并发和多线程编程特性避免不可重入函数的问题
C++11并没有直接引入特性来解决不可重入函数的问题,但是它引入了一些新的并发和多线程编程特性,这些特性可以帮助开发者更好地处理并发和同步问题,从而避免不可重入函数的问题。
1. **线程库**: C++11引入了新的线程库,包括std::thread,std::mutex,std::condition_variable等类,这些类可以帮助开发者更好地管理线程和同步。
2. **原子操作**: C++11引入了std::atomic类,可以用来执行原子操作。原子操作是一种特殊的操作,它在执行过程中不会被其他线程中断,因此可以用来避免竞争条件。
3. **线程局部存储**: C++11引入了thread_local关键字,可以用来声明线程局部变量。线程局部变量是每个线程都有自己的一份拷贝,因此可以避免多个线程同时访问同一变量导致的竞争条件。
4. **锁和条件变量**: C++11引入了多种锁(如std::unique_lock和std::lock_guard)和条件变量(std::condition_variable),这些工具可以用来同步多个线程的执行。
5. **future和promise**: C++11引入了future和promise,这两个类可以用来在多个线程之间传递数据,以及同步多个线程的执行。
通过使用这些特性,开发者可以更好地管理并发和同步问题,从而避免不可重入函数的问题。然而,这些特性并不能自动解决不可重入函数的问题,开发者需要自己理解并正确使用这些特性来避免问题。
可重入函数的力量:为什么 C++ 11 引入了 RAII 和 STL 中的 move 函数
可重入函数是指在多线程环境下可以安全地被多个线程同时调用的函数。可重入函数通常没有使用全局变量或静态变量,并且在函数内部使用了同步保护机制,如互斥锁等。
C++ 11引入了RAII(Resource Acquisition Is Initialization,资源获取即初始化)和move语义,使得可重入函数更加强大。
RAII是一种管理资源的方式,通过在对象的构造函数中获取资源,在对象的析构函数中释放资源,来保证资源的正确管理。在多线程环境下,RAII可以保证同一个资源只被一个线程所拥有,从而避免了竞争条件的出现。
move语义则是一种移动语义,可以将一个对象的资源转移给另一个对象,而不是拷贝。这种转移可以避免资源的重复创建和销毁,从而提高程序的效率。在多线程环境下,move语义可以避免资源的竞争情况,提高程序的并发性能。
在STL中,通过使用可重入函数和RAII,可以避免STL容器在多线程环境下出现竞争条件和数据不一致的问题。通过使用move语义,可以避免在容器元素的复制和销毁过程中出现的资源竞争问题,提高STL容器的并发性能。
因此,可重入函数的力量在于可以提高程序的并发性能和可维护性,而C++ 11引入的RAII和move语义则是为了更好地支持可重入函数的使用。
可重入函数的实现难点:如何确保函数在运行时可以修改其参数
可重入函数是指在多线程环境下可以安全地被多个线程同时调用的函数。在实现可重入函数时,确保函数在运行时可以修改其参数是一个较为困难的问题。
一种常见的解决方案是使用指针或引用来传递参数。通过使用指针或引用,函数可以直接修改参数的值,而不是通过返回值来获取修改后的结果。这种方式可以避免在函数调用过程中产生拷贝,提高程序的效率。同时,在函数调用前,需要确保参数的内存空间已经被分配,并且在函数调用结束后,需要确保参数的内存空间仍然可用。
另一种解决方案是使用线程局部存储(TLS,Thread Local Storage)。线程局部存储是一种可以让变量在每个线程中独立存在的机制。在可重入函数中,可以通过线程局部存储来存储函数的参数,确保函数在运行时可以修改其参数,而不会影响其他线程。这种方式需要使用操作系统提供的API来实现,不同的操作系统可能有不同的实现。
除了上述两种解决方案,还有其他一些方法可以实现可重入函数,在实现时需要考虑到程序的效率和安全性。例如,可以使用锁来保护函数的参数,确保只有一个线程可以修改参数的值;也可以使用原子操作来实现对参数的修改,避免竞争条件的出现。
总之,在实现可重入函数时,需要考虑到多线程环境下的线程安全问题,保证函数在运行时可以修改其参数,同时保证程序的效率和可维护性。
平台相关
在Windows和Linux中,可重入函数的定义是相同的,它们都是指在多线程环境下可以安全调用的函数。
但是,在实现上,Windows和Linux的可重入函数有一些区别。在Windows中,可重入函数通常使用__declspec(reentrant)修饰符来标识,而在Linux中,可重入函数通常使用pthread库中的函数来实现。
此外,Windows中的可重入函数通常使用TLS(线程本地存储)来存储线程特定的数据,而Linux中的可重入函数通常使用函数参数来传递线程特定的数据。
总结
可重入和不可重入函数的优缺点
可重入函数的优点在于它们可以在多线程环境下安全地使用,不会引发线程安全问题;缺点在于由于需要使用每个调用的自己的栈空间,所以可能会占用更多的内存。
不可重入函数的优点在于它们的实现比较简单,不需要考虑线程安全问题;缺点在于它们不能在多线程环境下安全地使用,可能会引发线程安全问题。
如何选择可重入或不可重入函数
在选择可重入或不可重入函数时,需要根据实际情况做出选择。如果程序需要在多线程环境下运行,那么应该优先选择可重入函数,避免引发线程安全问题。如果程序只在单线程环境下运行,或者只需要简单的功能,那么可以选择不可重入函数,以简化程序的实现。需要注意的是,在使用不可重入函数时,需要采取特殊的措施来保证线程安全,比如使用互斥锁等机制。