线程安全和可重入函数

先说结论

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

可重入函数的概念

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

  • 不能含有静态(全局)非常量数据。
  • 不能返回静态(全局)非常量数据的地址。
  • 只能处理由调用者提供的数据。
  • 调用(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 不可重入。

两者的关系

可重入概念会影响函数的外部接口,而线程安全只关心函数的实现。

  • 大多数情况下,要将不可重入函数改为可重入的,需要修改函数接口,使得所有的数据都通过函数的调用者提供。
  • 要将非线程安全的函数改为线程安全的,则只需要修改函数的实现部分。一般通过加入同步机制以保护共享的资源,使之不会被几个线程同时访问。
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

WALI-KANG

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值