线程安全和可重入函数
Posted Aladdin Wang
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程安全和可重入函数相关的知识,希望对你有一定的参考价值。
先说结论
可重入函数未必是线程安全的;线程安全函数未必是可重入的。
可重入函数的概念
可重入的程序(函数)允许在执行的过程中被打断,并在打断所执行的代码中再次安全的调用。重点在于安全,不允许程序挂掉。
若一个函数是可重入的,则该函数大多数应当满足下述条件:
- 不能含有静态(全局)非常量数据。
- 不能返回静态(全局)非常量数据的地址。
- 只能处理由调用者提供的数据。
- 调用(call)的函数也必需是可重入的。
- 一般外设寄存器单独考虑
总之一句话:函数在任意时刻重新进入时,都能够安全执行,就是可重入的。
举例说明,设计一个发送字符串的状态机:
/*例子来源:https://mp.weixin.qq.com/s/DVa7-4_o5IiWrkl-SXD2EQ*/
fsm_rt_t print_str(const char *pchStr)
{
static enum {
START = 0,
IS_END_OF_STRING,
SEND_CHAR,
} s_tState = START;
static const char *s_pchStr = NULL;
switch (s_tState) {
case START:
s_pchStr = pchStr;
s_tState = IS_END_OF_STRING;
//break; //!< fall-through
case IS_END_OF_STRING:
if (*s_pchStr == '\\0') {
PRINT_STR_RESET_FSM();
return fsm_rt_cpl;
}
s_tState = SEND_CHAR;
//break; //!< fall-through
case SEND_CHAR:
if (serial_out(*s_pchStr)) {
pchStr++;
s_tState = IS_END_OF_STRING;
}
break;
}
return fsm_rt_on_going;
}
由于状态机的中使用了静态变量,尤其是状态变量s_tState——这意味着同时执行的多个print_str,彼此共享同一个状态变量,它们是彼此干扰的。这意味着同时执行多个print_str是“不安全”的,是会出问题的(比如字符串长度不一致时很可能会出现buffer-overflow的问题),因此可以说 print_str 是不可重入的。
通过分析,可以注意到问题所在,即:如果存在多个 print_str 调用,那么它们其实是在“竞争”关键的状态变量 s_tState和上下文 s_pchStr,那么,为状态机提供一个状态机控制块就是一个很好的解决方案,这样每个实例就都有了自己的状态变量 s_tState和上下文 s_pchStr,从而消除了竞争关系。从oo角度来说,就是定义一个状态机类,把状态变量 s_tState和上下文 s_pchStr当做私有属性,多个 print_str就是多个print_str的实例,每个实例都有自己的上下文,互不影响。
更改代码后:
#undef this
#define this (*ptThis)
#define PRINT_STR_RESET_FSM() \\
do { this.State = START; } while(0)
typedef struct print_str_t {
uint8_t chState;
const char *pchStr;
} print_str_t;
fsm_rt_t print_str(print_str_t *ptThis, const char *pchStr)
{
enum {
START = 0,
IS_END_OF_STRING,
SEND_CHAR,
};
switch (this.chState) {
case START:
this.pchStr = pchStr;
this.chState = IS_END_OF_STRING;
//break; //!< fall-through
case IS_END_OF_STRING:
if (*(this.pchStr) == '\\0') {
PRINT_STR_RESET_FSM();
return fsm_rt_cpl;
}
this.chState = SEND_CHAR;
//break; //!< fall-through
case SEND_CHAR:
if (serial_out(*(this.pchStr))) {
this.pchStr++;
this.chState = IS_END_OF_STRING;
}
break;
}
return fsm_rt_on_going;
}
更改后还存在另一个问题,即:
状态机print_str使用了共享函数serial_out(),即便该函数本身可以保证原子性,但它是一个临界资源,当该状态机存在多个实例时,虽然能保证安全执行,但是并不能保证每个字符串的打印都是完整的,所以此状态机不是线程安全的。
线程安全的概念
线程安全指某个函数、函数库在多线程环境中被调用时,能够正确地处理多个线程之间的共享资源,使程序(函数)都能 给出正确的结果。重点在于“功能正常”。
- 线程私有资源,没有线程安全问题
- 共享资源,线程间以某种秩序使用共用资源也能实现线程安全。
举例说明:
#include <pthread.h>
int increment_counter ()
{
static int counter = 0;
static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex);
// only allow one thread to increment at a time
++counter;
// store value before any other threads increment it further
int result = counter;
pthread_mutex_unlock(&mutex);
return result;
}
上面的代码中,函数increment_counter可以在多个线程中被调用,因为有一个互斥锁mutex来同步对共享变量counter的访问。但是如果这个函数用在可重入的中断处理程序中,如果在pthread_mutex_lock(&mutex)和pthread_mutex_unlock(&mutex)之间产生另一个调用函数increment_counter的中断,则会第二次执行此函数,此时由于mutex已被lock,函数会在pthread_mutex_lock(&mutex)处阻塞,并且由于mutex没有机会被unlock,阻塞会永远持续下去。简言之,问题在于 pthread 的 mutex 不可重入。
两者的关系
可重入与线程安全两个概念都关系到函数处理资源的方式。但是,他们有重大区别:
- 可重入是单线程设计中的概念,线程安全是多线程设计中的概念。
- 可重入函数可能由于自身原因,如执行了jmp或者call,或者由于中断响应,又被同一执行线程重入执行该函数。可重入强调对单个线程执行时重新进入同一个子程序(函数)仍然是安全的。
- 线程安全的函数需要把多个线程共享的资源正确对待。如果一个函数中有多个线程共享的资源,要做好数据的保护,例如加锁。
- 可重入概念会影响函数的外部接口,而线程安全只关心函数的实现。
- 大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者提供。
- 要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个线程同时访问。
- 线程安全与可重入的根本区别在于线程安全是函数被多个线程调用都能给出正确结果,函数可重入在于函数多次重复进入不会导致程序挂掉,不在乎功能是否正常。
以上是关于线程安全和可重入函数的主要内容,如果未能解决你的问题,请参考以下文章