🔥 读写锁(Read-Write Lock)是一种用于多线程环境下同步访问共享资源的锁。它与传统的互斥锁(Mutex
)有所不同,提供了更细粒度的控制,以便提高并发性能。它允许多个线程同时 读取 数据,但在写入数据时,必须确保只有一个线程可以进行写操作,并且在写操作期间,所有的读操作都必须等待。
💧 读写锁的核心思想是:读操作之间是可以并发执行的,而写操作是独占的,即不能与其他读操作或者写操作同时执行
🐇 读写锁的特点:
🐇 具体来说,读写锁的行为如下:
在书写具体代码之前,我们先来了解一下其相关函数
① 初始化锁
原型:
int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
参数:
rwlock: 指向 pthread_rwlock_t 类型的读写锁对象的指针。
attr: 一个指向 pthread_rwlockattr_t 类型的指针,可以设置锁的属性。
如果不需要特定的属性,通常可以将其设置为 NULL。
返回值:
返回 0 表示成功,返回错误码(如 EINVAL)表示初始化失败。
范例:
#include <pthread.h>
#include <stdio.h>
pthread_rwlock_t rwlock;
int main() {
// 初始化读写锁
if (pthread_rwlock_init(&rwlock, NULL) != 0) {
perror("pthread_rwlock_init");
return 1;
}
// 后续可以使用 rwlock 执行读写操作
// 销毁读写锁
pthread_rwlock_destroy(&rwlock);
return 0;
}
🎈 pthread_rwlock_init 用于初始化一个读写锁。该函数会创建一个 pthread_rwlock_t 类型的读写锁变量,使其处于初始化状态,供后续的线程操作使用
② 获取锁
a. 读锁
原型:
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
rwlock: 指向要获取读锁的 pthread_rwlock_t 类型的读写锁对象。
返回值:
返回 0 表示成功,返回错误码表示失败(例如 EBUSY 表示写锁被持有,当前线程无法获得读锁)
范例:
#include <pthread.h>
#include <stdio.h>
pthread_rwlock_t rwlock;
void* read_data(void* arg) {
pthread_rwlock_rdlock(&rwlock); // 获取读锁
printf("Reading data...\n");
pthread_rwlock_unlock(&rwlock); // 释放读锁
return NULL;
}
int main() {
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
pthread_t threads[3];
for (int i = 0; i < 3; i++) {
pthread_create(&threads[i], NULL, read_data, NULL);
}
for (int i = 0; i < 3; i++) {
pthread_join(threads[i], NULL);
}
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
// 在上述代码中,多个线程可以同时获得读锁并执行读取操作,而无需相互阻塞。
🎈 pthread_rwlock_rdlock 用于获取读锁,即共享锁。多个线程可以同时持有读锁进行读取操作,但在任何时刻,写锁无法被获得,直到所有的读锁都被释放。
b. 写锁
原型:
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
rwlock: 指向要获取写锁的 pthread_rwlock_t 类型的读写锁对象。
返回值:
返回 0 表示成功,返回错误码表示失败如: EBUSY 表示有其他线程持有读锁或写锁,当前线程无法获得写锁
范例:
#include <pthread.h>
#include <stdio.h>
pthread_rwlock_t rwlock;
void* write_data(void* arg) {
pthread_rwlock_wrlock(&rwlock); // 获取写锁
printf("Writing data...\n");
pthread_rwlock_unlock(&rwlock); // 释放写锁
return NULL;
}
int main() {
// 初始化读写锁
pthread_rwlock_init(&rwlock, NULL);
pthread_t threads[2];
for (int i = 0; i < 2; i++) {
pthread_create(&threads[i], NULL, write_data, NULL);
}
for (int i = 0; i < 2; i++) {
pthread_join(threads[i], NULL);
}
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
// 在上面的示例中,write_data 函数每次都获取写锁,并在完成写操作后释放写锁。
// 在写操作期间,不允许其他线程获取读锁或写锁
🎈 pthread_rwlock_wrlock 用于获取写锁。写锁是独占的,即任何一个线程持有写锁时,其他线程不能获得读锁或写锁。只有当所有线程都释放了读锁,写锁才能被获取。
③ 释放锁
原型:
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数:
rwlock: 指向需要释放锁的 pthread_rwlock_t 类型的读写锁对象。
返回值:
返回 0 表示成功,返回错误码表示失败(例如 EINVAL 表示锁没有被当前线程持有)
🎈 每当一个线程完成对共享资源的读或写操作时,它需要释放相应的锁。pthread_rwlock_unlock 用于释放由 pthread_rwlock_rdlock 或 pthread_rwlock_wrlock 获得的锁。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
#include <cstdlib>
#include <ctime>
// 共享资源
int shared_data = 0;
// 读者锁
pthread_rwlock_t rwlock;
// 读者线程函数
void *Reader(void *arg)
{
// slee(1); // 读者优先, 一旦读者进入 && 读者很多,写者就进不去了
int number = *(int*)arg;
while(true)
{
pthread_rwlock_rdlock(&rwlock); // 读者加锁
std::cout << "读者- " << number << " 正在读取数据, 数据是: " << shared_data << std::endl;
sleep(1); // 模拟读取操作
pthread_rwlock_unlock(&rwlock); // 解锁
}
delete (int*)arg;
return NULL;
}
// 写者线程函数
void *Writer(void *arg)
{
int number = *(int*)arg;
while(true)
{
pthread_rwlock_wrlock(&rwlock); // 写者加锁
shared_data = rand() % 100; // 修改共享数据
std::cout << "写者- " << number << " 正在写入, 新的数据是: " << shared_data << std::endl;
sleep(2); // 模拟写入操作
pthread_rwlock_unlock(&rwlock); // 解锁
}
delete (int*)arg;
return NULL;
}
int main()
{
srand(time(nullptr)^getpid());
pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁
// 可以提高读写数量配比,观察现象
const int reader_num = 2;
const int writer_num = 2;
const int total = reader_num + writer_num;
pthread_t threads[total]; // 假设读者和写者数量相等
// 创建读者线程
for(int i = 0; i < reader_num; ++i)
{
int *id = new int(i);
pthread_create(&threads[i], NULL, Reader, id);
}
// 创建写者线程
for(int i = reader_num; i < total; ++i)
{
int *id = new int(i - reader_num);
pthread_create(&threads[i], NULL, Writer, id);
}
// 等待所有线程完成
for(int i = 0; i < total; ++i)
{
pthread_join(threads[i], NULL);
}
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
输出结果如下:
分析:
那么我们做出一些修改,比如让读锁在加锁前就休眠 1 s
则出现大部分情况都是写者正在写入,读者很难读取到
同样地,我们也可以修改读者、写者数量来改变输出情况,大家也可以自己试试,我就不多尝试了
虽然读写锁提高了并发性能,但它也有一些潜在的缺点:
🦋 读者优先(Reader-Preference)
🦋 写者优先(Writer-Preference)
🔥 自旋锁(Spinlock)是一种简单的同步机制,用于在多线程或多核系统中防止并发访问共享资源。在获取锁时,如果锁被其他线程占用,线程并不会进入休眠状态,而是不断地重复检查锁是否可用,这个过程就被称为“自旋”
🐳 自旋锁通常使用一个共享的标志位(如一个布尔值)来表示锁的状态。当标志位为true 时,表示锁已被某个线程占用;当标志位为false 时,表示锁可用。当一个线程尝试获取自旋锁时,它会不断检查标志位: 如果标志位为 false,表示锁可用,线程将设置标志位为true,表示自己占用了锁,并进入临界区。 如果标志位为 true(即锁已被其他线程占用),线程会在一个循环中不断自旋等待,直到锁被释放。
自旋锁的实现通常使用原子操作来保证操作的原子性,常用的软件实现方式是通过 CAS(Compare-And-Swap)指令实现。以下是一个简单的自旋锁实现示例(伪代码):
#include <stdio.h>
#include <stdatomic.h>
#include <pthread.h>
#include <unistd.h>
// 使用原子标志来模拟自旋锁
atomic_flag spinlock = ATOMIC_FLAG_INIT; // ATOMIC_FLAG_INIT 是 0
// 尝试获取锁
void spinlock_lock() {
while (atomic_flag_test_and_set(&spinlock)) {
// 如果锁被占用,则忙等待
}
}
// 释放锁
void spinlock_unlock() {
atomic_flag_clear(&spinlock);
}
typedef _Atomic struct
{
#if __GCC_ATOMIC_TEST_AND_SET_TRUEVAL == 1
_Bool __val;
#else
unsigned char __val;
#endif
}atomic_flag;
功能描述
原子性
Linux 提供的自旋锁系统调用
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);
int pthread_spin_unlock(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
int pthread_spin_destroy(pthread_spinlock_t *lock);
注意事项
结论
案例: 抢票卖票
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
int ticket = 1000;
pthread_spinlock_t lock;
void *routine(void *arg)
{
char *id = (char*)arg;
while(1){
if (ticket > 0){
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
}
else{
break;
}
}
return nullptr;
}
int main(){
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, routine, (void *) "thread-1");
pthread_create(&t2, NULL, routine, (void *) "thread-2");
pthread_create(&t3, NULL, routine, (void *) "thread-3");
pthread_create(&t4, NULL, routine, (void *) "thread-4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
还记得我们之前写的买票问题嘛,这个最后由于竞争问题,导致最后会出现负值,如下:
加上自旋锁,修改如下:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <string>
int ticket = 1000;
pthread_spinlock_t lock;
void *routine(void *arg)
{
char *id = (char*)arg;
while(1)
{
pthread_spin_lock(&lock);
if (ticket > 0)
{
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_spin_unlock(&lock);
}
else{
pthread_spin_unlock(&lock);
break;
}
}
return nullptr;
}
int main()
{
pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, routine, (void *) "thread-1");
pthread_create(&t2, NULL, routine, (void *) "thread-2");
pthread_create(&t3, NULL, routine, (void *) "thread-3");
pthread_create(&t4, NULL, routine, (void *) "thread-4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
相比于之前的互斥锁,这里我们把 加锁放到了 while 循环内部,因为需要一直自检锁,此时结果如下:
🔥 与传统的互斥锁(Mutex)不同,互斥锁通常会让线程在无法获得锁时进入休眠状态,减少 CPU 的浪费,而自旋锁则在锁被占用时不断轮询,直到获取到锁。这种方式通常用于锁持有时间较短的场景,因为自旋锁避免了线程切换的开销,但是不合理的使用,可能也会浪费 CPU 资源
【*★,°*:.☆( ̄▽ ̄)/$:*.°★* 】那么本篇到此就结束啦,如果有不懂 和 发现问题的小伙伴可以在评论区说出来哦,同时我还会继续更新关于【Linux】的内容,请持续关注我 !!