美文网首页
可重入与线程安全

可重入与线程安全

作者: suesai | 来源:发表于2018-01-03 22:56 被阅读0次

线程安全(thread safety)是指在多线程环境下,不同的线程在同一时刻能够安全访问临界区的能力,它可以让代码没有副作用地实现想要的功能。
可重入(reentrancy)是指一个函数如果在执行过程中被中断,当中断完成后又可以安全地进入上次中断点重新执行的能力。它有两种语义:

  • 在多线程环境下,一个线程因时间片使用完了(或者其他原因),另一个线程开始运行,接着该线程又安全地重新开始运行。在这种语境下,可重入等同于线程安全。
  • 在单线程的信号处理环境下,一个函数在运行过程中,此时异步来了个信号,控制流便转向了信号处理函数,当信号处理函数完成后该函数又可以安全地重新运行。在这种语境下,可重入又被称为异步信号安全(async-signal safety)。

当提到可重入的时候,我们一般指的是后者。


可重入

为了使函数达到可重入,需要遵循一定的规则,如下

  1. 不要包含静态数据,不要使用全局数据。
int global_var{10};

int NotReentrant()
{
  global_var = 20;
  // 在这里来了个信号
  return global_var;
}

如上所示,如果给 global_var 赋值之后来了个信号,在信号处理函数中又对 global_var 赋了不同的值,那么从信号处理函数返回到 NotReentrant 中,global_var 的值就不再是我们期望的值,因此该函数是不可重入的。
这个例子比较直观,信号也可能在一些不太直观的地方中发送过来。例如,在一个 32 位的机器上操作 64 位的数据,这个操作可能就要被分为两个 32 位的操作,而在这两个操作之间,信号就有可能被发送过来;对于 global_var = f() + g();f()g() 发生的先后顺序是不确定的,而且信号也可能在两个函数之间被发送过来。

  1. 不要使用 newmalloc)或 deletefree)。

不同实现中的 new 是不同的,可以是线程安全的也可以是线程不安全的,但无论如何都是不可重入的。
先假设它是线程不安全的。new 通常为它在堆上分配的存储区维护一个链表,而当信号来的时候,线程可能正在修改此链表,而信号处理函数中也可能调用了 new,也要修改链表,这就造成了冲突。因此线程不安全的 new 是不可重入的。
再假设它是线程安全的。这时候就要在修改链表的地方加上锁,如果在加上锁之后但还没有修改完链表的时候来了个信号,在信号处理函数中也调用了 new,也要加上锁,如果该锁不是递归的,那么该线程将会永久地等待该锁的释放,无法将控制流返回到之前的函数中。因此线程安全的 new 也是不可重入的。
在本文的测试环境中(Ubuntu-16.04-64bit GCC-5.4.0),newmalloc)和 deletemalloc)都是线程安全的。

  1. 不要使用不可重入的函数。

特别需要注意的是标准 I/0 函数,标准 I/O 库中的很多实现都以不可重入方式使用了全局数据。若标准 I/O 指向的是终端,则它是行缓冲的,否则是全缓冲的。例如对于 printf,并不是调用它就会立即将全局缓冲数据冲洗(flush),而是当遇到了换行符(行缓冲)或者是缓冲区满了(全缓冲)才会将数据传送。由于使用了全局数据,因此 printf 是不可重入的,不能将它用在可重入的函数中。

在本文的测试环境下,有些函数是不可重入的,例如 strerrorreaddir,但是系统提供了可重入的版本 strerror_rreaddir_r(后缀 r 表示 reentrant),这些可重入版本不再使用静态数据,而是需要调用者提供由自己管理的存储空间。
信号处理函数也需要是可重入的,当控制流在信号处理函数 A 中时,也可能会有另外的信号发送过来,如果此时的信号屏蔽字没有将该信号屏蔽掉,那么就会转到相应的信号处理函数 B 中,如果信号处理函数 A 和 B 都修改了同一个全局变量,那么结果将会是意料之外的。
对于以上的规则,errno 是一个例外,每个线程都会有自己的 errno,Single UNIX Specification 中要求的可重入函数(详见 APUE 第三版 10.6)也可能会出错,从而修改了 errno,但是依然认为这些函数是可重入的,所以如果在信号处理函数中调用了这些函数,需要在该信号处理函数开始的位置保存 errno,在函数的末尾再把保存的值重新赋给 errno


可重入与线程安全的区别

我们经常将可重入与线程安全视为相同的,但是它们之间还是有细微的差别。在多线程环境下,可重入即为线程安全;但是更常使用的语境是单线程的信号处理,因为满足了上述可重入的三个规则的函数,大多同时也是线程安全的,所以通常并不对其进行区分,但是也会有特殊的情况。

是可重入却是线程不安全

int global_var{20};

void Swap(int* lhs, int* rhs)
{
  int save{global_var};

  global_var = *lhs;
  *lhs = *rhs;
  // 假如信号在此时传来
  *rhs = global_var;

  global_var = save;
}

这种做法就类似与上文对 errno 的处理,先将 global_var 保存起来,在末尾的地方再还回去。如果信号在 Swap 中途传来,也不用担心控制流重新回来的时候 global_var 会发生改变,因此是可重入的;但是由于没有对临界区锁起来,这个函数就是线程不安全的。

是线程安全却是不可重入

上文中的线程安全的 new 就是一个例子。


参考

[1] Reentrancy(computing)
[2] Thread safety
[3] why are malloc and printf said as non-reentrant

相关文章

  • 可重入与线程安全

    一个函数对多个线程来说是可重入的,则说这个函数是线程安全的,但是并不能说明对信号处理函数来说该函数也是可以重入的。...

  • 可重入与线程安全

    线程安全(thread safety)是指在多线程环境下,不同的线程在同一时刻能够安全访问临界区的能力,它可以让代...

  • 可重入与线程安全

    在多任务系统下,中断可能在任务执行的任何时间发生,同时也可能在任务执行过程中发生系统调度而将执行转向另一个线程,如...

  • 线程安全与可重入

    线程安全 线程安全问题是由于线程之间存在共享变量(共享资源、临界资源、临界区)引起的。由于CPU的调度,多个线程访...

  • Qt:可重入和线程安全

    线程安全函数也是可重入函数,但可重入不一定是线程安全。 A thread-safe function is alw...

  • Qt 可重入和线程安全

    可重入和线程安全 本文翻译自Qt官网:重入和线程安全[https://doc.qt.io/qt-5/threads...

  • 锁 - 可重入 vs 不可重入

    可重入锁 在多线程编程和信号处理过程中,经常会遇到可重入(reentrance)和线程安全(thread-safe...

  • 线程安全与可重入性

    线程安全与可重入性 线程安全 一个函数是线程安全指的是,当且仅当多个并发线程反复地调用这个函数时,它会一直产生正确...

  • 线程安全与可重入函数

    一,什么是线程安全? 当对一个复杂对象进行某种操作时,从操作开始到操作结束,被操作的对象往往会经历若干非法的中间状...

  • 线程安全

    线程安全 线程安全定义:线程间共享可变资源(内存)。 实现线程安全的方法:不共享资源。使用可重入函数,不对外部资源...

网友评论

      本文标题:可重入与线程安全

      本文链接:https://www.haomeiwen.com/subject/lcjfnxtx.html