news 2026/3/14 5:30:36

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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; }

编译并运行,结果如下:

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/11 19:11:07

pywebview与React桌面应用开发实战:5个关键问题与架构解决方案

pywebview与React桌面应用开发实战&#xff1a;5个关键问题与架构解决方案 【免费下载链接】pywebview Build GUI for your Python program with JavaScript, HTML, and CSS 项目地址: https://gitcode.com/gh_mirrors/py/pywebview 厌倦了在Python桌面应用中平衡功能性…

作者头像 李华
网站建设 2026/3/13 8:43:38

LOOT插件管理器完整使用指南:从入门到精通

LOOT插件管理器完整使用指南&#xff1a;从入门到精通 【免费下载链接】loot A modding utility for Starfield and some Elder Scrolls and Fallout games. 项目地址: https://gitcode.com/gh_mirrors/lo/loot LOOT是一款专为《星空》、《上古卷轴》系列和《辐射》系列…

作者头像 李华
网站建设 2026/3/12 18:24:22

大模型强化学习:GRPO超级无敌深度剖析,看完即高手

前言&#xff1a;GRPO 宏观视角1. 为什么我们需要 GRPO&#xff1f;&#xff08;Motivation&#xff09;在DeepSeek-Math和DeepSeek-R1等前沿工作中&#xff0c;GRPO被证明是一种极其高效的强化学习算法。要理解它&#xff0c;我们必须先看一眼它的前辈——PPO (Proximal Polic…

作者头像 李华
网站建设 2026/3/12 19:57:12

粒子群优化算法实现PID参数自动调节的代码模型与使用说明

粒子群优化算法实现PID参数自动调节&#xff1a; 1.代码模型说明&#xff1a;针对手动调节PID参数困难、难以找到参数最优值的问题&#xff0c;首先建立了基于PID的simulink模型的评价指标&#xff0c;用以描述模型仿真结果的优劣&#xff0c;其次编写了粒子群优化代码对simuli…

作者头像 李华
网站建设 2026/3/13 3:13:13

Yolo系列:免环境训练工具,支持多版本自动标注、模型转换与训练

yolo免环境训练工具 yolo8标注工具 yolo训练工具 yolo8 yolo4 yolo3 yolo无需搭建环境训练工具 免环境标注、训练的工具 支持版本 yolo3 yolo4 yolo8(电脑显卡必须N卡) 可训练模型 cfg weights bin param pt yolo8l.pt yolo8m.pt yolo8n.pt yolo8s.pt yolo8x.pt 实用功能 自动…

作者头像 李华