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

线程的同步与互斥(死锁的产生和避免)

可以知道,一条语句对一个变量进行+1操作,转成汇编指令共有三条:将这个变量从内存中取出;将其值加1;再将加后的结果放回内存;当一个进程中的两个线程同时进行这个操作时,本来期望的是将

    可以知道,一条语句对一个变量进行+1操作,转成汇编指令共有三条:将这个变量从内存中取出;将其值加1;再将加后的结果放回内存;当一个进程中的两个线程同时进行这个操作时,本来期望的是将变量进行两次加1,但中途有可能当一个线程刚从内存中将变量取出就被切换暂停了,此时线程会保存硬件上下文,第二个线程将变量加1之后前面切出去的线程回来继续执行,这时保存的还是变量原来的值,再将变量加1,会发现变量的最终结果并没有加2而是只加了1,因此这种操作并不是原子的。

-------------------------------------------------------------------------------------------

栗子时间:

wKioL1cVy6HROe7OAACdTkNhKUY517.png


上面的程序中创建了两个线程执行同一个函数,都是将全局变量的值从0加到500,预期的结果应该是1000;但是运行程序,会发现结果不为预期那样,有时是500,有时是500多或者600多,这就是两个线程在访问同一块数据代码时产生了冲突,因为操作不是原子的,所以会有中间值的产生。

wKioL1cV1iqCRnXsAAAEs_hdVKA068.png

wKiom1cV1W6zTAvPAAAE05A1OJw094.png

wKioL1cV1i2j7iN5AAAEtfDLQcA612.png

-------------------------------------------------------------------------------------------

  1. 互斥量(mutex)

    要解决上面的问题,可以引入互斥量,在解决进程间通信所产生的冲突问题时,有一种机制就是信号量,互斥量和信号量基本上是相同的概念,就是当一个线程在访问某个数据时可以加上一把互斥锁,当别的线程也要访问这个数据时就要请求加锁,而此时锁已经被别的线程申请要过去了,那这个线程就需要等待,直到有锁可用才能再加锁访问数据,如此一来,就可以将“读取―执行―写入”这三部化成一个原子性的问题,要么都执行,要么一步也不执行,不会被中途打断。


  1. 互斥锁的初始化和销毁

wKiom1cVzqPR4dTYAAA7mtjrBAE164.png

当定义出了一个pthread_mutex_t类型的互斥锁之后,如果是全局变量或者static变量可以直接将其初始化为PTHREAD_MUTEX_INITIALIZER,相当于调用init的初始化函数来将其初始化;

函数参数中,

mutex是一个指向pthread_mutex_t类型的一个互斥量的指针;

attr可以设置mutex的属性,若为NULL直接用系统默认属性;

两个函数执行成功返回0,失败返回错误码;


b. 加锁与解锁

wKioL1cV0iXx6z7sAAAJNby1PVU531.png

函数参数为指向互斥量的一个指针;

当一个线程调用函数pthread_mutex_lock时,若锁已被其他线程申请使用,则该线程需要挂起等待,直到锁被使用完pthread_mutex_unlock释放回来,该线程才被唤醒继续申请锁;如果不想未获得锁挂起等待,可以调用pthread_mutex_trylock函数,如果锁已被其他线程获得,这个函数会失败返回EBUSY而不会挂起等待。

-------------------------------------------------------------------------------------------

栗子时间:

wKioL1cV1VHw3CNyAAC_EwdgGq4816.png


在公共的函数中加入了互斥锁,当一个线程进行循环体时,另一个线程要进入就要申请锁,此时锁已将被占用,需要挂起等待直到锁释放,这样两个线程就不会引起冲突而将数据加1的过程转换成原子性的;运行程序会得到结果始终稳定为预期值1000:

wKiom1cV1JOgvJbxAAAU_iIvsfY157.png


上面的栗子中,加锁和解锁是在while循环体外部进行的,也就是一个线程进入函数内部之后申请得到锁,完成了500次加1之后退出循环再释放锁;其实也可以将锁的申请和释放加在循环体的内部,只是这样加锁和释放锁的次数就随着每一次循环而进行一次;因此,加锁的粒度可以依据不同的需求和场景而定。

-------------------------------------------------------------------------------------------

2. 死锁

    上面谈论到加锁和解锁,试想,如果一个线程连续两次申请锁,当其第一次申请的时候获得了这把锁,而第二次申请的时候因为锁被占用着会挂起等待,而占用这把锁的正是自身,那么该线程将永远不会释放锁而会一直处于挂起等待的状态;还有,如果线程A获得了一把锁,线程B获得了另一把锁,而线程A还需要再获得线程B所拥有的那把锁才能继续往下执行,同样,线程B也正需要线程A占用的那把锁,但这时两个线程都会挂起等待对方释放那把需要的锁,这样一来两个线程也就一直僵持着处于挂起状态了;像上面所说的这两种情况,也就是两个进程或线程为了争夺资源而造成互相挂起等待,就是死锁

产生死锁的四个必要条件
(1) 互斥条件:资源不能被共享,一个资源每次只能被一个进程使用;
(2) 请求与保持条件:已经得到资源的进程可以再次申请新的资源;
(3) 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺;
(4) 循环等待条件:若干进程之间形成环路,环路中的每个进程都在等待相邻进程正占用的资源。

上述说的是进程,对线程也同样适用;

    因此,只要避免了上述条件,也就可以避免死锁,在一个线程中应避免同时获得多个锁,如若必须,则有一个原则:如果所有线程在需要多个锁是都按相同的先后顺序获得锁,就不会出现死锁的情况。比如一个线程需要获得锁1、锁2、锁3,那么其他线程也需要按相同的顺序来获得锁;如果不按顺序,也可以调用pthread_mutex_trylock来代替pthread_mutex_lock来获取锁。


-------------------------------------------------------------------------------------------

栗子时间:

wKiom1cXRtazkFjgAACh5v5Cu2A801.jpg

wKioL1cXR5rArB1xAAAtW2NjumI754.png


上面程序中定义了两个线程分别执行不同的线程函数,在线程1中先获取互斥锁a,然后再获取锁b,在线程2中先获取锁b再获取锁a,执行结果如下:

wKiom1cXRtqhtnBFAAAISjA6cO4748.png


可以看到程序运行结果就是两个线程都卡住不动了,也就是进入了死锁状态,因为两个线程获取了各自占有了对方所需的锁而导致双方一直僵持挂起着,这也是满足了上面所说的满足了产生死锁的四个必要条件。

将程序中两个线程函数申请获得锁的顺序改为一致,也就是都按先申请锁a再申请锁b的方式,或者将函数thread_mutex_lock改为thread_mutex_trylock,这样就不会产生死锁了。

-------------------------------------------------------------------------------------------

3. 条件变量(condition variable)

    前面提到互斥的概念就是某一时段只能有一个进程或线程访问某个资源,而同步就是有顺序性的执行访问某个资源,比如一个线程需要满足某个条件成立才能继续往下执行,条件不成立就会阻塞等待,而此时另一个线程在执行过程中使这个条件成立了,那该线程就会被唤醒继续执行。

pthread库中通过条件变量来阻塞等待一个条件,或者唤醒等待这个条件的线程,

  1. 该类型变量的初始化和销毁如下:

wKiom1cV3kHysZ61AAANStiUBIU537.png

和mutex的初始化和销毁类似,当定义了一个类型为pthread_cond_t的条件变量时,如果为全局的或者static类型的可以直接初始化为PTHREAD_COND_INITALIZER,和使用初始化函数一样;

函数参数中,

cond为pthread_cond_t类型的一个指针;

attr同样是设置条件变量的属性,可以为NULL使用默认属性;


b. 条件变量的等待与唤醒

wKiom1cV4oLzdoWZAAA0guwAB1o196.png

函数参数中,可以看到一个条件变量总是和一个互斥量搭配使用;

当一个线程调用wait函数阻塞等待时,一般会做以下三步操作:

  1. 释放mutex

  2. 阻塞等待

  3. 当被唤醒时,重新获得mutex并返回

pthread_cond_timedwait函数参数中,abstime可以设定等待的时间,若等待超时任然没有别的线程来唤醒当前线程,就会返回ETIMEDOUT;

wKioL1cV40GBDRrOAAAIqSkcZXg003.png

可以调用pthread_cond_signal函数来唤醒某个正在等待的线程;

也可以调用pthread_cond_boradcast函数来唤醒所有等待的线程;


-------------------------------------------------------------------------------------------

栗子时间:

wKioL1cV9XzRaGpIAAA0B6iUYHs627.png

wKiom1cV9L2TAIA2AAAM65nOBSo522.png


上面栗子中,一方作为接受信息方另一方作为发送信息方,当发送方未编辑好信息时接收方需要等待,发送方编辑好信息后唤醒接收方接收信息,运行程序如下:

wKioL1cV9X_gjVN_AAAUeRMv8ks975.png


--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

总结:

  1. 当一个进程中的多个线程对某个资源进行访问不能保证原子性时,可以使用互斥量来为该资源加上互斥锁保证结果的原子性;

  2. 一个线程不能连续两次申请互斥锁,也应该避免多个线程申请多个不同的锁,否则就会出现死锁的问题,避免方法就是:要么所有线程都按照相同的申请锁的顺序申请互斥锁,要么就用非阻塞方式申请锁;

  3. 条件变量可以使进程之间同步,一个线程可以满足另一个线程需要的条件而唤醒等待的线程,要和互斥量搭配使用。




《完》

本文出自 “敲完代码好睡觉zzz” 博客,请务必保留此出处http://2627lounuo.blog.51cto.com/10696599/1765473


推荐阅读
  • PHP 5.5.0rc1 发布:深入解析 Zend OPcache
    2013年5月9日,PHP官方发布了PHP 5.5.0rc1和PHP 5.4.15正式版,这两个版本均支持64位环境。本文将详细介绍Zend OPcache的功能及其在Windows环境下的配置与测试。 ... [详细]
  • andr ... [详细]
  • 深入理解Java泛型:JDK 5的新特性
    本文详细介绍了Java泛型的概念及其在JDK 5中的应用,通过具体代码示例解释了泛型的引入、作用和优势。同时,探讨了泛型类、泛型方法和泛型接口的实现,并深入讲解了通配符的使用。 ... [详细]
  • 本文介绍了如何通过 Maven 依赖引入 SQLiteJDBC 和 HikariCP 包,从而在 Java 应用中高效地连接和操作 SQLite 数据库。文章提供了详细的代码示例,并解释了每个步骤的实现细节。 ... [详细]
  • 本文详细介绍了Java中的访问器(getter)和修改器(setter),探讨了它们在保护数据完整性、增强代码可维护性方面的重要作用。通过具体示例,展示了如何正确使用这些方法来控制类属性的访问和更新。 ... [详细]
  • 使用Vultr云服务器和Namesilo域名搭建个人网站
    本文详细介绍了如何通过Vultr云服务器和Namesilo域名搭建一个功能齐全的个人网站,包括购买、配置服务器以及绑定域名的具体步骤。文章还提供了详细的命令行操作指南,帮助读者顺利完成建站过程。 ... [详细]
  • 本文介绍如何使用阿里云的fastjson库解析包含时间戳、IP地址和参数等信息的JSON格式文本,并进行数据处理和保存。 ... [详细]
  • 精选30本C# ASP.NET SQL中文PDF电子书合集
    欢迎订阅我们的技术博客,获取更多关于C#、ASP.NET和SQL的最新资讯和资源。 ... [详细]
  • MySQL 数据库迁移指南:从本地到远程及磁盘间迁移
    本文详细介绍了如何在不同场景下进行 MySQL 数据库的迁移,包括从一个硬盘迁移到另一个硬盘、从一台计算机迁移到另一台计算机,以及解决迁移过程中可能遇到的问题。 ... [详细]
  • 本文详细介绍了如何在Ubuntu系统中下载适用于Intel处理器的64位版本,涵盖了不同Linux发行版对64位架构的不同命名方式,并提供了具体的下载链接和步骤。 ... [详细]
  • 汇编语言等号伪指令解析:探究其陡峭的学习曲线
    汇编语言以其独特的特性和复杂的语法结构,一直被认为是编程领域中学习难度较高的语言之一。本文将探讨汇编语言中的等号伪指令及其对初学者带来的挑战,并结合社区反馈分析其学习曲线。 ... [详细]
  • ASP.NET MVC中Area机制的实现与优化
    本文探讨了在ASP.NET MVC框架中,如何通过Area机制有效地组织和管理大规模应用程序的不同功能模块。通过合理的文件夹结构和命名规则,开发人员可以更高效地管理和扩展项目。 ... [详细]
  • 深入解析 Spring Security 用户认证机制
    本文将详细介绍 Spring Security 中用户登录认证的核心流程,重点分析 AbstractAuthenticationProcessingFilter 和 AuthenticationManager 的工作原理。通过理解这些组件的实现,读者可以更好地掌握 Spring Security 的认证机制。 ... [详细]
  • 微软Exchange服务器遭遇2022年版“千年虫”漏洞
    微软Exchange服务器在新年伊始遭遇了一个类似于‘千年虫’的日期处理漏洞,导致邮件传输受阻。该问题主要影响配置了FIP-FS恶意软件引擎的Exchange 2016和2019版本。 ... [详细]
  • C++构造函数与初始化列表详解
    本文深入探讨了C++中构造函数的初始化列表,包括赋值与初始化的区别、初始化列表的使用规则、静态成员初始化等内容。通过实例和调试证明,详细解释了初始化列表在对象创建时的重要性。 ... [详细]
author-avatar
断肠人blogma_791
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有