基本术语

  • 不变量:由程序作出的 假设,是程序中显式/隐式的一种 不变 的关系

    举例来说,当我们有一个链表的时候,此时的不变量就可以是 每个节点都有一个指向下一个节点的next指针,所以当你删除或者增加节点时,需要维护这个指针

  • 临界区:有时也叫 串行区域,指代影响 共享数据 的代码段,每一个临界区都至少对应一个数据不变量, 反之亦然

  • 谓词:指代描述代码所需不变量的状态的语句,比如 当队列为空时,线程A...,谓词可以是布尔量、指针、复杂表达式等等,能够指示对所需不变量的状态要求即可

同步机制

同步其实就是在维护不变量,不变量经常会被破坏,而同步需要做的就是在被访问之前将其修复

原子操作

原子,即指不可分割

原子操作就是不可分割的操作,比如 x++ 这句代码,虽然只有一句,但他并非不可分割,也就是说并不是原子的,它会先将x 读入寄存器,然后再 加1,然后再 写回内存,如果在此期间有其他线程访问x则可能会出现非预期的结果

原子操作的实现:

  • 硬件支持
  • 互斥量

互斥量

互斥量基本概念

确保共享数据一次只能有 一个 线程访问,其他线程需要等待其释放,这可以通过 加锁解锁 实现

通常将互斥量和它要保护的数据定义在一起,如下:

struct tag {
  pthread_mutex_t mutext;
  int value;
};

当没有线程在这个互斥量上阻塞时,且该互斥量没有被加锁,那么可以立即释放它

互斥量的设计

互斥量的大小:并非指占用内存空间,指代的是 保护的串行区域保护的共享数据

  • 互斥量并非免费的,加锁和解锁会对性能造成影响,所以互斥量要 尽量少,保护的 串行区域要尽量大

  • 互斥量本质是 串行执行,当保护的 串行区域 过大时,多个线程频繁访问会造成大量的等待时间(一次只能一个线程访问),所以互斥量要 足够多,每个互斥量保护的 串行区域要尽量小

这两种策略给出了各自的缺点,实际编程中可以从这两方面去考虑互斥量的设计

死锁

1

如上图所示,两个进程互相等待对方释放资源,形成了一个循环

死锁发生通常需要满足以下四个条件:

  • 互斥条件:每个资源都只能被一个线程占用

  • 持有并等待条件:一个线程持有至少一个资源,并等待获取其他资源,这些资源当前被其他线程持有

  • 不剥夺条件:线程已获得的资源不能被强制剥夺,必须由线程自行释放

  • 循环等待条件:在一个资源的请求链中,形成了一个循环等待的情况

解决办法:(破坏四个条件中的一个即可)

  • 固定加锁层次固定加锁顺序,要锁住B,必须先锁住A

  • 试加锁和回退:在 正常锁住 集合中的某一个互斥量后,尝试锁住 集合中的其他互斥量,如果失败,则 释放 所有锁住的互斥量,重新加锁

拓展:

  • 层次锁:通过 定义锁的层次关系 来避免死锁的策略。在这种策略下,系统会对所有锁按某种顺序进行排列,并要求线程按照从低到高的顺序获取锁

  • 链锁:在链锁中,线程获取锁的顺序不是固定的,而是根据 资源之间的“链”或依赖关系 来动态决定(可以释放之前的互斥量

特性 层次锁 (Hierarchical Locking) 链锁 (Chain Locking)
死锁避免 通过严格的锁顺序避免死锁 通过动态调整锁顺序,依赖关系明确时也能避免死锁
灵活性 较低,锁的顺序是固定的 高,锁顺序根据资源依赖关系动态决定
实现难度 相对简单,易于理解和实现 较复杂,需要动态管理锁的依赖关系
适用场景 适合资源间依赖关系较简单且需要明确顺序获取锁的场景 适合资源间依赖关系复杂、动态调整锁获取顺序的场景

条件变量

概念

条件变量:

  • 是用来通知共享数据状态信息的

  • 是与互斥量以及互斥量保护的共享数据相关的信号

  • 是程序用来等待某个谓词为真的机制

  • 不提供互斥,所以需要和一个互斥量绑定

  • 每个条件变量都必须和一个特定的互斥量、一个谓词相关联

解锁和等待操作必须是原子的,这是因为,当线程A需要等待某个条件时,将互斥量解锁,然后陷入等待,如果这不是原子的,在解锁之后阻塞之前,线程B修改了状态,然后查找等待队列看是否需要唤醒其他线程,但此时线程A还未进入等待队列,所以就可能永远不会被唤醒(即使条件已经满足)。故引入了条件变量,它让二者是原子的

线程在等待条件变量时,它必须将相关互斥量锁住。在线程阻塞之前,条件变量操作将解锁互斥量,在它重返线程之前,它会再次锁住互斥量

条件变量在任何时刻只能和一个互斥量相关联,反之不然

条件变量必须在没有任何线程在等待它时销毁。并且通常是 先销毁条件变量,再销毁互斥量

信号与广播

  • 信号

    唤醒 一个 正在等待条件变量的线程。如果有多个线程在等待条件变量,只有其中一个线程会被唤醒

  • 广播

    唤醒 所有 等待条件变量的线程。所有等待的线程都会被唤醒,并且它们会竞争获得互斥锁,并继续执行

在信号和广播前,应该先锁住互斥量,如果不锁住很可能会造成信号丢失

首先,我们要知道,当没有线程等待条件变量时,pthread_cond_signal 不会做任何事情,并且它不会保存信号。也就是说,信号是一次性的。假设线程A没有锁住互斥量,直接更改条件并发送信号,而线程B很有可能在线程A发送信号之后还没有进入等待队列,因此 信号丢失,永远陷入阻塞等等

所以在信号和广播前锁住互斥量可以保证执行的顺序(即 线程A在线程B释放锁陷入等待之后才会发送信号

测试谓词

总是测试你的谓词,然后再次测试它

采用 循环 来检查条件,而不是简单地等待一次通知

while (!condition) {
    cond_wait(&cond, &mutex);
}

原因:

如果线程不在循环中检查条件,那么它可能会在条件未满足时继续执行,这可能导致竞争条件或不一致的状态。使用循环可以确保每次线程被唤醒时,只有在条件满足时才继续执行,从而避免了潜在的问题

  • 假唤醒:线程等待条件变量时,可能会无缘无故被唤醒,即使没有其他线程显式地通知它

总结

建议你将一组不变量、谓词和他们的互斥量,以及一个或多个的条件变量封装为一个数据结构的元素,并仔细地记录下他们之间的关系

 posted on 2025-03-16 14:09  Dylaris  阅读(30)  评论(0)    收藏  举报