编写可重入和线程安全的代码

(http://www.ualberta.ca/dept/chemeng/AIX-43/share/man/info/C/a_doc_lib/aixprggd/genprogc/writing_reentrant_thread_safe_code.htm)

单线程的进程中仅有一个控制流。这种进程执行的代码无需可重入或线程安全。在多线程的程序中,同一函数或资源可能被多个控制流并发访问。为保护资源完整性,多线程程序编码必须可重入且线程安全。

本节提供了一些编写可重入和线程安全程序的(指导)信息,但不包括编写线程高效程序的主题。线程高效程序是高效并行化的程序,仅可在程序设计中实现。现有的单线程程序可变得线程高效,但这需要完全地重新设计和重写。

理解可重入性和线程安全性

可重入和线程安全与函数处理资源的方式有关。可重入和线程安全是两个相互独立的概念:一个函数可以仅是可重入的,可以仅是线程安全的,可以两者皆是或两者皆不是。

可重入性

可重入函数不能为后续的调用保持静态(或全局)数据,也不能返回指向静态(或全局)数据的指针。函数中用到的所有数据,都应由函数调用者提供(不包括栈上的局部数据)。可重入函数不能调用不可重入的函数。

不可重入的函数经常(但不总是)可以通过其外部接口和用法识别。例如strtok子例程是不可重入的,因为它保存着将被分隔为子串的字符串。ctime也是不可重入的,它返回一个指向静态数据的指针,每次调用都会覆盖这些数据。

线程安全性

线程安全的函数通过“锁”来保护共享资源不被并发地访问。“线程安全”仅关心函数的实现,而不影响其外部接口。

在C中,局部变量在栈上动态分配,因此,任何不使用静态数据和其它共享资源的函数就是最普通的线程安全(函数)。例如,以下函数就是线程安全的:

1 /* thread-safe function */
2 int diff(int x, int y)
3 {
4         int delta;
5         delta = y - x;
6         if (delta < 0)
7             delta = -delta;
8         return delta;
9 }

View Code

使用全局数据是线程不安全的。应为每个线程维护一份全局数据的拷贝或封装全局数据,以使对它的访问变成串行的。线程可能读取另一线程造成的错误对应的错误码。在AIX系统中,每个线程拥有属于自己的错误码(errno)值。

编写可重入函数

在大部分情况下,不可重入的函数修改为可重入函数时,必须修改函数的对外接口。不可重入的函数不能用于多线程。此外,也许不可能让某个不可重入的函数是线程安全的。

返回数据

很多不可重入的函数返回一个指向静态数据的指针。这可通过两种方法避免:

  • 返回从堆中动态分配的数据(即内存空间地址)。在这种情况下,调用者负责释放堆中的存储空间。其优点是不必修改函数的外部接口,但不能保证向后兼容。现有的单线程程序若不修改而直接使用修改后的函数,将不会释放存储空间,进而导致内存泄露。
  • 由调用者提供存储空间。尽管函数的外部接口需要改变,仍然推荐使用这种方法。

例如,将字符串转换为大写的strtoupper函数实现可能如下代码片段所示:

 1 /* non-reentrant function */
 2 char *strtoupper(char *string)
 3 {
 4         static char buffer[MAX_STRING_SIZE];
 5         int index;
 6
 7         for (index = 0; string[index]; index++)
 8             buffer[index] = toupper(string[index]);
 9         buffer[index] = 0;
10
11         return buffer;
12 }

View Code

该函数既不是可重入的,也不是线程安全的。使用第一种方法将其改写为可重入的,函数将类似于如下代码片段:

 1 /* reentrant function (a poor solution) */
 2 char *strtoupper(char *string)
 3 {
 4         char *buffer;
 5         int index;
 6
 7         /* error-checking should be performed! */
 8         buffer = malloc(MAX_STRING_SIZE);
 9
10         for (index = 0; string[index]; index++)
11             buffer[index] = toupper(string[index]);
12         buffer[index] = 0;
13
14         return buffer;
15 }

View Code

更好的解决方案是修改接口。调用者须为输入和输出字符串提供存储空间,如下代码片段所示:

 1 /* reentrant function (a better solution) */
 2 char *strtoupper_r(char *in_str,  char *out_str)
 3 {
 4         int index;
 5
 6         for (index = 0; in_str[index]; index++)
 7             out_str[index] = toupper(in_str[index]);
 8         out_str[index] = 0;
 9
10         return out_str;
11 }

View Code

通过使用第二种方法,不可重入的C标准库子例程被改写为可重入的。见下文讨论。

为连续调用保持数据

(可重入函数)不应为后续调用保持数据,因为不同线程可能相继调用同一函数。若函数需要在连续调用期间维持某些数据,如工作缓存区或指针,则该数据(资源)应由调用方函数提供调用者应该提供。

考虑如下示例。函数返回字符串中的连续的小写字符。字符串仅在第一次调用时提供,类似strtok子例程。当遍历至字符串末尾时,函数返回0。函数实现可能如下代码片段所示:

/* non-reentrant function */
char lowercase_c(char *string)
{static char *buffer;static int index;char c = 0;/* stores the string on first call */if (string != NULL) {buffer = string;index = 0;}/* searches a lowercase character */for (; c = buffer[index]; index++) {if (islower(c)) {index++;break;}}return c;
}

View Code

该函数是不可重入的。为使它可重入,静态数据(即index变量)需由调用者来维护。该函数的可重入版本实现可能如下代码片段所示:

 1 /* reentrant function */
 2 char reentrant_lowercase_c(char *string, int *p_index)
 3 {
 4         char c = 0;
 5
 6         /* no initialization - the caller should have done it */
 7
 8         /* searches a lowercase character */
 9         for (; c = string[*p_index]; (*p_index)++) {
10             if (islower(c)) {
11                 (*p_index)++;
12                 break;
13             }
14         }
15         return c;
16 }

View Code

函数的接口和用法均发生改变。调用者每次调用时必须提供该字符串,并在首次调用前将索引(index)初始化为0,如下代码片段所示:

1 char *my_string;
2 char my_char;
3 int my_index;
4 ...
5 my_index = 0;
6 while (my_char = reentrant_lowercase_c(my_string, &my_index)) {
7         ...
8 }

View Code

编写线程安全函数

在多线程程序中,所有被多线程调用的函数都必须是线程安全的。然而,在多线程程序中可变通地使用线程不安全的子例程。注意,不可重入的函数通常都是线程不安全的,但将其改写为可重入时,一般也会使其线程安全。

对共享资源加锁

使用静态数据或其它任何共享资源(如文件或终端)的函数,必须对这些资源加“锁”来串行访问,以使该函数线程安全。例如,以下函数是线程不安全的:

1 /* thread-unsafe function */
2 int increment_counter()
3 {
4         static int counter = 0;
5
6         counter++;
7         return counter;
8 }

View Code

为使该函数线程安全,静态变量counter需要被静态锁保护,如下例(伪代码)所示:

 1 /* pseudo-code thread-safe function */
 2 int increment_counter();
 3 {
 4         static int counter = 0;
 5         static lock_type counter_lock = LOCK_INITIALIZER;
 6
 7         lock(counter_lock);
 8         counter++;
 9         unlock(counter_lock);
10         return counter;
11 }

View Code

在使用线程库的多线程应用程序中,应使用信号量互斥锁(mutex)来串行访问共享资源,独立库可能需要工作于线程上下文之外,因此使用其他类型的锁。

线程不安全函数的变通方案

多线程变通地调用线程不安全函数是可能的。这在多线程程序使用线程不安全库时尤其有用,如用于测试或待该库的线程安全版本可用时再予以替换。该变通方案会带来一些开销,因为需对整个函数甚至一组函数进行串行化。

  • 对该库使用全局锁,每次使用库(调用库内子例程或使用库内全局变量)时均对其加锁,如下伪代码片段所示:

1     /* this is pseudo-code! */
2     lock(library_lock);
3     library_call();
4     unlock(library_lock);
5
6     lock(library_lock);
7     x = library_var;
8     unlock(library_lock);

View Code

该方案可能产生性能瓶颈,因为任一时刻仅有一个线程可访问库的任一部分。仅当不常访问库,或作为初步快速实现的权宜之计时可以采用该方案。

  • 对每个库组件(例程或全局变量)或一组组件使用锁,如下例伪代码片段所示:

1     /* this is pseudo-code! */
2     lock(library_moduleA_lock);
3     library_moduleA_call();
4     unlock(library_moduleA_lock);
5
6     lock(library_moduleB_lock);
7     x = library_moduleB_var;
8     unlock(library_moduleB_lock);

View Code

该方案实现相比前者稍微复杂,但可提高性能。

该方案应仅用于应用程序而非库,故可用互斥锁对库加锁。

可重入和线程安全库

可重入和线程安全库广泛应用于并行(和异步)编程环境,而不仅仅用于线程内。因此,总是使用和编写可重入和线程安全的函数是良好的编程实践。

使用函数库

AIX基本操作系统附带的几个代码库是线程安全的。在AIX当前版本中,以下库是线程安全的。

  • C标准函数库(libc.a)
  • BSD兼容函数库(libbsd.a)

某些标准C子例程是不可重入的,如ctime和strtok子例程。它们的可重入版本函数名是原始子例程名添加"_r"后缀。

在编写多线程程序时,应使用子例程的可重入版本来替代原有版本。例如,以下代码片段:

1 token[0] = strtok(string, separators);
2 i = 0;
3 do {
4         i++;
5         token[i] = strtok(NULL, separators);
6 } while (token[i] != NULL);

View Code

在多线程程序中应替换为以下代码片段:

1 char *pointer;
2 ...
3 token[0] = strtok_r(string, separators, &pointer);
4 i = 0;
5 do {
6         i++;
7         token[i] = strtok_r(NULL, separators, &pointer);
8 } while (token[i] != NULL);

View Code

线程不安全库可用于单线程程序中。程序员必须确保使用该库的线程唯一性;否则,程序行为不可预料,甚至可能崩溃。

改写函数库

以下信息突出了将现有库转换为可重入和线程安全库的主要步骤(仅适用于C语言代码库)。

  • 识别对外的全局变量。这些变量通常在头文件中用export关键字定义*

【译者注:应为”用extern关键字声明”】

应封装对外的全局变量。该变量应改为私有(在库源代码内用static关键字定义)。应创建(读写)该变量的子例程。

  • 识别静态变量和其他共享资源。静态变量通常用static关键字定义。

任一共享资源均应与锁关联。锁的粒度及数目会影响库的性能。可使用”一次性初始化”特性(如pthread_once)来方便地初始化锁。

  • 识别不可重入函数并使之变为可重入函数。见”编写可重入函数“。
  • 识别线程不安全函数并使之变为线程安全函数。见”编写线程安全函数“。

转载于:https://www.cnblogs.com/clover-toeic/p/3739308.html

【译】编写可重入和线程安全的代码相关推荐

  1. (转)编写可重入和线程安全的代码(Writing Reentrant and Thread-Safe Code)

    Writing Reentrant and Thread-Safe Code 原文地址: http:/unet.univie.ac.at/aix/aixprggd/genprogc/writing_r ...

  2. Qt之可重入与线程安全

    本篇文章中,术语"可重入性"和"线程安全"被用来标记类与函数,以表明它们如何被应用在多线程应用程序中. 一个线程安全的函数可以同时被多个线程调用,甚至调用者会使 ...

  3. java 变量锁_并发编程高频面试题:可重入锁+线程池+内存模型等(含答案)

    对于一个Java程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一.因为并发编程是Java语言中最为晦涩的知识点,它涉及操作系统.内存.CPU.编程语言等多方面的基础能力,更为考验一个程序 ...

  4. java 线程重入,java synchronized加载加锁-线程可重入详解及实例代码

    java synchronized加载加锁-线程可重入 实例代码: public class ReGetLock implements Runnable { @Override public void ...

  5. Linux | 可重入函数 | volatile | SIGCHLD信号

    文章目录 可重入函数 可重入与线程安全 volatile volatile和const同时修饰变量 SIGCHLD信号 可重入函数 当一个函数可以被两个执行流调用,我们称该函数具有重入特征 如果一个函 ...

  6. 【Java 并发编程】多线程、线程同步、死锁、线程间通信(生产者消费者模型)、可重入锁、线程池

    并发编程(Concurrent Programming) 进程(Process).线程(Thread).线程的串行 多线程 多线程的原理 多线程的优缺点 Java并发编程 默认线程 开启新线程 `Ru ...

  7. 函数可重入性及编写规范

    一.可重入函数 1)什么是可重入性? 可重入(reentrant)函数可以由多于一个任务并发使用,而不必担心数据错误.相反, 不可重入(non-reentrant)函数不能由超过一个任务所共享,除非能 ...

  8. 线程安全与可重入函数

    线程安全:一个函数被称为线程安全的(thread-safe),当且仅当被多个并发进程反复调用时,它会一直产生正确的结果.如果一个函数不是线程安全的,我们就说它是线程不安全的(thread-unsafe ...

  9. java线程之可重入锁

    一.什么是可重入锁 可重入锁就是可以重复进入的锁,也叫递归锁.前提是同一把锁,如同一个类.同一个实例.同一个代码块. 来自知乎的解释:可重入锁指的是同一个线程外层函数获得锁之后,内层递归函数仍然能获取 ...

  10. 可重入函数与线程安全的区别与联系

    本文主要介绍一下可重入函数与线程安全的区别与联系,在此之前我们先来了解一些基本概念:什么是线程全函数,什么是可重入函数? 线程安全函数 概念 线程安全的概念比较直观,一般来说,一个函数被称为线程安全的 ...

最新文章

  1. flask_restplus的fields中怎么定义字典_Quora | 成功的定义是什么?
  2. 币未来趋势分析_分析:中国便利店零售市场现状以及未来发展趋势
  3. Feign的日志级别设置(显示feign调用服务过程中的日志信息)
  4. php建站静态,php网站如何生成静态
  5. 8个树莓派超级计算机_6 个可以尝试的树莓派教程
  6. ajax一次输出1万条数据库,后端接口一次给出100万条数据,请问你前端怎么分页处理...
  7. 嵌入式电路设计(最小系统)
  8. Java 学习笔记(9)——java常用类
  9. Struts2中Action的搜素顺序
  10. 在Oracle的FORM中对不同的状态采取不同的颜色显示
  11. C++ 派生类和virtual
  12. php中vc14是什么意思,VC9、VC11、VC14、VC15是啥?
  13. SPSS Statistical 软件的使用
  14. java整数最大_Java 整数最大值
  15. 转载 Java基本数据类型
  16. 计算机网络中表征数据传输有效性的指标是,表征数据传输有效性的指标是
  17. 都挺好 苏大强C位出道的不只表情包 还有大眼袋
  18. Apache ab性能测试结果分析
  19. presto基于kerberos访问hive的安装
  20. 各地级市-进出口与贸易差额(2000-2020)

热门文章

  1. 【Django 2021年最新版教程9】数据库查询操作
  2. kubernetes Containerd shim docker关系
  3. 选项不属于HTML语言特点,Web前端开发初级模拟测试卷一(2)
  4. Leetcode之路径总和II
  5. Android持久化技术
  6. LayaAir 性能统计面板 laya.utils.Stat
  7. Spring Boot 集成 Thymeleaf 快速入门、静态资源映射规则、WebJars
  8. 985大学计算机专业保研率排名,2016中国大学保研率排行榜出炉
  9. 涿州有没有java_谁说涿州没有方言?
  10. 阶段3 1.Mybatis_02.Mybatis入门案例_3.mybatis注解开发和编写dao实现类的方式