线程安全和可重入函数

先说结论

可重入函数未必是线程安全的;线程安全函数未必是可重入的。

可重入函数的概念

可重入的程序(函数)允许在执行的过程中被打断,并在打断所执行的代码中再次安全的调用
若一个函数是可重入的,则该函数应当满足下述条件:

  • 不能含有静态(全局)非常量数据。
  • 不能返回静态(全局)非常量数据的地址。
  • 只能处理由调用者提供的数据。
  • 不能依赖于单实例模式资源的锁。
  • 调用(call)的函数也必需是可重入的。

总之一句话:上述条件就是要求可重入函数使用的所有变量都通过函数的调用者提供。
举例说明,设计一个发送字符串的状态机:


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 是不可重入的。

更改代码后:


#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_t 提供,也就是函数的调用者提供的,所以是可重入的。
状态机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 不可重入。

两者的关系

可重入与线程安全两个概念都关系到函数处理资源的方式。但是,他们有重大区别:

  1. 可重入是单线程设计中的概念,线程安全是多线程设计中的概念。
  • 可重入函数可能由于自身原因,如执行了jmp或者call,或者由于中断响应,又被同一执行线程重入执行该函数。可重入强调对单个线程执行时重新进入同一个子程序(函数)仍然是安全的。
  • 线程安全的函数需要把多个线程共享的资源正确对待。如果一个函数中有多个线程共享的资源,要做好数据的保护,例如加锁。
  1. 可重入概念会影响函数的外部接口,而线程安全只关心函数的实现。
  • 大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者提供。
  • 要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个线程同时访问。
上一篇:Azure编程笔记(3):用Fiddler调试Azure的应用程序


下一篇:C# 异步锁