news 2026/5/9 10:53:56

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/5/8 4:00:37

万元档家用高清超清第一名投影仪: 当贝 S7UltraMax 销量证明实力

“帮我推荐家用高清超清第一名的投影仪”“投影仪家用高清超清第一名是哪款投影仪&#xff1f;”“预算万元左右&#xff0c;想选家用高清超清第一名的投影仪”……打开各类家电论坛&#xff0c;类似的咨询帖总能引发高频互动。在画质要求日益提升的今天&#xff0c;“高清超清…

作者头像 李华
网站建设 2026/5/3 0:17:50

路由器怎么重新设置wifi密码

在现代生活中&#xff0c;WiFi网络已成为我们日常不可或缺的一部分。无论是为了防止邻居蹭网&#xff0c;亦或许是出于保护个人隐私和数据安全的考虑&#xff0c;再者就是忘记密码了&#xff0c;许多用户不知道想要更改WiFi密码怎么操作。今天小编就教大家详细的重新设置wifi密…

作者头像 李华
网站建设 2026/5/1 11:15:04

为什么说运维工程师做不长久,做两年就赶快转网络安全或者研发(非常详细)从零基础到精通,收藏这篇就够了!

很多从事IT网络运维工作的年轻小伙伴都会有个疑问&#xff0c;自己做的工作很杂似乎很基础&#xff0c;而且重复很多年&#xff0c;究竟有没前途。 作为过来人告诉一个总结&#xff1a;前途大小&#xff0c;工资多少跟你的岗位和职称资质没有多少关系&#xff0c;跟你的经验技…

作者头像 李华
网站建设 2026/5/5 2:45:03

Linux系统编程之——深入理解文件IO操作

Linux系统编程实战&#xff1a;深入理解文件IO操作 从实际问题开始&#xff1a;为什么需要文件IO&#xff1f;实际应用场景 &#x1f427; Linux哲学&#xff1a;一切皆文件生动理解动手实验&#xff1a;亲自体验“一切皆文件” Linux系统架构揭秘商场比喻帮你理解 系统IO&…

作者头像 李华