Linux线程同步(1)——一个例子看懂为什么需要线程同步?

news/2025/2/22 8:36:38/

        对于一个单线程进程来说,它不需要处理线程同步的问题,所以线程同步是在多线程环境下需要注意的问题。线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享,不过这种便捷的共享是有代价的,那就是多个线程并发访问共享数据所导致的数据不一 致的问题。

为什么需要线程同步?

        线程同步是为了对共享资源的访问进行保护。这里说的共享资源指的是多个线程都会进行访问的资源, 譬如定义了一个全局变量 a,线程 1 访问了变量 a、同样在线程 2 中也访问了变量 a,那么此时变量 a 就是多个线程间的共享资源,大家都要访问它。

        保护的目的是为了解决数据一致性的问题。当然什么情况下才会出现数据一致性的问题,根据不同的情况进行区分;如果每个线程访问的变量都是其它线程不会读取和修改的(譬如线程函数内定义的局部变量或者只有一个线程访问的全局变量),那么就不存在数据一致性的问题;同样,如果变量是只读的,多个线程同时读取该变量也不会有数据一致性的问题;但是,当一个线程可以修改的变量,其它的线程也可以读取或者修改的时候,这个时候就存在数据一致性的问题,需要对这些线程进行同步操作,确保它们在访问变量的存储内容时不会访问到无效的值。

        出现数据一致性问题其本质在于进程中的多个线程对共享资源的并发访问(同时访问)。前面给大家介绍了,进程中的多个线程间是并发执行的,每个线程都是系统调用的基本单元,参与到系统调度队列中; 对于多个线程间的共享资源,并发执行会导致对共享资源的并发访问,并发访问所带来的问题就是竞争,因此可能会出现数据一致性问题,所以就需要解决这个问题;要防止并发访问共享资源,那么就需要对共享资源的访问进行保护,防止出现并发访问共享资源。

        当一个线程修改变量时,其它的线程在读取这个变量时可能会看到不一致的值,下图描述了两个线程读写相同变量(共享变量、共享资源)的假设例子。在这个例子当中,线程 A 读取变量的值,然后再给这个变量赋予一个新的值,但写操作需要 2 个时钟周期(这里只是假设);当线程 B 在这两个写周期中间读取了这个变量,它就会得到不一致的值,这就出现了数据不一致的问题。

         我们可以编写一个简单地代码对此文件进行测试,2 个线程在常规方式下访问共享资源,这里的共享资源指的就是静态全局变量 g_count。该程序创建了两个线程,且均执行同一个函 数,该函数执行一个循环,重复以下步骤:将全局变量 g_count 复制到本地变量 l_count 变量中,然后递增 l_count,再把 l_count 复制回 g_count,以此不断增加全局变量 g_count 的值。因为 l_count 是分配于线程栈中的自动变量(函数内定义的局部变量),所以每个线程都有一份。循环重复的次数要么由命令行参数指定, 要么去默认值 1000 万次,循环结束之后线程终止,主线程回收两个线程之后,再将全局变量 g_count 的值打印出来。

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>static int g_count = 0;static void *new_thread_start(void *arg){int loops = *((int *)arg);int l_count, j;for (j = 0; j < loops; j++) {l_count = g_count;l_count++;g_count = l_count;}return (void *)0;
}
static int loops;
int main(int argc, char *argv[]){pthread_t tid1, tid2;int ret;/* 获取用户传递的参数 */if (2 > argc)loops = 10000000; //没有传递参数默认为 1000 万次elseloops = atoi(argv[1]);/* 创建 2 个新线程 */ret = pthread_create(&tid1, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}ret = pthread_create(&tid2, NULL, new_thread_start, &loops);if (ret) {fprintf(stderr, "pthread_create error: %s\n", strerror(ret));exit(-1);}/* 等待线程结束 */ret = pthread_join(tid1, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}ret = pthread_join(tid2, NULL);if (ret) {fprintf(stderr, "pthread_join error: %s\n", strerror(ret));exit(-1);}/* 打印结果 */printf("g_count = %d\n", g_count);exit(0);
}

         编译代码,进行测试,首先执行代码,传入参数 1000,也就是让每个线程对全局变量 g_count 递增 1000 次,如下所示:

 

         从打印结果看,得到了我们想象中的结果,每个线程递增 1000 次,最后的数值就是 2000;接着我们把递增次数加大,采用默认值 1000 万次,如下所示:

         可以发现,结果竟然不是我们想看到的样子,执行到最后,应该是 2000 万才对,这里其实就出现了上文提到的问题,数据不一致。

如何解决对共享资源的并发访问出现数据不一致的问题?

        为了解决数据不一致的问题,就得需要 Linux 提供的一些方法,也就是接下来将要向大家介绍的线程同步技术,来实现同一时间只允许一个线程访问该变量,防止出现并发访问的情况、消除数据不一致的问题,下图描述了这种同步操作,从图中可知,线程 A 和线程 B 都不会同时访问这个变量,当线程 A 需要修改变量的值时,必须等到写操作完成之后(不能打断它的操作),才运行线程 B 去读取。

         线程的主要优势在于,资源的共享性,譬如通过全局变量来实现信息共享。不过这种便捷的共享是有代价的,必须确保多个线程不会同时修改同一变量、或者某一线程不会读取正由其它线程修改的变量,也就是必须确保不会出现对共享资源的并发访问。Linux 系统提供了多种用于实现线程同步的机制,常见的方法有: 互斥锁、条件变量、自旋锁以及读写锁等,将在后续介绍


http://www.ppmy.cn/news/64297.html

相关文章

极客公园对话 Zilliz 星爵:大模型时代,需要新的「存储基建」

大模型在以「日更」进展的同时&#xff0c;不知不觉也带来一股焦虑情绪&#xff1a;估值 130 亿美元的 AI 写作工具 Grammarly 在 ChatGPT 发布后网站用户直线下降&#xff1b;AI 聊天机器人独角兽公司 Character.AI 的自建大模型在 ChatGPT 进步之下&#xff0c;被质疑能否形成…

线上 FullGC 问题排查实践 —— 手把手教你排查线上问题

一、问题发现与排查 1.1 找到问题原因 问题起因是我们收到了 jdos 的容器 CPU 告警&#xff0c;CPU 使用率已经达到 104% 观察该机器日志发现&#xff0c;此时有很多线程在执行跑批任务。正常来说&#xff0c;跑批任务是低 CPU 高内存型&#xff0c;所以此时考虑是 FullGC 引…

Rust + 嵌入式:强力开发组合

Rust 的由来 Rust 编程语言的灵感诞生于一次意外。2006年&#xff0c;当 Graydon Hoare 回到位于温哥华的公寓时&#xff0c;发现电梯又因为软件崩溃出了故障。住在 21 楼的他无奈爬楼时&#xff0c;不禁心想&#xff0c;“我们搞计算机的&#xff0c;怎么连个能正常运行的电梯…

Spring Security OAuth2.0(四)-----OAuth2+JWT

传统的通过 session 来记录用户认证信息的方式我们可以理解为这是一种有状态登录&#xff0c;而 JWT 则代表了一种无状态登录。「无状态登录天然的具备单点登录能力」 1. 无状态登录 1.1 什么是有状态 有状态服务&#xff0c;即服务端需要记录每次会话的客户端信息&#xff…

2023-05-08 数据库-流水操作与物化-分析

摘要: 数据库的查询模型比较经典的是火山模型, 其实就是流水线操作, 例如mysql. 作为对应的便是物化模型, 例如monetdb. 列存数据库自从monetdb的开创性paper开始, 几乎都是采用了物化模型. 并且一定程度上结合了流水线操作, 例如duckdb. 本文做些简要的分析. 流水线操作/火…

细说Hibernate的缓存机制

Hibernate 的缓存机制主要包括一级缓存和二级缓存。 1. 一级缓存&#xff08;Session 缓存&#xff09;&#xff1a; 一级缓存是 Hibernate 的 Session 级别的缓存&#xff0c;与每个 Session 对象相关联。当您通过 Session 对象执行查询、保存或更新操作时&#xff0c;Hibern…

关于git stash使用

在 Git 中&#xff0c;可以使用 git stash 命令将未提交的更改保存在一个临时存储区中&#xff0c;以便在需要的时候重新应用这些更改。如果需要撤销最近的 git stash 操作&#xff0c;可以使用 git stash pop 命令来将最近一次存储的更改应用到工作区。 如果需要回到之前的某…

vue3项目搭建

一、安装 vue3.0 脚手架 &#xff08;1&#xff09;node安装&#xff08;前端开发环境&#xff09; 打开node官网:https://nodejs.org/zh-cn/ 下载node并安装&#xff08;安装vue3建议node在10.0版本以上&#xff09;。 输入node -v可显示node版本 &#xff08;2&#xff09;…