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


推荐阅读
  • 不知道你是否还记得之前在进程中的信号处理时,提到过阻塞信号集与未决信号集的概念,如果你已经忘记了,请参考《阻塞信号与未决信号》一文回忆一下 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 模块化区块链生态系统的优势概述及其应用案例
    本文介绍了相较于单体区块链,模块化区块链生态系统的优势,并以Celestia、Dymension和Fuel等模块化区块链项目为例,探讨了它们解决可扩展性和部署问题的方案。模块化区块链架构提高了区块链的可扩展性和吞吐量,并提供了跨链互操作性和主权可扩展性。开发人员可以根据需要选择执行环境,并获得奖学金支持。该文对模块化区块链的应用案例进行了介绍,展示了其在区块链领域的潜力和前景。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • BZOJ1233 干草堆单调队列优化DP
    本文介绍了一个关于干草堆摆放的问题,通过使用单调队列来优化DP算法,求解最多可以叠几层干草堆。具体的解题思路和转移方程在文章中进行了详细说明,并给出了相应的代码示例。 ... [详细]
  • JVM的方法执行引擎模板表
    Java的模板解析执行需要模板表与转发表的支持,而这2个表中的数据在HotSpot虚拟机启动时就会初始化。这一篇首先介绍模板表。在启动虚拟机阶段会调用init_globals()方 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • C++中的三角函数计算及其应用
    本文介绍了C++中的三角函数的计算方法和应用,包括计算余弦、正弦、正切值以及反三角函数求对应的弧度制角度的示例代码。代码中使用了C++的数学库和命名空间,通过赋值和输出语句实现了三角函数的计算和结果显示。通过学习本文,读者可以了解到C++中三角函数的基本用法和应用场景。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • Linux 中使用 clone 函数来创建线程
    2019独角兽企业重金招聘Python工程师标准Linux上创建线程一般使用的是pthread库实际上libc也给我们提供了创建线程的函数那就是cloneintclone(i ... [详细]
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社区 版权所有