聊聊高并发(三)锁的一些基本概念

本文介绍了并发编程中锁的基本概念,包括互斥性、无死锁和无饥饿等特性,并探讨了如何通过减小锁的粒度提高并发效率,以及通过Peterson算法和Bakery锁实现无死锁和无饥饿的互斥协议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

理解并发编程的一些基本概念很重要,给我们思考问题指明一个基本的方向。这篇说一说锁的一些基本概念。

在通常情况下我们说的锁都指的是“互斥”锁,因为在还存在一些特殊的锁,比如“读写锁”,不完全是互斥的。这篇文章说的锁专指互斥锁。


锁是处理并发的一种同步手段。单线程程序和并发程序的最终目的都是要保证程序的正确性,但是最大的区别是:

  • 单线程程序的正确性只关注程序的运行结果和目标是一致的
  • 并发程序的正确性除了运行结果正确外,还包含了活性的特性,所谓活性,指的就是程序无死锁,无饥饿

所以考察一个锁,也需要从三个方面考察:

1. 互斥性

2. 无死锁

3. 无饥饿


最简单的锁只保证互斥性,而互斥性本质上可以用一个布尔值表示,即一个二元状态。

互斥是保证并发程序正确性的一种特性,和互斥相关的一个专用名词就是临界区

临界区指的是“某个时刻仅能被一个线程执行的代码段”,也就是通常锁的被锁保护的代码段。


一个互斥锁的定义通常如下

interface Lock {
     public void lock();
     public void unlock();
}

线程必须用指定的方式使用锁,lock动作必须在try块之前调用,如果lock在try里面执行,可能会在取到锁之前抛出异常,导致执行了unlock动作,从而发生错误。

熟悉Java显示锁的同学肯定知道使用ReentryLock就是如下的用法。

mutex.lock();
try{
 ...临界区
}finally{
    mutex.unlock()
}

互斥意味着串行,也意味着等待。 这引出了著名的Amdahl定律

Amdahl定律: 即完成一个工作能获得的加速比,受限于这个工作中必须被串行的部分。(通常串行部分都是因为被互斥锁保护了)

加速比的定义是一个处理器完成一个工作的时间和采用n个处理器并发完成该工作的时间比。

Amdahl定律给出的加速比如下

S = 1 / ( 1 - p + p/n)

S为加速比
1为完成工作的时间
p指可以并行的部分
n指处理器个数

从Amdahl定律可以看出,串行的工作越多,获得的加速比就越小。

Amdahl给我们编程实际启示有:

1. 尽量减小互斥锁的粒度,锁粒度越小表示串行的部分越少

2. 能不用锁,就不要用锁。不用锁表示串行的部分越少


接下来说说活性相关的概念。

死锁意味者系统冻结最终相关的所有线程都永久地停滞等待。

饥饿则是总有一些线程能够运行一小部分线程永久停滞等待


所以无饥饿意味着肯定无死锁。但是无死锁不意味着无饥饿。


《多处理器编程的艺术》一书中给出了几种锁的实现,其中Peterson算法可以保证两个线程使用锁的时候锁具备互斥,无死锁,无饥饿特性。

class Peterson implements Lock {
     private boolean[] flag = new boolean[2];
     private int victim;
     public void lock(){
           int i = ThreadID.get();
           int j = 1 - i;
           flag[i]= true;  // 保证两个线程先后运行不死锁,实现互斥 
           victim = i;  // 保证两个线程同时运行时不死锁,实现互斥
           while(flag[j] && victim == i){}  // 互斥意味着等待
     }

     public void unlock(){
           int i = ThreadID.get();
           flag[i] = false;
    }

}

Bakery锁支持n个线程的互斥协议。通过数学证明了:

n线程的无死锁互斥算法需要n个不同的存储单元(变量)来保存中间状态。






<think>首先,用户的问题是:“我想了解互斥的概念应用 请问互斥是什么 如何使用互斥解决并发问题”。根据系统级指令,我需要: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如[^1]。 - 回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。 - 参考站内引用:我有几个引用:[^1], [^2], [^3], [^4], [^5],这些是关于互斥的引用。我需要自然地融入这些引用。 引用内容总结: - [^1]: 介绍了使用互斥实现线程互斥访问,避免竞态条件数据不一致。 - [^2]: 互斥的特点:独占性、阻塞等待、原子性。 - [^3]: 在Linux中,互斥的使用,locktrylock的区别。 - [^4]: 互斥保护共享资源,如get()addOne()方法需要访问受保护的资源。 - [^5]: 在嵌入式系统中使用互斥信号量的伪代码。 我的回答应该覆盖互斥的概念应用,并使用这些引用支持。 回答结构: 1. **介绍互斥的概念**:解释什么是互斥,为什么需要它。 2. **互斥的工作原理**:基于引用描述其特点。 3. **如何使用互斥解决并发问题**:给出例子或伪代码,参考引用。 4. **应用场景**:讨论互斥的应用,如多线程编程。 5. **生成相关问题**:在结束时添加相关问题。 使用中文回答。 确保LaTeX正确:用户提到数学表达式,但互斥主题可能不涉及太多数学。如果有,如描述原子操作,可能需要数学表达式。例如,原子性可以用数学表示,但可能不需要。保持简单。 步骤: - **开头**:直接回答用户问题,介绍互斥。 - **主体**:分步骤解释概念应用。 - **结尾**:总结并生成相关问题。 融入引用: - 在解释概念时,引用[^1][^2]。 - 在讨论使用时,引用[^3][^4]。 - 在嵌入式应用,引用[^5]。 生成相关问题:像示例一样,在最后添加“§§相关问题§§”部分,列出几个问题。 最终输出结构: - 回答内容。 - §§相关问题§§ - 相关问题列表。</think>### 互斥的概念应用 互斥(Mutex)是一种在多线程或多进程编程中用于实现并发控制的同步机制。它的核心目的是确保在任意时刻,只有一个线程或进程可以访问共享资源(如变量、文件或数据结构),从而避免竞态条件(race conditions)数据不一致的问题。当多个线程同时尝试修改共享数据时,如果没有互斥,可能会导致数据损坏或程序崩溃。互斥通过提供“独占访问”的机制来解决这一问题,类似于一个房间的门:只有持有钥匙的线程才能进入“临界区”(critical section),其他线程必须等待被释放后才能尝试获取[^1]。 #### 互斥的核心特点 互斥的工作原理基于几个关键特性,这些特性确保了其有效性可靠性: - **独占性**:当一个线程成功获取互斥后,其他线程无法同时获得该,必须等待被释放。这保证了共享资源在同一时间只被一个线程访问[^2]。 - **阻塞等待**:如果一个线程尝试获取已被其他线程持有的互斥,它会被阻塞(即进入等待状态),直到被释放。这避免了线程在资源冲突时继续执行,从而防止数据错误[^2]。 - **原子性**:互斥的获取(lock)释放(unlock)操作是原子的,意味着这些操作不会被中断或分割。这确保了操作的完整性一致性,不会出现部分执行导致的并发问题[^2]。 在数学上,互斥的原子性可以用一个简单的模型表示:假设的状态 $S$ 是一个二元变量(0表示未定,1表示定),则获取的操作可以描述为: $$ \text{lock}() : \begin{cases} \text{if } S = 0, & \text{set } S = 1 \\ \text{else}, & \text{wait} \end{cases} $$ 释放的操作则是: $$ \text{unlock}() : \text{set } S = 0 $$ 这些操作在底层硬件或操作系统支持下实现原子性,确保并发安全[^3]。 #### 如何使用互斥解决并发问题 使用互斥解决并发问题通常涉及以下步骤,我将通过一个简单的代码示例(基于C语言)来说明。假设我们有一个共享变量 `counter`,多个线程可能同时修改它,我们需要互斥来保护这个变量。 1. **定义初始化互斥**:在程序开始时,创建一个互斥对象。 2. **在临界区前获取**:在访问共享资源的代码段(临界区)前,调用获取函数。如果已被占用,线程会阻塞等待。 3. **执行临界区操作**:安全地修改共享资源。 4. **释放**:操作完成后立即释放,允许其他线程获取。 5. **处理的获取方式**:根据需求选择阻塞式(lock)或非阻塞式(trylock)。lock会让未抢到的线程等待,而trylock会立即返回状态(成功或失败),避免死风险[^3]。 以下是一个伪代码示例,演示如何用互斥保护一个计数器: ```c #include <pthread.h> pthread_mutex_t lock; // 定义互斥 int counter = 0; // 共享变量 void* increment_counter(void* arg) { for (int i = 0; i < 1000; i++) { pthread_mutex_lock(&lock); // 获取(阻塞式) counter++; // 临界区操作 pthread_mutex_unlock(&lock); // 释放 } return NULL; } int main() { pthread_mutex_init(&lock, NULL); // 初始化互斥 pthread_t thread1, thread2; pthread_create(&thread1, NULL, increment_counter, NULL); pthread_create(&thread2, NULL, increment_counter, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); pthread_mutex_destroy(&lock); // 销毁 printf("Final counter value: %d\n", counter); // 应输出2000 return 0; } ``` 在这个例子中,互斥确保 `counter++` 操作是原子的:线程1获取后执行自增,线程2必须等待释放后才能进入临界区。这样,最终结果总是2000,而不会因并发冲突导致数据错误(如输出小于2000)[^4]。 在实际应用中,互斥常用于: - **多线程数据共享**:如数据库操作、文件读写或缓存更新,确保数据一致性[^1]。 - **嵌入式系统**:在RT-Thread等实时操作系统中,结合信号量(semaphore)处理多路并发。例如,一个传感器采集线程获取互斥后,独占CPU资源读取数据,然后释放,避免多个传感器同时访问冲突[^5]。 - **分布式系统**:扩展到分布式,协调多个节点对共享资源的访问。 #### 注意事项最佳实践 - **避免死**:确保的获取释放成对出现,避免嵌套或长时间持有。 - **性能考虑**:操作可能引入开销,在高并发场景下,可优化为细粒度或使用无数据结构。 - **与信号量结合**:互斥(用于互斥)常与信号量(用于同步)配合,解决更复杂的并发问题,如生产者-消费者模型[^5]。 总之,互斥并发编程的基础工具,通过强制独占访问来保护共享资源,确保程序正确性可靠性[^1][^2]。在实际开发中,应根据语言平台(如Linux的pthread库或嵌入式RT-Thread)选择合适的实现。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值