当前位置: 首页 > news >正文

Linux学习日记19:线程同步与互斥锁

一、前言

前面我们了解了线程的基础知识,而在多线程编程中,线程同步是核心技术,用于解决多线程并发访问共享资源时的竞态条件,保证数据一致性和线程执行顺序的可控性;互斥锁就是线程同步的其中一种机制。

二、线程同步

2.1、线程同步的定义

在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

线程共享进程的地址空间(全局变量、堆、文件描述符等),当多个线程同时读写「临界资源」(如全局变量、硬件设备、网络连接)时,会导致数据错乱。

同步的目标:1、保护临界资源,同一时间只有一个线程访问;2、协调线程执行顺序(如生产者生产完数据后,消费者再消费)。

补:临界区:访问临界资源的代码段(需被同步机制保护);竞态条件:多线程并发执行临界区代码,导致结果依赖于线程执行顺序的不可控问题;同步机制:通过内核 / 库提供的接口,限制临界区的并发访问、协调线程执行时机。

2.2、同步的目标

1、原子性:保证临界区代码 “要么全执行,要么全不执行”,不可中断;

2、可见性:一个线程修改的共享变量,其他线程能立即看到;

3、有序性:保证线程按预期的顺序执行(如生产者先生产,消费者后消费)。

2.3、典型示例

首先创建一个pthread_tb.c文件,然后输入以下代码:

#include <stdio.h> #include <pthread.h> #include <unistd.h> int number;//共享全局变量 void *myfun1(void *arg) { for(int i=0;i<10000;i++) { int ret; ret = number; ret++; number = ret; printf("fun1 is %ld,number is %d\n",pthread_self(),number); usleep(10);//微秒级睡眠 } } void *myfun2(void *arg) { for(int i=0;i<10000;i++) { int ret; ret = number; ret++; number = ret; printf("fun2 is %ld,number is %d\n",pthread_self(),number); usleep(10); } } int main() { pthread_t pthid1; pthread_t pthid2; pthread_create(&pthid1,NULL,myfun1,NULL);//线程创建 pthread_create(&pthid2,NULL,myfun2,NULL); pthread_join(pthid1,NULL);//线程等待 pthread_join(pthid2,NULL); return 0; }

编译并运行,结果如下:

看到这里可能会有疑问,理论上运行完后number会运行到20000,那为什么最终只有19803呢?

因为理论结果前提是20000 次自增都能被正确累加,但你这段代码里的“自增”并不是一条不可分割的操作,而是读+加+写三步,两个线程一旦交叉执行,就会发生丢失更新,导致很多次“+1”被白白覆盖掉,假设某一刻number=19780,线程1执行到ret = number;读到ret = 19780(但还没写回),发生线程切换,线程也执行到:ret = number,也读到ret=19780,线程2,ret++;number=ret,写回number =19781(这次+1生效),再切回线程1,线程1:ret++;number=ret;也写回 number=19781(把线程2的结果“覆盖成同一个值”);这两次自增,本该让 number 变成19782,结果只变成19781——少加了 1。这种“两个线程读到同一个旧值,然后分别写回同一个新值”的情况在你循环 20000 次里会发生很多很多次,于是最终就会出现19803 < 20000。以上例子就是典型的反面案例。

三、互斥锁

3.1、互斥锁的定义

互斥锁是 Linux 多线程同步中最基础、最常用的核心机制,其核心目标是保证同一时间只有一个线程能进入 “临界区”(访问共享资源的代码段),从而解决 “竞态条件”,保证共享资源的原子性、可见性和有序性。

互斥锁本质是一个“二值锁”,状态只有未锁定与已锁定。通过 “加锁 - 访问临界区 - 解锁” 的闭环,强制临界区代码原子执行(要么全执行,要么全不执行,不可被线程切换中断)。

3.2、Linux互斥锁的底层实现

1、用户态尝试加锁:线程加锁时,先通过原子操作尝试获取锁(修改锁的状态);

2、成功则直接执行:若锁未被持有,加锁成功,直接进入临界区;

3、失败则内核态挂起:若锁已被持有,线程进入内核态的 “等待队列” 挂起(放弃 CPU),避免无意义的自旋;

4、解锁时唤醒线程:持有锁的线程解锁时,若等待队列有线程,内核会唤醒其中一个线程重新尝试加锁。

补:原子操作是指一个操作在执行过程中不可被中断、不可被其他线程打断,从而保证对共享数据的修改要么完整发生、要么完全不发生。

3.3、互斥锁的相关函数

1、静态初始化互斥锁

函数原型如下:

#include <pthread.h> // 静态初始化全局互斥锁 pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

注:静态初始化的互斥锁无需手动销毁;同一互斥锁不能重复初始化(已初始化的锁再次调用 pthread_mutex_init 会导致未定义行为)。

2、动态初始化互斥锁

函数原型如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

参数:mutex:指向要初始化的互斥锁对象(不能为 NULL);

attr:互斥锁属性(NULL 表示使用默认属性);

返回值:成功:返回0;失败:非0错误码。

3、阻塞加锁

函数原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

参数:mutex:指向已初始化的互斥锁对象。

返回值:成功:0(获取到锁);失败:非0错误码。

功能:若锁未被持有,当前线程立即获取锁,锁状态变为已锁定;若锁已被持有,当前线程放弃 CPU 使用权,进入内核态等待队列,直到锁被释放。

4、非阻塞加锁

函数原型如下:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

参数:mutex:指向已初始化的互斥锁对象。

返回值:成功:0 (获取到锁);失败:EBUSY(锁已被持有),EINVAL(锁未初始化)等。

功能:若锁未被持有,立即获取并返回 0;若锁已被持有,不阻塞,直接返回EBUSY错误,线程可执行其他逻辑。

适用场景:不想让线程阻塞等待锁的场景。

5、解锁

函数原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

参数:mutex:指向已初始化的互斥锁对象。

返回值:成功:0;失败:非 0 错误码。

功能:释放当前线程持有的锁

注:只有持有锁的线程能解锁,解锁未加锁的锁会触发错误。

6、销毁互斥锁

函数原型如下:

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

功能:销毁属性对象,释放其占用的资源(必须与pthread_mutex_init 配对使用)。

3.4、典型示例

接上一个线程同步的反面案例,通过互斥锁的形式来实现线程的同步,具体代码如下所示:

#include <stdio.h> #include <pthread.h> #include <unistd.h> int number; pthread_mutex_t mutex; void *myfun1(void *arg) { for(int i=0;i<10000;i++) { //lock pthread_mutex_lock(&mutex); int ret; ret = number; ret++; number = ret; printf("fun1 is %ld,number is %d\n",pthread_self(),number); //ulock pthread_mutex_unlock(&mutex); usleep(10); } } void *myfun2(void *arg) { for(int i=0;i<10000;i++) { //lock pthread_mutex_lock(&mutex); int ret; ret = number; ret++; number = ret; printf("fun2 is %ld,number is %d\n",pthread_self(),number); //ulock pthread_mutex_unlock(&mutex); usleep(10); } } int main() { //init mutex pthread_mutex_init(&mutex,NULL); pthread_t pthid1; pthread_t pthid2; pthread_create(&pthid1,NULL,myfun1,NULL); pthread_create(&pthid2,NULL,myfun2,NULL); pthread_join(pthid1,NULL); pthread_join(pthid2,NULL); //kill mutex pthread_mutex_destroy(&mutex); return 0; }

编译并运行,结果如下:

可以看到通过互斥锁来实现了线程的同步,避免了线程之间的竞争关系。

http://www.cnnetsun.cn/news/25509.html

相关文章:

  • CV技术的应用现状与CNN模型识别图像中对象的流程
  • 小兔鲜儿微信小程序开发全攻略
  • 项目分享 | Agent Lightning:零代码改动训练与优化你的AI智能体
  • 用户体验设计终极指南:《用户体验的要素》免费PDF下载
  • 如何快速分析C盘里到底是什么东西占用了最多空间?
  • C盘如何清理?
  • C盘清理?
  • 终极指南:5步轻松部署any-listen私有音乐播放器
  • 从零到一:ArcObjects SDK实战开发完全指南
  • sql server 事务日志备份异常恢复案例---惜分飞
  • 蓝牙音箱EMC整改实战——从±8KV静电复位到稳定过检的技术路径
  • 强力解锁Obsidian时间管理:告别笔记混乱的日历插件实战
  • GitHub Actions下载工件全攻略:从基础到高级应用
  • 24.vsftpd服务--CentOS7
  • 在Python中使用Kafka帮助我们处理数据
  • iPhone15信号算弱网嘛,工作中又该如何进行弱网测试?
  • 75、深入理解与运用SELinux:保障Linux系统安全
  • JetBrains Maple Mono终极指南:免费开源编程字体的完美选择
  • 兴顺物流管理系统(11451)
  • 2025年智能家居完整指南:掌握Home Assistant核心功能
  • Notion + Miro二合一?我用3分钟零成本搭了个私有知识库,太爽了!
  • Codeforces Round 1070 (Div. 2) A~D F
  • 【上海交通大学主办 | 连续6年IEEE出版 | 连续5届快速检索-往届会后3个月EI, Scopus检索 | 设优秀评选】第六届IEEE信息科学与教育国际学术会议(ICISE-IE 2025)
  • 区块链核心知识点梳理(8)-钱包与账户体系
  • 如何快速开展中小学AI教育:完整的AI通识课程指南
  • LeetCode 6. Z 字形变换 | 详细题解(附 C++ 代码)
  • 22、Linux 系统基础管理入门指南
  • 2026年大模型应用开发学习路线:四阶段转型指南,抓住未来3年的职业发展机遇!转AI大模型开发学习顺序真的很重要!
  • 26、Linux文件系统管理全攻略
  • 27、Linux 系统文件管理与共享全攻略