Linux 读写者问题和读写者锁

[复制链接]
发表于 2024-10-20 21:32:32 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
媒介

在计算机中,生产消费者问题是经典的并发控制问题之一,他描述的是多个生产实行流和多个消费实行流之间协调(同步与互斥)式的访问临界资源!与此类似,读写者问题也是一个紧张的并发控制问题!本期我们将来先容一下读写者问题和读写锁~!
目录
媒介
一、读写者问题和读写者锁
1.1 什么是读写者问题
1.2 读写者的特点
1.3 为什么读者之间是并发关系?
1.4、理解读写者问题
1.5、读写锁先关的接口
• 初始化和销毁
• 申请读写锁
• 测试小Demo
1.6、读写锁的应用场景
1.7、读写者锁的优缺点
二、自旋锁
2.1 什么是自旋锁
2.2 自旋锁的原理
2.3 自旋锁的优缺点
2.4 自旋锁的使用场景
2.5 自旋锁的接口
• 加锁和解锁
• 初始化和销毁
2.6 测试小Demo


一、读写者问题和读写者锁

1.1 什么是读写者问题

读写者问题 是一个紧张的并发控制问题,涉及多个读写的实行流,这些实行流需要并发式的访问共享的资源(文件、数据库、或某种数据布局)为了包管数据一致性和完整性,我们需要设计一种同步机制来协调这些实行流对共享资源的访问!
读写者问题中存在两种角色的实行流:
   1、读者(Reader):只读取临界资源,不会拿走/修改临界资源
  2、写者(Writer):修改共享资源
  这就和我们平常的生活中的,栗子很相似。例如:小学/初中的时候,隔一段时间就需要画黑板,黑板报就是那个临界资源,负责画黑板报的那一批童鞋就是 写者;画完之后观看黑板报的那一群吃瓜群众就是 读者

1.2 读写者的特点

读写者问题的特点和生产消费者的特点根本一致,都可以用 321 原则总结:
   1 体现:一个生意业务场所
  2 体现:两种角色
  3 体现:三种关系
  其中三种关系是:
  读者和读者:并发关系(没关系)
  读者和写者:互斥 && 同步关系
  写者和写者:互斥关系
  这三种关系中后两个很好理解:
读者读的时候写者不能修改,写者写的时候读者也不允许读,即读者和写者是互斥和同步!同一个黑板报,我张三画画的时候你李四就等着我画完了你在写,即写者之间是互斥的!
1.3 为什么读者之间是并发关系?

   我们上面先容的时候也说了,读者只是负责读取并不会拿走/修改 共享资源,所以多个读者可以同时读取共享资源而不会发生冲突!
  1.4、理解读写者问题

我们为了理解读写者问题,下面现将用一段伪代码先容,然后完了之后再用一个栗子验证~!
  1. // C++ -> Public
  2. uint32_t reader_count = 0;
  3. lock_t count_lock;
  4. lock_t writer_lock;
  5. // C++ -> Reader
  6. // 加锁
  7. lock(count_lock);
  8. if(reader_count == 0)
  9.         lock(writer_lock);
  10. ++reader_count;
  11. unlock(count_lock);
  12. // read;
  13. //解锁
  14. lock(count_lock);
  15. --reader_count;
  16. if(reader_count == 0)
  17.         unlock(writer_lock);
  18. unlock(count_lock);
  19. // C++ -> Writer
  20. lock(writer_lock);
  21. // write
  22. unlock(writer_lock);
复制代码
为了包管读写者之间的互斥,读者第一次读的时候会先把写者的锁给占有了,如许即使读者读的时候,有写者来也不会访问到临界资源,而是写者在他的锁位置期待~!
同理,写者持有写者锁时,只能写者中的一个实行流写入操作,读者在第一次进入申请写者锁时,发现写者再用,于是直接在写者锁的那个位置期待!

 读者读取

 写者写入

这里有前面互斥同步以及生产消费者的理解应该是很容易理解的!
1.5、读写锁先关的接口

Linux操作体系提供了读写锁(Read-Write Lock)来实现读写者问题的同步控制。在pthread库提供的相关的接口实现。
• 初始化和销毁


  1. #include <pthread.h>
  2. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  3. int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
  4.            const pthread_rwlockattr_t *restrict attr);
  5. pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
复制代码
  留意:这里的初始化读写锁的第二个参数体现读写锁的属性,我们不用关心直接设置为nullptr 即可;别的这些函数的返回值都是一样的乐成,返回0,失败返回错误码,后续不在先容!
  • 申请读写锁


  1. #include <pthread.h>
  2. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  3. int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
复制代码
  这里读者加锁,还是两个版本,try版本是没有锁直接返回,try可以使用克制死锁!
  

  1. #include <pthread.h>
  2. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
复制代码
OK,下面我们来实现一个简单的读写锁的Demo代码:
• 测试小Demo

   我们创建一批线程,让几个去实行读者,几个去实行写者;
  界说一个全局的整型变量,然后写者写就是想这个变量中写入,读者读取就是读取这个变量中的值!
  1. #include <iostream>
  2. #include <pthread.h>
  3. #include <unistd.h>
  4. #include <ctime>
  5. int share_count = 0;// 读写者共享
  6. pthread_rwlock_t g_rwlock = PTHREAD_RWLOCK_INITIALIZER;// 定义一把全局的读写锁
  7. void* Writer(void* args)
  8. {
  9.     auto num = *(int*)args;
  10.     while (true)
  11.     {
  12.         // 加锁
  13.         pthread_rwlock_rdlock(&g_rwlock);
  14.         // 构造一个数据
  15.         share_count = rand() % 100 + 1;
  16.         std::cout << "写者_" << num << "写入了:" << share_count << std::endl;
  17.         sleep(2);
  18.         // 解锁
  19.         pthread_rwlock_unlock(&g_rwlock);
  20.     }
  21.     delete (int*)args;
  22.     return nullptr;
  23. }
  24. void* Reader(void* args)
  25. {
  26.     auto num = *(int*)args;
  27.     while (true)
  28.     {
  29.         // 加锁
  30.         pthread_rwlock_rdlock(&g_rwlock);
  31.         // 读取
  32.         std::cout << "读者_" << num << "读取了:" << share_count << std::endl;
  33.         sleep(2);
  34.         // 解锁
  35.         pthread_rwlock_unlock(&g_rwlock);
  36.     }
  37.     delete (int*)args;
  38.     return nullptr;
  39. }
  40. int main()
  41. {
  42.     srand(time(nullptr) ^ getpid());
  43.     // 初始化读写锁
  44.     pthread_rwlock_init(&g_rwlock, nullptr);
  45.     const int read = 2; // 读线程个数
  46.     const int write = 2;// 写线程个数
  47.     const int total = read + write; // 总个数
  48.     pthread_t tids[total]; // 管理线程的数组
  49.     // 创还能线程
  50.     for(int i = 0; i < read; i++)
  51.     {
  52.         int* num = new int(i);
  53.         pthread_create(tids+i, nullptr, Writer, num);
  54.     }
  55.     for(int i = read; i < total; i++)
  56.     {
  57.         int* num = new int(i);
  58.         pthread_create(tids+i, nullptr, Reader, num);
  59.     }
  60.     // 等待线程
  61.     for(int i = 0; i < total; i++)
  62.     {
  63.         pthread_join(tids[i], nullptr);
  64.     }
  65.     // 销销毁读写锁
  66.     pthread_rwlock_destroy(&g_rwlock);
  67.     return 0;
  68. }
复制代码

我们这里由于设置的时间问题,可以看到交替时的现象,有时候大概会出现一直是读者读/一直写者写的情况!这是正常的,由于读写锁有他的优先机制!
      读者优先(   Reader-Preference          在这种策略中,体系会尽大概多地允许多个读者同时访问资源(比如共享文件或数        据),而不会优先思量写者。这意味着当有读者正在读取时,新到达的读者会立即被        允许进入读取区,而写者则会被阻塞,直到全部读者都离开读取区。读者优先策略可        能会导致写者饥饿(即写者长时间无法获得写入权限),特殊是当读者频繁到达时。        写者优先(   Writer-Preference          在这种策略中,体系会优先思量写者。当写者请求写入权限时,体系会尽快地让写者        进入写入区,即使此时有读者正在读取。这通常意味着一旦有写者到达,全部后续的        读者都会被阻塞,直到写者完成写入并离开写入区。写者优先策略可以减少写者期待        的时间,但大概会导致读者饥饿(即读者长时间无法获得读取权限),特殊是当写者        频繁到达时    默认是读锁优先,固然可以设置这些策略!
  1. int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
  2. /*
  3. pref 共有 3 种选择
  4. PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
  5. PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和
  6. PTHREAD_RWLOCK_PREFER_READER_NP 一致
  7. PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
  8. */
复制代码
1.6、读写锁的应用场景

读写锁在以下的场景中是很有用的:
1、数据库体系:在数据库体系中,读取操作通常远多于写入操作。使用读写锁可以提高并发性,允许多个读者同时读取数据,而写者在写入时独占资源。
2、缓存体系:缓存体系中的数据读取非常频繁,而写入(缓存失效或更新)相对较少。读写锁可以确保在读取时不会阻塞其他读者,同时包管写者在更新时能够独占资源。
3、配置文件:多个进程或线程大概需要读取配置文件,但修改配置文件的操作相对较少。使用读写锁可以确保配置文件在更新时不会影响大量读取操作。
1.7、读写者锁的优缺点

读写锁机制具有以下长处:
1、提高并发性:允许多个读者同时读取共享资源,提高了体系的并发性能
2、简化同步控制:读写锁提供了轻便的API函数,使得同步控制更加容易实现。
然而,读写锁也存在一些潜在的缺点:
1、写者饥饿:在长时间运行的体系中,如果写操作频繁被读操作打断,大概会导致写者饥饿问题。这可以通过调整读写锁的策略(如写者优先)来解决。
2、死锁:虽然读写锁本身设计为防止死锁(由于它不允许多个线程同时持有写入锁),但在复杂的体系中,如果读写锁与其他同步机制(如互斥锁、条件变量等)结合使用时,仍旧大概出现死锁。因此,需要通过仔细设计锁的顺序和克制嵌套锁等策略来预防。

二、自旋锁

2.1 什么是自旋锁

自旋锁 是一种多线程同步机制,用于并发时对共享资源的保护。在多个线程同时获取锁时,他们连续自旋(在一个循环中不停地查抄所是否可用而不是没有锁立即阻塞休眠期待。这种机制减少了线程的开销,适用于短时间内锁的竞争情况!但如果不公道使用,大概会造成CPU的浪费!
这里也不难理解,我们知道:多线程并发访问一个共享资源时,为了克制线程安全,就要对共享资源保护,被保护的这部门共享资源叫临界资源,访问临界资源的代码叫临界区,而全部访问临界资源的操作都是用代码访问的,所以对临界资源的保护本质就是对临界区的保护!

以前是加互斥锁/读写锁等包管,无论是互斥锁还是读写锁,他们都是当申请不乐成时就去相应阻塞队列期待,即将对应的线程挂起期待,等到有锁了在唤醒!但是挂起和唤醒是需要时间的而且挂起务必伴随着线程切换!

如果是实行实行临界区代码的是将轻微长一点那还好,但如果实行临界区代码的时间较短就伴随着频繁的阻塞与唤醒,这也会导致性能受影响!所以这种情况下自旋锁就产生了重大的意义!
2.2 自旋锁的原理

自旋锁 通常使用一个共享的标志位(入一个布尔值)来体现锁的状态。当标志位为 true 时,表锁已经被某个线程占用;当标志位是 false 时,体现锁 可以用。当一个线程尝试获取自旋锁时,它的内部会不停地查抄标志位:
   标志位为 true :体现锁以被占用,线程会在一个循环中不停地自旋做检测,直到所释放!
  标志位为 false : 体现锁可用,当线程申请到时会将标记为设为 true,体现当前已占用,并进入临界区!
  我们下面用一段C++的伪代码来模拟实现一下自旋锁,紧张是理解他的原理:
自旋锁的实现通常是使用原子操作来包管的,软件实现通常是CAS(Compaer-And-Swap)指令实现,我们这里使用C++11的原子性操作来简单先容:
  1. C++
  2. #include <stdio.h>
  3. #include <stdatomic.h>
  4. #include <pthread.h>
  5. #include <unistd.h>
  6. // 使用原子标志来模拟自旋锁
  7. atomic_flag spinlock = ATOMIC_FLAG_INIT; // ATOMIC_FLAG_INIT 是 0
  8. // 尝试获取锁
  9. void spinlock_lock()
  10. {
  11.         while (atomic_flag_test_and_set(&spinlock))
  12.         {
  13.                 // 如果锁被占用,则忙等待
  14.         }
  15. }
  16. // 释放锁
  17. void spinlock_unlock()
  18. {
  19.         atomic_flag_clear(&spinlock);
  20. }
复制代码
其中 atomic_flag 是C++11提供的原子范例,它的布局如下:
  1. typedef _Atomic struct
  2. {
  3.         #if __GCC_ATOMIC_TEST_AND_SET_TRUEVAL == 1
  4.         _Bool __val;
  5.         #else
  6.         unsigned char __val;
  7.         #endif
  8. } atomic_flag;
复制代码
atomic_flag_test_and_set 这个函数的作用有两个,1是检测自旋锁 2是设置互斥锁的标记位!
当检测完自旋锁没有被使用就是用,atomic_flag_test_and_set 会将标记为设计为 true 并返回原来的标记位即 false 所以死循环就竣事,即自旋锁加锁乐成!
当检测完是已被使用时,atomic_flag_test_and_set 会返回 true ,即一直循环的检测!
2.3 自旋锁的优缺点

长处
   1、低延迟 : 自旋锁适用于短时间内的竞争情况,由于他不会让线程进入休眠状态,从而克制了线程切换到开销提高了所操作的服从!
  2、减少体系的调度开销:期待线程不需要阻塞,不需要上下文的切换、从而减少了体系调度的开销!
  缺点
   1、CPU 资源浪费:如果持有自旋锁的线程长时间不释放,期待获取的线程会一直获取,导致CPU资源的浪费!
  2、大概引起活锁:当多个线程同时自旋期待同一把锁时,如果由于某些缘故原由,这把锁无法释放!这就会导致全部获取的线程都会在检测那里一直检测,而无法进入临界区,即形成活锁!
  留意
   在使用自旋锁时,需要确保锁被释放的时间尽大概的短,以克制CPU资源的浪费!
  在多CPU情况下,自旋锁看不如其他锁高效,由于他大概让线程在不同的CPU上自旋期待!
  2.4 自旋锁的使用场景

自旋锁在上层的应用中是非常的罕见的,可以说是险些见不到!它的应用场景有:
   1、短暂期待的情况:适用于锁占有时间很短的场景,如多线程对共享数据的简单读写操作
  2、多线程锁的使用:通常用于OS的底层(非常常见),同步多个CPU对共享资源的访问
  2.5 自旋锁的接口

• 加锁和解锁

  1. // 加自旋锁,不成功,自旋检测
  2. int pthread_spin_lock(pthread_spinlock_t *lock);
  3. // 加自旋锁,不成功不会自旋,而是返回错误码
  4. int pthread_spin_trylock(pthread_spinlock_t *lock);
  5. // 解自旋锁
  6. int pthread_spin_unlock(pthread_spinlock_t *lock);
复制代码
• 初始化和销毁

  1. // 定义一把自旋锁
  2. pthread_spinlock_t spin_lock;
  3. // 初始化自旋锁
  4. int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
  5. // 销毁自旋锁
  6. int pthread_spin_destroy(pthread_spinlock_t *lock);
复制代码
留意:自旋锁没有提供和互斥锁那样的全局的初始化宏的,所以得使用init初始化!
2.6 测试小Demo

由于上层对自旋锁的使用是非常的少的,所以我们找一个对共享的数据只是读写的例子,我们以前写的抢票就是一个不错的例子,这里可以使用自旋锁,就以他为例:
  1. // 操作共享变量会有问题的售票系统代码
  2. #include <stdio.h>
  3. #include <stdlib.h>
  4. #include <string.h>
  5. #include <unistd.h>
  6. #include <pthread.h>
  7. int ticket = 1000;
  8. pthread_spinlock_t lock;
  9. void *route(void *arg)
  10. {
  11.     char *id = (char *)arg;
  12.     while (1)
  13.     {
  14.         pthread_spin_lock(&lock);
  15.         if (ticket > 0)
  16.         {
  17.             usleep(1000);
  18.             printf("%s sells ticket:%d\n", id, ticket);
  19.             ticket--;
  20.             pthread_spin_unlock(&lock);
  21.         }
  22.         else
  23.         {
  24.             pthread_spin_unlock(&lock);
  25.             break;
  26.         }
  27.     }
  28.     return nullptr;
  29. }
  30. int main(void)
  31. {
  32.     pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);
  33.     pthread_t t1, t2, t3, t4;
  34.     pthread_create(&t1, NULL, route, (void *)"thread 1");
  35.     pthread_create(&t2, NULL, route, (void *)"thread 2");
  36.     pthread_create(&t3, NULL, route, (void *)"thread 3");
  37.     pthread_create(&t4, NULL, route, (void *)"thread 4");
  38.     pthread_join(t1, NULL);
  39.     pthread_join(t2, NULL);
  40.     pthread_join(t3, NULL);
  41.     pthread_join(t4, NULL);
  42.     pthread_spin_destroy(&lock);
  43.     return 0;
  44. }
复制代码

这里的结果也是符合我们的预期的~!

OK,,本期分享就到这里,好兄弟,我是cp我们下期再见~!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告
回复

使用道具 举报

© 2001-2025 Discuz! Team. Powered by Discuz! X3.5

GMT+8, 2025-7-10 01:44 , Processed in 0.081018 second(s), 28 queries 手机版|qidao123.com技术社区-IT企服评测▪应用市场 ( 浙ICP备20004199 )|网站地图

快速回复 返回顶部 返回列表