热门标签 | 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将自己挂起等待,挂起等待时是带锁等待的!!!如果不解锁,其他线程无法访问条件变量,条件永远也不会成立,该线程将一直等待下去。因此,这里的互斥量起到解锁的作用。


推荐阅读
  • 文件描述符、文件句柄与打开文件之间的关联解析
    本文详细探讨了文件描述符、文件句柄和打开文件之间的关系,通过具体示例解释了它们在操作系统中的作用及其相互影响。 ... [详细]
  • 题目描述:给定n个半开区间[a, b),要求使用两个互不重叠的记录器,求最多可以记录多少个区间。解决方案采用贪心算法,通过排序和遍历实现最优解。 ... [详细]
  • 本文详细探讨了KMP算法中next数组的构建及其应用,重点分析了未改良和改良后的next数组在字符串匹配中的作用。通过具体实例和代码实现,帮助读者更好地理解KMP算法的核心原理。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 深入理解 SQL 视图、存储过程与事务
    本文详细介绍了SQL中的视图、存储过程和事务的概念及应用。视图为用户提供了一种灵活的数据查询方式,存储过程则封装了复杂的SQL逻辑,而事务确保了数据库操作的完整性和一致性。 ... [详细]
  • 本文详细介绍了Java编程语言中的核心概念和常见面试问题,包括集合类、数据结构、线程处理、Java虚拟机(JVM)、HTTP协议以及Git操作等方面的内容。通过深入分析每个主题,帮助读者更好地理解Java的关键特性和最佳实践。 ... [详细]
  • 在金融和会计领域,准确无误地填写票据和结算凭证至关重要。这些文件不仅是支付结算和现金收付的重要依据,还直接关系到交易的安全性和准确性。本文介绍了一种使用C语言实现小写金额转换为大写金额的方法,确保数据的标准化和规范化。 ... [详细]
  • UNP 第9章:主机名与地址转换
    本章探讨了用于在主机名和数值地址之间进行转换的函数,如gethostbyname和gethostbyaddr。此外,还介绍了getservbyname和getservbyport函数,用于在服务器名和端口号之间进行转换。 ... [详细]
  • 题目Link题目学习link1题目学习link2题目学习link3%%%受益匪浅!-----&# ... [详细]
  • 本实验主要探讨了二叉排序树(BST)的基本操作,包括创建、查找和删除节点。通过具体实例和代码实现,详细介绍了如何使用递归和非递归方法进行关键字查找,并展示了删除特定节点后的树结构变化。 ... [详细]
  • 技术分享:从动态网站提取站点密钥的解决方案
    本文探讨了如何从动态网站中提取站点密钥,特别是针对验证码(reCAPTCHA)的处理方法。通过结合Selenium和requests库,提供了详细的代码示例和优化建议。 ... [详细]
  • C++实现经典排序算法
    本文详细介绍了七种经典的排序算法及其性能分析。每种算法的平均、最坏和最好情况的时间复杂度、辅助空间需求以及稳定性都被列出,帮助读者全面了解这些排序方法的特点。 ... [详细]
  • 本文详细探讨了Java中的24种设计模式及其应用,并介绍了七大面向对象设计原则。通过创建型、结构型和行为型模式的分类,帮助开发者更好地理解和应用这些模式,提升代码质量和可维护性。 ... [详细]
  • C++: 实现基于类的四面体体积计算
    本文介绍如何使用C++编程语言,通过定义类和方法来计算由四个三维坐标点构成的四面体体积。文中详细解释了四面体体积的数学公式,并提供了两种不同的实现方式。 ... [详细]
  • C++构造函数与初始化列表详解
    本文深入探讨了C++中构造函数的初始化列表,包括赋值与初始化的区别、初始化列表的使用规则、静态成员初始化等内容。通过实例和调试证明,详细解释了初始化列表在对象创建时的重要性。 ... [详细]
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社区 版权所有