本书的原著为:《Design Patterns for Embedded Systems in C ——An Embedded Software Engineering Toolkit 》,讲解的是嵌入式系统设计模式,是一本不可多得的好书。

本系列描述我对书中内容的理解。本文章描述嵌入式并发和资源管理模式之八:同时锁定模式。

同时锁定模式 (Simultaneous Locking Pattern) 是一种避免死锁模式。在并发编程中,避免死锁模式 是为了防止死锁(Deadlock)的发生而采用的一系列设计策略。

在开始了解同时锁定模式之前,有必要弄明白什么是 死锁

死锁 是并发编程中的一个重要问题,它可能导致系统资源无法被有效利用,甚至导致整个系统的失效。当多个任务或线程相互等待对方释放资源时,就可能发生死锁。例如,如果任务 A 持有资源 1 但请求资源 2,而任务 B 持有资源 2 但请求资源 1,那么这两个任务就会陷入死锁状态,因为它们都在等待对方释放自己所需的资源。

不考虑软件错误的问题,死锁通常发生在以下四个条件同时满足时,这被称为死锁的四个必要条件( Coffman 条件):

  1. 互斥条件:进程对所分配到的资源进行排它性使用,即一次只有一个进程能够使用。如果其他进程请求该资源,请求进程只能等待,直到资源被释放。
  2. 持有并等待条件:一个进程至少持有一个资源,并且正在等待获取其他进程持有的额外资源。
  3. 不可剥夺条件:资源不能被强制从一个进程中夺走,它必须被持有它的进程显式地释放。
  4. 环路等待条件:存在一个进程等待环路,即 P1 等待 P2 持有的资源,P2 等待 P3 持有的资源,…,Pn 等待 P1 持有的资源。

为了避免死锁,只需破坏四个必要条件中的任何一个即可确保死锁不会出现。同时锁定模式有序锁定模式临界区模式 都可以避免死锁问题。例如:

  • 同时锁定模式:确保任务在需要多个资源时能够一次性获取所有所需的资源,从而避免 持有并等待条件 (条件 2)。
  • 有序锁定模式:要求任务按照特定的顺序请求资源,这可以消除 环路等待条件 (条件 4)。
  • 临界区模式:通过限制任务切换来防止死锁的发生。

同时锁定模式 通过打破条件 2 (持有并等待条件) 来实现避免死锁的目的。在这种模式中,进程在尝试执行其任务之前,会先尝试获取其所需的所有资源。只有当它能够一次性获得所有必要的资源时,它才会开始执行。如果不能同时获得所有资源,那么进程会先是否已经获取的资源,然后等待,直到所有资源都可用为止。

摘要

死锁可以通过打破四个必要条件中的任何一个来解决。同时锁定模式 要么一次性获取全部资源,要么一个资源也不持有。与 临界区模式 相比,该模式的优势在于,其它不需要资源的更高优先级任务可以正常运行。而临界区模式会禁止任务切换,这样会影响到所有的任务。

问题

在高度可靠的计算环境中,死锁问题被视为一个极为严重的问题,因此许多系统都专门设计了相应的机制来检测或预防它的发生。如前所述,死锁是指任务在等待一个理论上永远无法满足的条件时陷入的僵局。而要避免死锁的发生,只需破坏其四个必要条件中的任意一个即可。同时锁定模式 便是一种有效的策略,它要么一次性获取全部资源,要么一个资源也不持有,这样就禁止了任务只锁定部分资源,从而打破了死锁的第 2 个必要条件。

模式结构

通用同时锁定模式结构图如下所示:
通用的同时锁定模式
通常,一个 多主资源 代表任意数量的不同资源集合中的一部分。需要资源的 客户端 不能直接访问 多主资源多主资源 集合由 资源掌控者 管理。当客户端需要同时锁定资源时,它首先会和 资源掌控者 进行沟通,确定资源的当前状态。比如,查询到资源全部可用,资源掌控者 会锁定资源,然后将资源分配给客户端使用。

上面所示的结构图中,一个 多主资源 可以被多个 资源掌控者 管理 (这也是它叫做“多主”的原因),这种资源共享使得情况变得复杂。如果不需要这种灵活性时,可以将模式结构简化为下图所示的情况:

简化的同时锁定模式
简化的 同时锁定模式 消除了 互斥锁 类中 tryLock() 函数的需求,从算法角度来看,简化后的模式变得简单的多。简化的资源掌控者 直接 代理 了它所管理的资源,现在多个资源集中到了一处,即资源管理者的手中。这简化了操作逻辑:当客户端需要同时锁定多个资源时,它只需要锁定 简化的资源掌控者 即可。

模式详情

单主资源 (MasteredResource)

单主资源 类是相对于 多主资源 类而言的,用于简化版的 同时锁定模式单主资源 表示本资源只能由一个 资源掌控者 所管理。这个条件简化了设计,在这种情况下,资源不需要互斥锁,因为它是在 简化的资源掌控者 的互斥锁保护下执行的。如果资源必须在多个 资源掌控者 中共享,那么它就不是 单主资源 ,而会变成 多主资源,这是更通用的 同时锁定模式 的一部分。

多主资源 (MultimasteredResource)

多主资源 用于通用的 同时锁定模式多主资源 可以由多个 资源掌控者 所管理,因此它需要自己的互斥锁。它使用的互斥锁必须提供 tryLock() 函数。tryLock() 尝试获取锁但不会等待,如果锁已经被其他实体持有,它通常会立即返回失败状态。所以 资源掌控者 可以使用 tryLock() 函数检查是否可以成功获得所有所需的锁。如果任何一个锁无法获得,资源掌控者 就可以放弃尝试并释放它已经获得的锁,从而避免死锁的发生。所以,带有 tryLock() 函数的互斥量,也称为 查询互斥量

查询互斥量 (QueryMutex)

查询互斥量 也是一个普通的 互斥信号量,但它为通用的 同时锁定模式 提供了 tryLock() 函数。这个函数的工作方式与普通的 lock() 函数类似,不同之处在于,如果锁定失败,它会返回一个错误代码,而不是阻塞当前线程。在简化的 同时锁定模式 中,则不需要 tryLock() 函数。

资源掌控者 (Resource Master)

资源掌控者 用于通用的 同时锁定模式资源掌控者 控制着整个资源集合的锁,只有锁定所有资源时,才返回锁。因为 资源掌控者 在尝试锁定各个 多主资源 集合时,可能会多次阻塞。在实践中,这可能会导致不可接受的执行时间。解决方案是使用 查询互斥量 ,它提供的 tryLock() 函数可以非阻塞地测试资源是否已经被其他实体持有。如果所有 多主资源 集合的 tryLock() 函数都能成功调用,则 资源掌控者 本身被锁定,并允许 客户端 使用所需的多个资源。如果 资源掌控者 无法锁定所有 多主资源,则 资源掌控者 会解锁它锁定的所有 多主资源,并向客户端返回一个错误代码。

简化的资源掌控者

简化的资源掌控者 用于简化的 同时锁定模式 。在简化的模式中,客户端本身不需要知道资源的详细细节。取而代之的是,简化的资源掌控者 代理了 单主资源 的所有服务,所有资源服务集中到了 简化的资源掌控者 手中,这些服务共享一个互斥锁。

客户端

客户端希望一次性访问一组资源以避免潜在的死锁。尽管客户端知道资源的细节,但在成功获得 资源掌控者 的锁之前,它不会访问这些资源。这要求客户端的开发者遵守规范

  1. 没有获取 资源掌控者 的锁之前,禁止访问任何资源;
  2. 使用完资源后,立即释放 资源掌控者 的锁。

效果

同时锁定模式 是一种通过一次性锁定所需全部资源或完全不锁定任何资源的策略,从而消除了死锁产生的两个必要条件之一:即在部分资源被锁定时请求其他资源。然而,尽管该模式有效地避免了死锁的发生,但它也可能导致其他任务的执行出现延迟。这种延迟甚至可能在没有实际资源冲突的情况下发生,特别是在广泛共享的资源中,这一问题更为显著。

此外,同时锁定模式并未解决优先级反转问题,甚至可能加剧这一问题,除非结合使用其他优先级反转限制模式,如 带优先级继承的保护调用模式 。在实际应用中,开发者需要严格遵守规范,避免在成功获得锁之前直接访问 多主资源 ,以确保模式的正确实施。然而,在大型项目团队中,这种规范有时可能被忽视,导致难以察觉的细微错误。

相比之下,简化模型通过 简化的资源掌控者 重新发布资源访问方法,有效地避免了上述问题。

实现策略

实现这种策略需要注意两个方面:

  1. 需要使用支持 tryLock() 函数的互斥信号量
  2. 遵循规范:在访问资源前满足锁定条件。

实例

见原书。






读后有收获,资助博主养娃 - 千金难买知识,但可以买好多奶粉 (〃‘▽’〃)
千金难买知识,但可以买好多奶粉

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐