linux锁

自旋锁(spinlock)很好理解。对自旋锁加锁的操作,你可以认为是类似这样的:

while (抢锁(lock) == 没抢到) {
}

只要没有锁上,就不断重试。显然,如果别的线程长期持有该锁,那么你这个线程就一直在 while while while 地检查是否能够加锁,浪费 CPU 做无用功。

仔细想想,其实没有必要一直去尝试加锁,因为只要锁的持有状态没有改变,加锁操作就肯定是失败的。所以,抢锁失败后只要锁的持有状态一直没有改变,那就让出 CPU 给别的线程先执行好了。这就是互斥器(mutex)也就是题目里的互斥锁(不过个人觉得既然英语里本来就不带 lock,就不要称作锁了吧)。对互斥器加锁的操作你可以认为是类似这样的:

while (抢锁(lock) == 没抢到) {
    本线程先去睡了请在这把锁的状态发生改变时再唤醒(lock);
}

操作系统负责线程调度,为了实现「锁的状态发生改变时再唤醒」就需要把锁也交给操作系统管理。所以互斥器的加锁操作通常都需要涉及到上下文切换,操作花销也就会比自旋锁要大。

以上两者的作用是加锁互斥,保证能够排它地访问被锁保护的资源。

不过并不是所有场景下我们都希望能够独占某个资源,很快你可能就会不得不写出这样的代码:

// 这是「生产者消费者问题」中的消费者的部分逻辑
// 等待队列非空,再从队列中取走元素进行处理

加锁(lock);  // lock 保护对 queue 的操作
while (queue.isEmpty()) {  // 队列为空时等待
    解锁(lock);
    // 这里让出锁,让生产者有机会往 queue 里安放数据
    加锁(lock);
}
data = queue.pop();  // 至此肯定非空,所以能对资源进行操作
解锁(lock);
消费(data);  // 在临界区外做其它处理

你看那个 while,这不就是自己又搞了一个自旋锁么?区别在于这次你不是在 while 一个抽象资源是否可用,而是在 while 某个被锁保护的具体的条件是否达成。

有了前面自旋锁、互斥器的经验就不难想到:「只要条件没有发生改变,while 里就没有必要再去加锁、判断、条件不成立、解锁,完全可以让出 CPU 给别的线程」。不过由于「条件是否达成」属于业务逻辑,操作系统没法管理,需要让能够作出这一改变的代码来手动「通知」,比如上面的例子里就需要在生产者往 queue 里 push 后「通知」!queue.isEmpty() 成立。

也就是说,我们希望把上面例子中的 while 循环变成这样:

while (queue.isEmpty()) {
    解锁后等待通知唤醒再加锁(用来收发通知的东西, lock);
}

生产者只需在往 queue 中 push 数据后这样,就可以完成协作:

触发通知(用来收发通知的东西);
// 一般有两种方式:
//   通知所有在等待的(notifyAll / broadcast)
//   通知一个在等待的(notifyOne / signal)

这就是条件变量(condition variable),也就是问题里的条件锁。它解决的问题不是「互斥」,而是「等待」。

至于读写锁(readers-writer lock),看英文可以顾名思义,在执行加锁操作时需要额外表明读写意图,复数读者之间并不互斥,而写者则要求与任何人互斥。读写锁不需要特殊支持就可以直接用之前提到的几个东西实现,比如可以直接用两个 spinlock 或者两个 mutex 实现:

void 以读者身份加锁(rwlock) {
    加锁(rwlock.保护当前读者数量的锁);
    rwlock.当前读者数量 += 1;
    if (rwlock.当前读者数量 == 1) {
        加锁(rwlock.保护写操作的锁);
    }
    解锁(rwlock.保护当前读者数量的锁);
}

void 以读者身份解锁(rwlock) {
加锁(rwlock.保护当前读者数量的锁);
rwlock.当前读者数量 -= 1;
if (rwlock.当前读者数量 == 0) {
解锁(rwlock.保护写操作的锁);
}
解锁(rwlock.保护当前读者数量的锁);
}

void 以写者身份加锁(rwlock) {
加锁(rwlock.保护写操作的锁);
}

void 以写者身份解锁(rwlock) {
解锁(rwlock.保护写操作的锁);
}

如果整个场景中只有一个读者、一个写者,那么其实可以等价于直接使用互斥器。不过由于读写锁需要额外记录读者数量,花销要大一点。

你可以认为读写锁是针对某种特定情景的「优化」。但个人还是建议忘掉读写锁,直接用互斥器。

### Linux 机制的实现与应用 在 Linux 环境下,机制主要用于解决多线程或多进程环境中的资源竞争问题。通过使用,可以确保多个线程或进程不会同时访问共享资源,从而避免数据不一致或其他并发问题。 #### 1. 常见的类型 Linux 提供了多种类型的机制以满足不同的需求,以下是一些常见的类型及其特点: - **自旋 (Spinlock)** 自旋是一种轻量级的,适用于短时间持有的情况。当一个线程尝试获取自旋时,如果已经被其他线程持有,则该线程会不断循环等待,直到被释放[^1]。这种机制适合于实时性要求较高的场景,但可能会浪费 CPU 资源。 - **互斥 (Mutex)** 互斥是一种更为通用的机制,允许线程阻塞并进入等待队列,而不是像自旋那样忙等待。当被释放时,等待队列中的线程会被唤醒并继续执行[^2]。互斥通常用于长时间持有的场景。 - **读写 (Read-write Lock)** 读写允许多个线程同时读取共享资源,但在写入时会独占,防止其他线程访问资源。这种机制适合于读操作远多于写操作的场景[^3]。 - **信号量 (Semaphore)** 信号量是一种同步机制,既可以用于互斥,也可以用于线程间的通信。它通过计数器来控制对共享资源的访问。在 Linux 内核中,信号量的操作函数定义在 `include/linux/semaphore.h` 中[^3]。 #### 2. 机制的实现细节 机制的实现依赖于底层硬件和操作系统提供的原语。以下是几种常见机制的实现方式: - **原子操作** 原子操作是实现的基础,它确保某些关键操作(如加法、减法)在多处理器环境下不可中断地完成。例如,在 x86 架构上,可以通过 `LOCK` 指令前缀来实现原子操作[^2]。 - **条件变量** 条件变量通常与互斥结合使用,用于线程间的协调。一个线程可以在条件变量上等待,直到另一个线程发出信号通知其继续执行。 - **内核抢占机制** 在多线程环境中,内核抢占机制允许高优先级的任务打断低优先级任务的执行。这有助于减少的竞争时间,提高系统的整体性能[^2]。 #### 3. 机制的应用场景 机制广泛应用于各种需要并发控制的场景,以下是一些典型的应用: - **文件系统** 文件系统中使用来保护元数据和数据块的完整性,防止多个进程同时修改同一文件导致数据损坏。 - **网络协议栈** 网络协议栈中的用于保护连接状态、缓冲区等共享资源,确保多线程环境下的安全性。 - **驱动程序** 驱动程序中常用信号量或互斥来保护设备寄存器的访问,避免多线程并发访问导致设备行为异常[^3]。 #### 示例代码:互斥的使用 以下是一个简单的 C 语言示例,展示了如何使用互斥保护共享资源: ```c #include <pthread.h> #include <stdio.h> pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; int shared_resource = 0; void* increment(void* arg) { pthread_mutex_lock(&mutex); // 加 shared_resource++; printf("Incremented: %d\n", shared_resource); pthread_mutex_unlock(&mutex); // 解 return NULL; } int main() { pthread_t thread1, thread2; pthread_create(&thread1, NULL, increment, NULL); pthread_create(&thread2, NULL, increment, NULL); pthread_join(thread1, NULL); pthread_join(thread2, NULL); return 0; } ``` ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值