线程安全函数与可重入函数理解指南

2,464 阅读6分钟

一. 背景

现如今一个服务不是多线程运行,就是多进程运行,以此来提高服务的并发能力,虽然这样服务的承载能力和性能得到了提升,但线程安全又是一个非常值得考虑的问题,否则程序就会出现预想不到bug、莫名其妙的crash、程序死锁等问题。因此在编程过程中线程安全是每个coder都需要考虑的问题,在线程安全问题领域还有一个概念叫做可重入函数,本文将对两者的区别做以概述,方便大家在今后编码过程中,提升代码安全的意识,减少线上莫名其妙的bug以及crash,提升线上服务的稳定性。

二.两者的基本概念

  • 线程 安全函数:一个函数被若被称为线程安全,当且仅当被多个并发线程反复的调用时,它会一直产生正确的结果
  • 可重入函数:有一类比较重要的线程安全函数,叫做可重入函数,其特点在于它们都具有一种特性:当它们被多个线程调用时,不会引用任何共享的数据因此可以看出可重入函数是线程安全函数的一个真子集

三.详细阐述

  1. 线程安全函数

    • 如果一个函数在同一时刻可以被多个线程安全的调用,就称该函数是线程安全的。例如malloc函数是线程安全的。

    • 不需要共享时,请为每个线程提供一个专用的数据副本thead local。如果必须要数据共享,则提供显示同步或者lock_free的方式,以确保程序已确定预期的方式运行。通过将函数的数据竞争包含在锁定和解除锁定语句中,可以使不安全的函数过程变成线性安全,而且可以进行串行化。

    • 很多函数并不是线程安全的,因为他们返回的数据是存在静态的内存缓冲区中的。通过修改接口,由调用者自行提供缓冲区就可以使这些函数变为线程安全的。

      •  例如:

      •     操作系统实现支持线程安全函数的时候,会对POSIX中的一些非线程安全的函数提供一些可替换的线程安全版本。

      •    比如gethostbyname是线程不安全的,在Linux中提供了gethostbyname_r()的线程安全实现。函数名字后面加上"_r",已表明这个版本是可重入的(reentrant)(对于线程可重入,也就是说是线程安全的,但并不是说对于信号处理函数也是可重入的或者是异步信号安全的)。 详见: man7.org/linux/man-p…

image.png

  1. 可重入函数(reentrant function)

    • 重入即表示可以重复进入,首先意味着这个函数是可以被中断的,并且该函数除了使用自己栈上的变量外不依赖任何全局的数据,可以允许有该函数的多个副本在运行,由于它们使用的都是各自的栈空间,所以互相不会干扰。

    • 可重入函数可以在执行的任何时刻被中断,转入OS调度去执行,而当中断返回控制时,不会出现undefined行为,因此可重入函数可以由多个并发的任务调度,而不会出现数据错误以及其他异常的情况,并且能够中断后可以继续正确的运行。相反不可重入函数不能由多个任务去调度,除非使用了同步的相关手段来保护数据。

    • 可重入的函数是 线程 安全函数,但是反过来,线程安全函数未必是可重入函数。可重入是线程安全的充分非必要条件

    • 不可重入的例子:

      • 在信号处理函数中,只能调用可重入的函数,因此不能调用不可重入函数,进程捕获到信号比如Ctrl+C(SIGINT),因为很多比较友好的程序,都会捕获该信号做些服务中止的数据处理,从而使程序优雅的退出,此时正在执行的正常指令就会被信号处理程序临时中断,它首先执行信号处理函数中的指令,如果从信号处理函数中返回,则继续执行捕获到信号时正在执行的指令。由于信号处理函数默认是在主线程中调用,比如此时主线程正在调用malloc,此时触发信号处理函数,并且在信号函数中也调用了malloc,由于malloc是线程安全的,所以内部使用了锁,根据malloc中锁的不同处理方式,如果是普通锁,则会造成死锁,因为信号处理函数中想要的锁,在主线程已经被拿走了,而此时主线程又在等待信号处理函数的返回,因此就造成了死锁(之前排查服务ctrl+c退不出来,就是该问题导致) 。如果是递归锁,那么信号处理函数中的malloc获取到锁后进行内存分配,由于在主线程的malloc还没有执行完,可能就会造成内存数据混乱的现象。
      • 题外话:即使是可重入函数,在信号处理函数中也需要注意errno的问题,相信大家在阅读一些源码的时候,经常会看到先临时保存errno,然后do something,再将errno恢复。因为一个线程只有一个errno变量,信号处理函数使用的可重入函数也有可能改变errno。因此正确的做法是在信号处理函数开始先临时保存,在返回时再恢复errno。

四.总结

  • 判断一个函数是否是线程安全的,需要判断该函数是否能够在多个线程下同时执行,保证每个线程都能得到正确的结果
  • 判断一个函数是否是可重入函数,需要判断该函数是否可以被中断,中断恢复后程序运行能够得到预期的结果。
  • 如果一个函数对于多个线程来说是可重入的,则说这个函数是线程安全的,但这并不能说明对信号处理函数来说该函数也是可重入,即此时就是异步信号不安全的,如果函数对异步信号处理程序是可重入的,那么该函数就是异步信号安全函数。而线程安全函数不保证在信号处理函数中被安全的调用,比如上面介绍的malloc函数。如果通过一些人为的信号阻塞方式保证非可重入函数不被信号中断,则也是异步信号安全。