ToB企服应用市场:ToB评测及商务社交产业平台

标题: 初识Linux · 系统编程done [打印本页]

作者: 拉不拉稀肚拉稀    时间: 2025-1-7 13:04
标题: 初识Linux · 系统编程done
目录
前言:
死锁
可重入函数
读写锁
自旋锁

前言:

本文作为Linux系统编程的收尾工作,先容的是些零碎的概念,比如死锁,可重入函数,自旋锁,读写锁等,其中死锁概念要重要些,对于自旋锁,读写锁来说都没有那么重要,所以咱们相识一下即可。
那么废话不多说,我们直接进入第一个主题,死锁。

死锁

死锁的概念为:
   死锁是指两个或两个以上的线程互相申请对方的资源,同时不开释自己的资源导致的一种互相称待的情况。
  我们可以举个简单的例子,A持有1块钱,B持有一块钱,A和B想要买商品C,代价为2块钱,此时A申请B的一块钱,B申请A的一块钱,但是它们都不想开释自己的一块钱,此时A等B给一块钱,B等A给它一块钱,结果就互相称待。
那么从上面的例子,我们不妨总结一下死锁构成的原因:

互斥条件:至少有一个资源只能被某种实行流持有
哀求和保持:一个实行流在申请资源的时间,仍然保持自己的资源
不可剥夺条件:已经分配给一个实行流的资源不能被强行剥夺,只能由实行流自己开释
环路等待条件:存在一个回路,每个实行流都在等待其他实行流持有的资源。
以上是死锁形成的四个条件,那么我们想要破坏死锁,肯定就要从这四个条件里面破坏,当然了,也有算法是用来检测死锁的,比方银行家算法,资源分配算法,我们这里不做先容。
对于第一个条件:
   互斥条件指的是资源每次只能被一个进程(或线程)利用。要破坏这个条件,可以将独占资源改造成共享资源,允许多个进程同时利用。比方,操作系统可以采用SPOOLing(Simultaneous Peripheral Operations On-Line,即同时外围操作联机)技术,将独占设备在逻辑上改造成共享设备。但需要注意的是,并不是全部的资源都可以改造成可共享利用的资源,并且为了系统安全,很多地方还必须保护这种互斥性。因此,很多时间都无法破坏互斥条件。
  对于第二个条件:
     对于第三个条件:
   当某个进程需要的资源被其他进程所占用的时间,可以由操作系统帮忙,将想要的资源强行剥夺。这种方式一般需要考虑各进程的优先级,并且实现起来比力复杂。此外,开释已获得的资源也可能造成前一阶段工作的失效,因此这种方法一般只适用于易保存和恢复状态的资源,如CPU
  对于第四个条件:
   给系统中的全部资源范例举行排序编号,每个进程只能按递增顺序申请资源。即进程申请了序号为n的资源后,下次只能申请序号为n+1或以上资源。假如进程后面又想申请序号低的资源,那就必须把如今拥有的序号为该资源及其以上的资源全部开释
  对于死锁的解决方法有很多种,我们应该根据实际情况具体分析具体判定。

可重入函数

这里就相识一下可重入函数和线程安全的接洽和区别即可:
可重入函数和线程安全的接洽:
   函数是可重入的,那就是线程安全的
  函数是不可重入的,那就不能由多个线程利用,有可能引发线程安全问题
  假如一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的
  假如函数是可重入的,说明没有临界资源,那么就不会出现多个实行流访问一个数据的情况,反之同理,对于一个函数假如有全局变量,那么多个实行流都能访问,实际上就是第二个情况。
可重入函数和线程安全的区别:
   可重入函数是线程安全函数的一种
  线程安全不愿定是可重入的,而可重入函数则肯定是线程安全的
  假如将对临界资源的访问加上锁,则这个函数是线程安全的,但假如这个重入函数若锁还未开释则会产生死锁,因此是不可重入的。
  可重入函数通常是线程安全的,因为它们被设计为在多线程情况中安全地实行。但是,并不是全部线程安全的代码或函数都是可重入的。比方,一个利用全局变量但通过互斥锁保护的函数可能是线程安全的,但假如它在持有锁的同时调用了另一个可能也持有相同锁的不可重入函数,那么它就不是可重入的。
对于线程安全来说,STL中的几乎全部函数都不是可重入函数。

读写锁

对于读写锁来说,同样存在321原则,即一个交易场所,两个脚色,三个关系,其中我们从名字来看,脚色分别是读者和写者,那么对于交易场所我们不妨看作是黑板报,写者写黑板报,读者读黑板报,那么对于关系呢?
假如关系和生产消费模型一样的,那么读写锁应该就没有存在的意义了吧?
读写锁 VS 生产消费模型
它们的一个本质区别就是,消费者是真真实实的要拿数据的,读者对于数据只是阅读,并不会做出任何处理,仅仅是读取。
三种关系
对于读者和读者来说,它们有关系吗?你读你的,我读我的,我们毫无关系。
对于写者和写者来说,它们的关系是一目了然的,我写的时间你不能写,你写的时间我不能写,这是一种典型的互斥关系。
对于读写和写者来说,我写的时间你不能读,万一你读取到的信息是不完整的,上法庭告我怎么办?因为当时本来就还没有写完,这是一种互斥关系。可是当我写完了,叫读者一声,读者就来读取了,这是一种典型的同步关系
对于读者和写者来说,利用的锁也不是典型的互斥锁,因为读者之间是不需要加锁的,它们利用的锁是pthread_rwlock_init,pthread_rwlock_init,pthread_rwlock_rdlock,pthread_rwlock_wrlock,对于它们来说摧毁的函数都是pthread_rwlock_unlock。
函数的原型分别为:
  1. int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr);
  2. int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  3. int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
  4. int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
  5. int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
复制代码
那么这里给一份示例代码:
  1. // 共享资源
  2. int shared_data = 0;
  3. // 读写锁
  4. pthread_rwlock_t rwlock;
  5. // 读者线程函数
  6. void *Reader(void *arg)
  7. {
  8.     //sleep(1); //读者优先,一旦读者进入&&读者很多,写者基本就很难进入了
  9.     int number = *(int *)arg;
  10.     while (true)
  11.     {
  12.         pthread_rwlock_rdlock(&rwlock); // 读者加锁
  13.         std::cout << "读者-" << number << " 正在读取数据, 数据是: " << shared_data << std::endl;
  14.         sleep(1);                       // 模拟读取操作
  15.         pthread_rwlock_unlock(&rwlock); // 解锁
  16.     }
  17.     delete (int*)arg;
  18. }
  19. // 写者线程函数
  20. void *Writer(void *arg)
  21. {
  22.     int number = *(int *)arg;
  23.     while (true)
  24.     {
  25.         pthread_rwlock_wrlock(&rwlock); // 写者加锁
  26.         shared_data = rand() % 100;     // 修改共享数据
  27.         std::cout << "写者- " << number << " 正在写入. 新的数据是: " << shared_data << std::endl;
  28.         sleep(2);                       // 模拟写入操作
  29.         pthread_rwlock_unlock(&rwlock); // 解锁
  30.     }
  31.     delete (int*)arg;
  32. }
  33. int main()
  34. {
  35.     srand(time(nullptr)^getpid());
  36.     pthread_rwlock_init(&rwlock, NULL); // 初始化读写锁
  37.     // 可以更高读写数量配比,观察现象
  38.     const int reader_num = 2;
  39.     const int writer_num = 2;
  40.     const int total = reader_num + writer_num;
  41.     pthread_t threads[total]; // 假设读者和写者数量相等
  42.     // 创建读者线程
  43.     for (int i = 0; i < reader_num; ++i)
  44.     {
  45.         int *id = new int(i);
  46.         pthread_create(&threads[i], NULL, Reader, id);
  47.     }
  48.     // 创建写者线程
  49.     for (int i = reader_num; i < total; ++i)
  50.     {
  51.         int *id = new int(i - reader_num);
  52.         pthread_create(&threads[i], NULL, Writer, id);
  53.     }
  54.     // 等待所有线程完成
  55.     for (int i = 0; i < total; ++i)
  56.     {
  57.         pthread_join(threads[i], NULL);
  58.     }
  59.     pthread_rwlock_destroy(&rwlock); // 销毁读写锁
  60.     return 0;
  61. }
复制代码
读写模型分为读者优先和写者优先,一般默认的都是读者优先。

自旋锁


对于自旋锁来说,它的原理和互斥锁几乎一样:
   自旋锁通常利用一个共享的标志位(如一个布尔值)来表示锁的状态。当标志位为 true 时,表示锁已被某个线程占用;当标志位为false时,表示锁可用。当一个线程尝 试获取自旋锁时,它会不断查抄标志位:
  • 假如标志位为false,表示锁可用,线程将设置标志位为true,表示自己占用了 锁,并进入临界区。
  • 假如标志位为true(即锁已被其他线程占用),线程会在一个循环中不断自旋等 待,直到锁被开释。
  对于资源来说,分为临界资源和非临界资源,我们寻常几乎没有关心临界资源的实行时间问题,我们假设这么一个场景,实行流AB,A持有了锁,B自然应该挂起等待,那么B怎么挂起的你别管,反正数据是被cpu寄存器存储了,那么假如B挂起的时间只有1ms,A实行的时间有1秒,那么这个挂起无所谓,没有大消耗。
可是假如实行时间只有1ms,挂起的时间有1秒呢?多个实行流被挂起,此时cpu的切片的时间多了,实行的时间那么短,其他实行流就浪费了许多时间,假如实行流能够不挂起,一直查询呢?
此时,自旋锁就出来了,也就是实行流选择不挂起,一直轮询查看锁的状态,假如锁的状态是被占用,那么实行流就一直循环查看,直到锁被开释。
这种锁就叫做自旋锁。
但是实际上,自旋锁应用的场景十分少,固然它减去了系统调度开销,镌汰了时间成本,但是它的缺点十分明显,假如持有锁的线程出问题了,那么其他全部的实行流都轮询锁,此时cpu资源肯定是被浪费了,而全部线程都在检测锁的状态无法进入临界区,此时引发的问题是活锁:
   活锁的定义为:
  活锁指的是任务或实行者没有被阻塞,但由于某些条件没有满意,导致它们一直重复实验、失败、再实验、再失败的状态。处于活锁的实体是在不断地改变状态,即所谓的“活”。
  而因为原理部分自旋锁和互斥锁几乎一样,接口其实也差不了多少,所以直接给函数原型了:
  1. int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
  2. int pthread_spin_destroy(pthread_spinlock_t *lock);
  3. int pthread_spin_lock(pthread_spinlock_t *lock);
  4. int pthread_spin_unlock(pthread_spinlock_t *lock);
复制代码
给一段测试代码,对于自旋锁我们是人为感知不到它正在循环查询的,加上自旋锁应用的场景有临界区实行时间非常非常短,以及一下极其特殊的场景,所以就不外多先容了:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <string.h>
  4. #include <unistd.h>
  5. #include <pthread.h>
  6. int ticket = 1000;
  7. //pthread_spinlock_t lock;
  8. void *route(void *arg)
  9. {
  10. char *id = (char *)arg;
  11. while (1)
  12. {
  13. //pthread_spin_lock(&lock);
  14. if (ticket > 0)
  15. {
  16. usleep(1000);
  17. printf("%s sells ticket:%d\n", id, ticket);
  18. ticket--;
  19. //pthread_spin_unlock(&lock);
  20. }
  21. else
  22. {
  23. //pthread_spin_unlock(&lock);
  24. break;
  25. }
  26. }
  27. return nullptr;
  28. }
  29. int main(void)
  30. {
  31. //pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);
  32. pthread_t t1, t2, t3, t4;
  33. pthread_create(&t1, NULL, route, (void *)"thread 1");
  34. pthread_create(&t2, NULL, route, (void *)"thread 2");
  35. pthread_create(&t3, NULL, route, (void *)"thread 3");
  36. pthread_create(&t4, NULL, route, (void *)"thread 4");
  37. pthread_join(t1, NULL);
  38. pthread_join(t2, NULL);
  39. pthread_join(t3, NULL);
  40. pthread_join(t4, NULL);
  41. //pthread_spin_destroy(&lock);
  42. }
复制代码
以上,或者说初识Linux系统编程的文章就更新完了,大部分文章先容的不是那么清楚的,博主会重制Linux系统编程的,算是对博主自己的一个交接啦,敬请期待!! 

感谢阅读!

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




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4