热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Linux线程的同步和互斥

目录1、线程的互斥2、可重入VS线程安全3、线程的同步1、线程的互斥

目录

1、线程的互斥

2、可重入VS线程安全

3、线程的同步


1、线程的互斥

1)线程互斥的相关概念

  • 临界资源:被多个执行流共享的资源就称为临界资源,例如全局变量。
  • 临界区:访问临界资源的代码称为临界区。
  • 互斥:互斥保证了任何时刻只有一个线程进入临界区访问临界资源。
  • 原子性:不会被任何机制打断的操作,该操作只有两态,要么已经完成要么还没开始(不能存在已经开始了,但是还没完成的情况,简单理解就是一句汇编代码就可以实现的)。

2)通过订票示例引入互斥量

#include
#include
#include

int tickets = 1000;//票数,每订购一张票数减1
void* ticket(void* arg)
{
//模拟订票过程,多个线程(执行流)访问该程序
while(1)
{
if(tickets > 0)
{
usleep(500);
printf("%s,抢票成功,剩余票数:%d\n",(char*)arg,--tickets);
}
else
break;
} }

int main()
{
//创建多个线程
pthread_t tid[5];
pthread_create(&tid[0],NULL,ticket,"new thread1");
pthread_create(&tid[1],NULL,ticket,"new thread2");
pthread_create(&tid[2],NULL,ticket,"new thread3");
pthread_create(&tid[3],NULL,ticket,"new thread4");
pthread_create(&tid[4],NULL,ticket,"new thread5");

//线程等待
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_join(tid[2],NULL);
pthread_join(tid[3],NULL);
pthread_join(tid[4],NULL);
return 0;
}

运行结果分析:

第一个原因:当一个执行流执行到if判断为真以后,首先会usleep(500),在这期间ticket还没有进行--操作,因此ticket还是大于0的,如果这时再有其他执行流执行if还是判断为真,然后等待一段时间多个执行流对ticket(此时值为0)进行--就变成了负值。

第二个原因:ticket--本身就不是原子性的,汇编代码如下:

如何解决这个问题?

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区执行
  • 如果多个线程同时要进入临界区执行,且当前临界区没有线程在执行时,只允许一个线程进入该临界区进行执行。
  • 如果线程不在临界区执行,或者已经从临界区执行完了,则不能阻止其他线程进入临界区执行。

满足这些条件的实际上就是一把锁,Linux中提供的锁叫做互斥量。注意:要保证加锁后其他线程不能进入临界区,则其他线程必须能够看到锁,也就是说锁也是临界资源(多个线程都可以访问),因此锁在保证临界区前必须先保证自己的安全,也就是说加锁的过程必须是原子性的(保证了多个线程进入该临界区时只允许一个进入)。

3)互斥量的相关接口

初始化互斥量

方法1:静态分配(全局变量)

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER ;

方法2:动态分配

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

         参数:mutex需要初始化的互斥量    attr暂不关注,置NULL即可

         返回值:成功返回0,失败返回错误码

注意:如果使用方法1初始化的互斥量不需要销毁

销毁互斥量

int  pthread_mutex_destroy(pthread_mutex_t *restrict mutex);

注意:不要销毁一个已经加锁的互斥量;已经销毁的互斥量,要确保后面不会在尝试进行加锁。

互斥量加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

使用互斥量改进售票系统代码

#include
#include
#include

pthread_mutex_t mutex;//初始化互斥量
int tickets = 10000;//票数,每订购一张票数减1

void* ticket(void* arg)
{
//模拟订票过程,多个线程(执行流)访问该程序
while(1)
{
pthread_mutex_lock(&mutex);//加锁
if(tickets > 0)
{
printf("%s,抢票成功,剩余票数:%d\n",(char*)arg,--tickets);
pthread_mutex_unlock(&mutex);//解锁
usleep(500);
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
}
}

int main()
{
//创建多个线程
pthread_t tid[5];
pthread_create(&tid[0],NULL,ticket,"new thread1");
pthread_create(&tid[1],NULL,ticket,"new thread2");
pthread_create(&tid[2],NULL,ticket,"new thread3");
pthread_create(&tid[3],NULL,ticket,"new thread4");
pthread_create(&tid[4],NULL,ticket,"new thread5");

//线程等待
pthread_join(tid[0],NULL);
pthread_join(tid[1],NULL);
pthread_join(tid[2],NULL);
pthread_join(tid[3],NULL);
pthread_join(tid[4],NULL);

//销毁互斥量
pthread_mutex_destroy(&mutex);
return 0;
}

4)互斥量原理

语言层面单纯的i++/++i等并不是具有原子性,可能会有数据一致性问题。为了实现互斥锁操作,大多数体系结构都实现了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相互交换,由于只有一跳指令,保证了原子性。

2、可重入VS线程安全

1)相关概念

  • 线程安全:多个线程并发执行同一段代码不会出现不同结果。常见对全局变量或静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 可重入:同一个函数被不同的执行流执行,当一个执行流还没有结束,其他执行流就再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或任何问题的情况下,我们称之为可重入。反之,称之为不可重入。

2)常见线程不安全的情况

  • 不保护共享变量的函数

  • 函数状态随着被调用,状态发生变化的函数

  • 返回指向静态变量指针的函数

  • 调用线程不安全函数的函数

3)常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的

  • 类或者接口对于线程来说都是原子操作

  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

4)常见不可重入的情况

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的

  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构

  • 可重入函数体内使用了静态的数据结构

5)常见可重入的情况

  • 不使用全局变量或静态变量

  • 不使用用malloc或者new开辟出的空间

  • 不调用不可重入函数

  • 不返回静态或全局数据,所有数据都有函数的调用者提供

  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

6)可重入与线程安全的区别与联系

  • 函数是可重入的,那就是线程安全的

  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题

  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

  • 可重入函数是线程安全函数的一种

  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。

  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的

3、线程的同步

1)常见锁的概念

死锁是指在在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程占用的不会释放的资源而处于一种永久等待的状态。

死锁的四个必要条件

  • 互斥条件:一个资源一次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而等待时,对已申请到的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在未使用完之前不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

死锁的避免

  • 破坏死锁的四个条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

死锁的避免算法

  • 死锁检测算法
  • 银行家算法

3)Linux线程同步相关概念

  • 条件变量:当一个线程互斥地访问某个变量时,在其它线程改变状态之前,它什么也做不了。例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量
  • 同步概念:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  • 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

4)什么是线程同步?为什么要存在同步?

在保证线程安全的条件下,让多个执行流按照特定的顺序访问临界资源,我们称之为同步。

线程同步保证了多个线程协同完成任务的安全性和高效性。

5)线程同步相关接口

初始化条件变量

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

        参数:cond要初始化的条件变量   attr:暂不关注,置空即可

        返回值:成功返回0,失败返回错误码

销毁条件变量

int pthread_cond_desroy(pthread_cond_t *restrict cond);//销毁条件变量

等待条件满足

int  pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

       参数:cond等待的条件变量      mutex:互斥量

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);

int pthread_cond_signal(pthread_cond_t *cond);

#include
#include
#include

pthread_mutex_t mutex;
pthread_cond_t cond;

void* fun1(void* arg)
{
while(1)
{
sleep(1);//每隔一秒发送一个信号
pthread_cond_signal(&cond);
}
}

void* fun2(void* arg)
{
while(1)
{
//等待,当接收到信号就会执行下面输出语句。
pthread_cond_wait(&cond,&mutex);
printf("开始行动\n");
}
}

int main()
{
//初始化互斥量和条件变量
pthread_mutex_init(&mutex,NULL);
pthread_cond_init(&cond,NULL);
//创建两个线程,一个发送信号,另一个接收到信号打印“活动”
pthread_t t1,t2;
pthread_create(&t1,NULL,fun1,NULL);
pthread_create(&t2,NULL,fun2,NULL);

//线程等待
pthread_join(t1,NULL);
pthread_join(t2,NULL);

//销毁互斥量和条件变量
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
return 0;
}

6)为什么pthread_cond_wait需要互斥量

  • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足时一直等下去条件都不会满足。所以必须要有一个线程通过某些操作,改变共享变量,使得原先不满足的条件变得满足并去通知等待的线程。
  • 条件变量的满足必然会牵扯到共享数据的变化,所以必须有互斥锁的保护,没有互斥锁就无法保证线程安全的获取修改条件变量。
  • 当一个线程发现条件不满足时,就要调用wait将自己挂起等待,挂起等待时是带锁等待的!!!如果不解锁,其他线程无法访问条件变量,条件永远也不会成立,该线程将一直等待下去。因此,这里的互斥量起到解锁的作用。


推荐阅读
  • 作者:守望者1028链接:https:www.nowcoder.comdiscuss55353来源:牛客网面试高频题:校招过程中参考过牛客诸位大佬的面经,但是具体哪一块是参考谁的我 ... [详细]
  • MySQL索引详解与优化
    本文深入探讨了MySQL中的索引机制,包括索引的基本概念、优势与劣势、分类及其实现原理,并详细介绍了索引的使用场景和优化技巧。通过具体示例,帮助读者更好地理解和应用索引以提升数据库性能。 ... [详细]
  • 基于KVM的SRIOV直通配置及性能测试
    SRIOV介绍、VF直通配置,以及包转发率性能测试小慢哥的原创文章,欢迎转载目录?1.SRIOV介绍?2.环境说明?3.开启SRIOV?4.生成VF?5.VF ... [详细]
  • 深入解析 Apache Shiro 安全框架架构
    本文详细介绍了 Apache Shiro,一个强大且灵活的开源安全框架。Shiro 专注于简化身份验证、授权、会话管理和加密等复杂的安全操作,使开发者能够更轻松地保护应用程序。其核心目标是提供易于使用和理解的API,同时确保高度的安全性和灵活性。 ... [详细]
  • 微软Exchange服务器遭遇2022年版“千年虫”漏洞
    微软Exchange服务器在新年伊始遭遇了一个类似于‘千年虫’的日期处理漏洞,导致邮件传输受阻。该问题主要影响配置了FIP-FS恶意软件引擎的Exchange 2016和2019版本。 ... [详细]
  • 深入理解Redis的数据结构与对象系统
    本文详细探讨了Redis中的数据结构和对象系统的实现,包括字符串、列表、集合、哈希表和有序集合等五种核心对象类型,以及它们所使用的底层数据结构。通过分析源码和相关文献,帮助读者更好地理解Redis的设计原理。 ... [详细]
  • 本文探讨了如何在日常工作中通过优化效率和深入研究核心技术,将技术和知识转化为实际收益。文章结合个人经验,分享了提高工作效率、掌握高价值技能以及选择合适工作环境的方法,帮助读者更好地实现技术变现。 ... [详细]
  • 深入探讨CPU虚拟化与KVM内存管理
    本文详细介绍了现代服务器架构中的CPU虚拟化技术,包括SMP、NUMA和MPP三种多处理器结构,并深入探讨了KVM的内存虚拟化机制。通过对比不同架构的特点和应用场景,帮助读者理解如何选择最适合的架构以优化性能。 ... [详细]
  • 本文探讨了《魔兽世界》中红蓝两方阵营在备战阶段的策略与实现方法,通过代码展示了双方如何根据资源和兵种特性进行战士生产。 ... [详细]
  • 在多线程编程环境中,线程之间共享全局变量可能导致数据竞争和不一致性。为了解决这一问题,Linux提供了线程局部存储(TLS),使每个线程可以拥有独立的变量副本,确保线程间的数据隔离与安全。 ... [详细]
  • 本文探讨了 Spring Boot 应用程序在不同配置下支持的最大并发连接数,重点分析了内置服务器(如 Tomcat、Jetty 和 Undertow)的默认设置及其对性能的影响。 ... [详细]
  • 实体映射最强工具类:MapStruct真香 ... [详细]
  • 本文探讨了在Java多线程环境下,如何确保具有相同key值的线程能够互斥执行并按顺序输出结果。通过优化代码结构和使用线程安全的数据结构,我们解决了线程同步问题,并实现了预期的并发行为。 ... [详细]
  • 深入理解Shell脚本编程
    本文详细介绍了Shell脚本编程的基础概念、语法结构及其在操作系统中的应用。通过具体的示例代码,帮助读者掌握如何编写和执行Shell脚本。 ... [详细]
  • Startup 类配置服务和应用的请求管道。Startup类ASP.NETCore应用使用 Startup 类,按照约定命名为 Startup。 Startup 类:可选择性地包括 ... [详细]
author-avatar
小荷蛋蛋图_945
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有