Linux线程(七)线程安全详解

news2024/10/6 9:58:08

当我们编写的程序是一个多线程应用程序时,就不得不考虑到线程安全的问题,确保我们编写的程序是一个线程安全(thread-safe)的多线程应用程序,什么是线程安全以及如何保证线程安全?带着这些问题,本小节将讨论线程安全相关的话题。

1.线程栈 

进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈。譬如主线程调用 pthread_create()创建了一个新的线程,那么这个新的线程有它自己独立的栈地址空间、而主线程也有它自己独立的栈地址空间。

通过前面的文章可知,在创建一个新的线程时,可以配置线程栈的大小以及起始地址,当然在大部分情况下,保持默认即可!

既然每个线程都有自己的栈地址空间,那么每个线程运行过程中所定义的自动变量(局部变量)都是分配在自己的线程栈中的,它们不会相互干扰。在示例代码 11.10.1 中,主线程创建了 5 个新的线程,这 5 个线程使用同一个 start 函数 new_thread,该函数中定义了局部变量 number 和 tid 以及 arg 参数,意味着这 5个线程的线程栈中都各自为这些变量分配了内存空间,任何一个线程修改了 number 或 tid 都不会影响其它线程。

//示例代码 11.10.1 线程栈示例
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static void *new_thread(void *arg) {
	 int number = *((int *)arg);
	 unsigned long int tid = pthread_self();
	 printf("当前为<%d>号线程, 线程 ID<%lu>\\n", number, tid);
	 return (void *)0; 
}

static int nums[5] = {0, 1, 2, 3, 4};

int main(int argc, char *argv[])
{
 pthread_t tid[5];
 int j;
 /* 创建 5 个线程 */
 for (j = 0; j < 5; j++)
 pthread_create(&tid[j], NULL, new_thread, &nums[j]);
 /* 等待线程结束 */
 for (j = 0; j < 5; j++)
 pthread_join(tid[j], NULL);//回收线程
 exit(0);
}

2. 可重载函数

要解释可重入(Reentrant)函数为何物,首先需要区分单线程程序和多线程程序。本章开头部分已向各位读者进行了详细介绍,单线程程序只有一条执行流(一个线程就是一条执行流),贯穿程序始终;而对于多线程程序而言,同一进程却存在多条独立、并发的执行流。进程中执行流的数量除了与线程有关之外,与信号处理也有关联。因为信号是异步的,进程可能会在其运行过程中的任何时间点收到信号,进而跳转、执行信号处理函数,从而在一个单线程进程(包含信号处理)中形成了两条(即主程序和信号处理函数)独立的执行流。

接下来再来介绍什么是可重入函数,如果一个函数被同一进程的多个不同的执行流同时调用,每次函数调用总是能产生正确的结果(或者叫产生预期的结果),把这样的函数就称为可重入函数。

Tips:上面所说的同时指的是宏观上同时调用,实质上也就是该函数被多个执行流并发/并行调用,无特别说明,本章内容所提到的同时均指宏观上的概念。

重入指的是同一个函数被不同执行流调用,前一个执行流还没有执行完该函数、另一个执行流又开始调用该函数了,其实就是同一个函数被多个执行流并发/并行调用,在宏观角度上理解指的就是被多个执行流同时调用。

看到这里大家可能会有点不解,我们使用示例进行讲解。示例代码 11.10.2 是一个单线程与信号处理关联的程序。main()函数中调用 signal()函数为 SIGINT 信号注册了一个信号处理函数 sig_handler,信号处理函数 sig_handler 会调用 func 函数;main()函数最终会进入到一个循环中,循环调用 func()。

//示例代码 11.10.2 信号与可重入问题
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
static void func(void) {
 /*...... */
}
static void sig_handler(int sig) {
 func();
}
int main(int argc, char *argv[])
{
 sig_t ret = NULL;
 ret = signal(SIGINT, (sig_t)sig_handler);
 if (SIG_ERR == ret) {
 perror("signal error");
 exit(-1);
 }
 /* 死循环 */
 for ( ; ; )
 func();
 exit(0);
}

当 main()函数正在执行 func()函数代码,此时进程收到了 SIGINT 信号,便会打断当前正常执行流程、跳转到 sig_handler()函数执行,进而调用 func、执行 func()函数代码;这里就出现了主程序与信号处理函数并发调用 func()的情况,示意图如下所示:

在信号处理函数中,执行完 func()之后,信号处理函数退出、返回到主程序流程,也就是被信号打断的位置处继续运行。如果每次出现这种情况执行 func()函数都能产生正确的结果,那么 func()函数就是一个可重入函数。

接着再来看看在多线程环境下,示例代码 11.10.1 是一个多线程程序,主线程调用 pthread_create()函数创建了 5 个新的线程,这 5 个线程使用同一个入口函数 new_thread;所以它们执行的代码是一样的,除了参数 arg 不同之外;在这种情况下,这 5 个线程中的多个线程就可能会出现并发调用 pthread_self()函数的情况。

以上举例说明了函数被多个执行流同时调用的两种情况:

  • 在一个含有信号处理的程序当中,主程序正执行函数 func(),此时进程接收到信号,主程序被打断,跳转到信号处理函数中执行,信号处理函数中也调用了 func()。
  • 在多线程环境下,多个线程并发调用同一个函数。所以由此可知,在多线程环境以及信号处理有关应用程序中,需要注意不可重入函数的问题,如果多条执行流同时调用一个不可重入函数则可能会得不到预期的结果、甚至有可能导致程序崩溃!不止是在应用程序中,在一个包含了中断处理的裸机应用程序中亦是如此!所以不可重入函数通常存在着一定的安全隐患。

可重入函数的分类

  • 绝对的可重入函数:所谓绝对,指的是该函数不管如何调用,都刚断言它是可重入的,都能得到预期的结果。
  • 带条件的可重入函数:指的是在满足某个/某些条件的情况下,可以断言该函数是可重入的,不管

怎么调用都能得到预期的结果。

绝对可重入函数

笔者查阅过多的书籍以及网络文章,并未发现有提出过这种分类,所以这完全是笔者个人对此的一个

理解,首先来看一下绝对可重入函数的一个例子,如下所示:

函数 func()就是一个标准的绝对可重入函数:

static int func(int a){

int local;

int j;

for (local = 0, j = 0; j < 5; j++) {

local += a * a;

a += 2;

}

return local;

}

该函数内操作的变量均是函数内部定义的自动变量(局部变量),每次调用函数,都会在栈内存空间为局部变量分配内存,当函数调用结束返回时、再由系统回收这些变量占用的栈内存,所以局部变量生命周期只限于函数执行期间。

除此之外,该函数的参数和返回值均是值类型、而并非是引用类型(就是指针)。

如果多条执行流同时调用函数 func(),那必然会在栈空间中存在多份局部变量,每条执行流操作各自的局部变量,相互不影响,所以即使函数同时被调用,依然每次都能得到正确的结果。所以上面列举的函数func()就是一个非常标准的绝对可重入函数,函数内部仅操作了函数内定义的局部变量,除了使用栈上的变量以外不依赖于任何环境变量,这样的函数就是 purecode(纯代码)可重入,可以允许该函数的多个副本同时在运行,由于它们使用的是分离的栈,所以不会相互干扰!

总结下绝对可重入函数的特点:

  • 函数内所使用到的变量均为局部变量,换句话说,该函数内的操作的内存地址均为本地栈地址;
  • 函数参数和返回值均是值类型;
  • 函数内调用的其它函数也均是绝对可重入函数。

带条件的可重入函数

带条件的可重入函数通常需要满足一定的条件时才是可重入函数,我们来看一个不可重入函数的例子,

如下所示:

static int glob = 0;

static void func(int loops)

{

int local;

int j;

for (j = 0; j < loops; j++) {

local = glob;

local++;

glob = local;

}

}

当多个执行流同时调用该函数,全局变量 glob 的最终值将不得而知,最终可能会得不到正确的结果,因为全局变量 glob 将成为多个线程间的共享数据,它们都会对 glob 变量进行读写操作、会导致数据不一致的问题,关于这个问题在 12.1 小节中给大家做了详细说明。这个函数就是典型的不可重入函数,函数运行需要读取、修改全局变量 glob,该变量并非在函数自己的栈上,意味着该函数运行依赖于外部环境变量。

但如果对上面的函数进行修改,函数 func()内仅读取全局变量 glob 的值,而不更改它的值:

static int glob = 0;

static void func(int loops)

{

int local;

int j;

for (j = 0; j < loops; j++) {

local = glob;

local++;

printf("local=%d\n", local);

}

}

修改完之后,函数 func()内仅读取了变量 glob,而并未更改 glob 的值,那么此时函数 func()就是一个可重入函数了;但是这里需要注意,它需要满足一个条件,这个条件就是:当多个执行流同时调用函数 func()时,全局变量 glob 的值绝对不会在其它某个地方被更改;譬如线程 1 和线程 2 同时调用了函数 func(),但是另一个线程 3 在线程 1 和线程 2 同时调用了函数 func()的时候,可能会发生更改变量 glob 值的情况,如果是这样,那么函数 func()依然是不可重入函数。这就是有条件的可重入函数的概念,这通常需要程序员本身去规避这类问题,标准 C 语言函数库中也存在很多这类带条件的可重入函数,后面给大家看一下。

再来看一个例子:

static void func(int *arg)

{

int local = *arg;

int j;

for (j = 0; j < 10; j++)

local++;

arg = local;
}

这是一个参数为引用类型的函数,传入了一个指针,并在函数内部读写该指针所指向的内存地址,该函数是一个可重入函数,但同样需要满足一定的条件;如果多个执行流同时调用该函数时,所传入的指针是共享变量的地址,那么在这种情况,最终可能得不到预期的结果;因为在这种情况下,函数 func()所读写的便是多个执行流的共享数据,会出现数据不一致的情况,所以是不安全的。

但如果每个执行流所传入的指针是其本地变量(局部变量)对应的地址,那就是没有问题的,所以呢,这个函数就是一个带条件的可重入函数。

总结

相信笔者列举了这么多例子,大家应该明白了什么是可重入函数以及绝对可重入函数和带条件的可重入函数的区别,还有很多的例子这里就不再一一列举了,相信通过笔者的介绍大家应该知道如何去判断它们了。很多的 C 库函数有两个版本:可重入版本和不可重入版本,可重入版本函数其名称后面加上了“_r”,用于表明该函数是一个可重入函数;而不可重入版本函数其名称后面没有“_r”,前面章节内容中也已经遇到过很多次了,譬如 asctime()/asctime_r()、ctime()/ctime_r()、localtime()/localtime_r()等。

通过 man 手册可以查询到它们“ATTRIBUTES”信息,譬如执行"man 3 ctime",在帮助页面上往下翻便可以找到,如下所示:图 11.10.3 asctime()/asctime_r()函数的 ATTRIBUTES 信息

可以看到上图中有些函数 Value 这栏会显示 MT-Unsafe、而有些函数显示的却是 MT-Safe。MT 指的是multithreaded(多线程),所以 MT-Unsafe 就是多线程不安全、MT-Safe 指的是多线程安全,通常习惯上将MT-Safe 和 MT-Unsafe 称为线程安全或线程不安全。

Value 值为 MT-Safe 修饰的函数表示该函数是一个线程安全函数,使用 MT-Unsafe 修饰的函数表示它是 一 个 线 程 不 安 全 函 数 , 下 一 小 节 会 给 大 家 介 绍 什 么 是 线 程 安 全 函 数 。 从 上 图 可 以 看 出 ,asctime_r()/ctime_r()/gmtime_r()/localtime_r()这些可重入函数都是线程安全函数,但这些函数都是带条件的可重入函数,可以发现在 MT-Safe 标签后面会携带诸如 env 或 locale 之类的标签,这其实就表示该函数需要在满足 env 或 locale 条件的情况下才是可重入函数;如果是绝对可重入函数,MT-Safe 标签后面不会携带任何标签,譬如数学库函数 sqrt:

诸如 env 或 locale 等标签,可以通过 man 手册进行查询,命令为"man 7 attributes",这文档里边的内容反正笔者是没太看懂,不知所云;但是经过我的对比 env 或 locale 这两个标签还是很容易理解的。这两个标签在 man 测试里边出现的频率相对于其它的标签要大,这里笔者就简单地提一下:

  • env:这个标签指的是该函数内部会读取进程的某个/某些环境变量,譬如 getenv()函数,前面也给大家介绍过,进程的环境变量其实就是程序的一个全局变量,前面也讲了,对于这类读取(但没更改)了全局变量的可重入函数应该要满足的条件,这里就不再重述了;
  • local:local 指的是本地,很容易理解,通常该类函数传入了指针,前面也提到了传入了指针的可

3.线程安全函数

 了解了可重入函数之后,再来看看线程安全函数。

一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流)同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。线程安全函数包括可重入函数,可重入函数是线程安全函数的一个真子集,也就是说可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数,它们之间的关系如下:

譬如下面这个函数是一个不可重入函数,同样也是一个线程不安全函数(上小节的最后一个例子):

static int glob = 0;
static void func(int loops)
{
		int local;
		int j;
		for (j = 0; j < loops; j++) {
				local = glob;
				local++;
				glob = local;
		} 
}

如果对该函数进行修改,使用线程同步技术(譬如互斥锁)对共享变量 glob 的访问进行保护,在读写该变量之前先上锁、读写完成之后在解锁。这样,该函数就变成了一个线程安全函数,但是它依然不是可重该变量之前先上锁、读写完成之后在解锁。这样,该函数就变成了一个线程安全函数,但是它依然不是可重入函数,因为该函数更改了外部全局变量的值。

可重入函数只是单纯从语言语法角度分析它的可重入性质,不涉及到一些具体的实现机制,譬如线程同步技术,这是判断可重入函数和线程安全函数的区别,因为你单从概念上去分析的话,其实可以发现可重入函数和线程安全函数好像说的是同一个东西,“一个函数被多个线程同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数”,多个线程指的就是多个执行流(不包括信号处理函数执行流),所以从这里看跟可重入函数的概念是很相似的。

判断一个函数是否为线程安全函数的方法是,该函数被多个线程同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个线程安全函数。判读一个函数是否为可重入函数的方法是,从语言语法角度分析,该函数被多个执行流同时调用是否总能产生正确的结果,如果每次都能产生预期的结果则表示该函数是一个可重入函数。

POSIX.1-2001 和 POSIX.1-2008 标准中规定的所有函数都必须是线程安全函数,但以下函数除外:

表 11.10.1 POSIX.1-2001 和 POSIX.1-2008 中列出的线程不安全函数

以上所列举出的这些函数被认为是线程不安全函数,大家也可以通过 man 手册查询到这些函数,"man 7 pthreads",如下所示:

如果想确认某个函数是不是线程安全函数可以

上小节给大家提到过,man 手册可以查看库函数的 ATTRIBUTES 信息,如果函数被标记为 MT-Safe,则表示该函数是一个线程安全函数,如果被标记为 MT-Unsafe,则意味着该函数是一个非线程安全函数,对于非线程安全函数,在多线程编程环境下尤其要注意,如果某函数可能会被多个线程同时调用时,该函数不能是非线程安全函数,一定要是线程安全函数,否则将会出现意想不到的结果、甚至使得整个程序崩溃!

对于一个中大型的多线程应用程序项目来说,能够保证整个程序的安全性,这是非常重要的,程序员必须要正确对待线程安全以及信号处理等这类在多线程环境下敏感的问题,这通常对程序员提出了更高的要求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2191626.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

光伏项目管理如何更高效化?

一、项目规划与启动阶段的优化 1、智能规划工具&#xff1a;光伏管理软件通常配备有智能项目规划模块&#xff0c;能够根据地理位置、气候条件、政策补贴等因素&#xff0c;自动计算最佳装机容量、预测发电量及收益&#xff0c;帮助项目团队快速制定合理的项目方案。这大大缩短…

大数据毕业设计选题推荐-NBA球员数据分析系统-Python数据可视化-Hive-Hadoop-Spark

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、PHP、.NET、Node.js、GO、微信小程序、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇…

知识图谱入门——9: spaCy中命名实体识别(NER)任务中的预定义标签详解及案例(GPE、MONEY、PRODUCT、LAW、FAC、NORP是什么?)

命名实体识别&#xff08;NER, Named Entity Recognition&#xff09;是自然语言处理&#xff08;NLP&#xff09;中的重要任务之一&#xff0c;旨在从文本中识别出特定的实体&#xff0c;如人名、地名、时间等。spaCy 是一个广泛使用的 NLP 库&#xff0c;它提供了预训练的模型…

数据结构之排序(5)

摘要&#xff1a;本文主要讲各种排序算法&#xff0c;注意它们的时间复杂度 概念 将各元素按关键字递增或递减排序顺序重新排列 评价指标 稳定性: 关键字相同的元素经过排序后相对顺序是否会改变 时间复杂度、空间复杂度 分类 内部排序——数据都在内存中 外部排序——…

涂色问题 乘法原理(2024CCPC 山东省赛 C)

//*下午打得脑子连着眼睛一起疼 很多很基础的题目都没有做出来&#xff0c;规律题也找得很慢。比如下面这题&#xff0c;一定要多做&#xff0c;下次看到就直接写。 原题链接&#xff1a;https://codeforces.com/group/w6iGs8kreW/contest/555584/problem/C C. Colorful Segm…

LabVIEW光偏振态检测系统

开发一套LabVIEW的高精度光偏振态检测系统&#xff0c;采用机械转动法结合光电探测器和高性能数据采集硬件&#xff0c;能快速、准确地测量光的偏振状态。该系统广泛应用于物理研究、激光技术和光学工业中。 系统组成 该光偏振态检测系统主要由以下硬件和软件模块构成&#xf…

无人机+无人车+机器狗+无人船:大规模组网系统技术详解

无人机、无人车、机器狗和无人船的大规模组网系统技术&#xff0c;是实现海陆空全空间无人设备协同作业的关键。这种组网系统技术通过集成先进的通信、控制、感知和决策技术&#xff0c;使得不同类型的无人平台能够高效、准确地完成各种复杂任务。以下是对该技术的详细解析&…

SysML案例-呼吸机

DDD领域驱动设计批评文集>> 《软件方法》强化自测题集>> 《软件方法》各章合集>> 图片示例摘自intercax.com&#xff0c;作者是Intercax公司总裁Dirk Zwemer博士。

【项目安全设计】软件系统安全设计规范和标准(doc原件)

1.1安全建设原则 1.2 安全管理体系 1.3 安全管理规范 1.4 数据安全保障措施 1.4.1 数据库安全保障 1.4.2 操作系统安全保障 1.4.3 病毒防治 1.5安全保障措施 1.5.1实名认证保障 1.5.2 接口安全保障 1.5.3 加密传输保障 1.5.4终端安全保障 资料获取&#xff1a;私信或者进主页。…

将列表中的各字符串sn连接成为一个字符串s使用;将各sn间隔开os.pathsep.join()

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 将列表中的各字符串sn 连接成为一个字符串s 使用;将各sn间隔开 os.pathsep.join() [太阳]选择题 下列说法中正确的是? import os paths ["/a", "/b/c", "/d&q…

Android开发修改为原生主题(在Android Studio开发环境下)

结构如下图&#xff1a; 修改方法&#xff1a;在Android模式目录下&#xff0c;将res下的values文下的themes.xml文件中的 &#xff1a; parent"Theme.Material3.DayNight.NoActionBar" 修改为&#xff1a; parent"Theme.MaterialComponents.DayNight.Bridge&…

Meta 推出Movie Gen

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

ChatGPT 更新 Canvas 深度测评:论文写作这样用它!

我是娜姐 迪娜学姐 &#xff0c;一个SCI医学期刊编辑&#xff0c;探索用AI工具提效论文写作和发表。 ChatGPT又又更新了&#xff1a;基于ChatGPT 4o模型的Canvas 写作和代码功能。目前&#xff0c;仅针对Plus和Team用户。是一个独立的模块&#xff0c;如下所示&#xff1a; 官方…

【Python】simplejson:Python 中的 JSON 编解码利器

simplejson 是一个高效且功能丰富的 Python JSON 编码和解码库。它能够快速地将 Python 数据结构转换为 JSON 格式&#xff08;序列化&#xff09;&#xff0c;或将 JSON 格式的字符串转换为 Python 对象&#xff08;反序列化&#xff09;。相比标准库中的 json 模块&#xff0…

数据结构实验二 顺序表的应用

数据结构实验二 顺序表的应用 一、实验目的 1、掌握建立顺序表的基本方法。 2、掌握顺序表的插入、删除算法的思想和实现&#xff0c;并能灵活运用 二、实验内容 用顺序表实现病历信息的管理与查询功能。具体要求如下: 1.利用教材中定义顺序表类型存储病人病历信息(病历号…

直立行走机器人技术概述

直立行走机器人技术作为现代机器人领域的重要分支&#xff0c;结合了机械工程、计算机科学、人工智能、传感技术和动态控制等领域的最新研究成果。随着技术的不断发展&#xff0c;直立行走机器人在救灾、医疗、家庭辅助等领域开始发挥重要作用。本文旨在对直立行走机器人的相关…

Java 注释新手教程一口气讲完!ヾ(≧▽≦*)o

Java 注释 Java面向对象设计 - Java注释 什么是注释&#xff1f; Java中的注释允许我们将元数据与程序元素相关联。 程序元素可以是包&#xff0c;类&#xff0c;接口&#xff0c;类的字段&#xff0c;局部变量&#xff0c;方法&#xff0c;方法的参数&#xff0c;枚举&…

【STM32开发之寄存器版】(五)-窗口看门狗WWDG

一、前言 窗口看门狗简介&#xff1a; 窗口看门狗通常被用来监测&#xff0c;由外部干扰或不可预见的逻辑条件造成的应用程序背离正常的运行序列而产生的软件故障。除非递减计数器的值在T6位变成0前被刷新&#xff0c;看门狗电路在达到预置的时间周期时&#xff0c;会产生一个M…

C语言 | Leetcode C语言题解之第459题重复的子字符串

题目&#xff1a; 题解&#xff1a; bool kmp(char* query, char* pattern) {int n strlen(query);int m strlen(pattern);int fail[m];memset(fail, -1, sizeof(fail));for (int i 1; i < m; i) {int j fail[i - 1];while (j ! -1 && pattern[j 1] ! pattern…

Pikachu-PHP反序列化

从后端代码可以看出&#xff0c;拿到序列化后的字符串&#xff0c;直接做反序列化&#xff1b;并且在前端做了展示&#xff1b; 如果虚拟化后的字符串&#xff0c;包含alert 内容&#xff0c;反序列化后&#xff0c;就会弹出窗口 O:1:"S":1:{s:4:"test";s…