Linux自旋锁和读写锁

news2024/9/22 7:33:41

        在前面的文章中我们已经介绍了有关互斥锁的概念与使用,本篇将开始介绍在 Linux 中的自旋锁和读写锁。这三种锁分别用于在不同的应用场景之中,其中互斥锁最为常用,但是我们需要了解一下其他的锁。

        对于自旋锁和读写锁都介绍了其原理以及接口使用,并且给出了样例代码。

目录

自旋锁

1. 自选锁概念和原理

2. 自旋锁的优缺点与接口使用

3. 自旋锁使用样例代码

读写锁

1. 读者写者模型 vs 生产消费模型

2. 读写锁伪代码和读写锁函数接口

3. 读者优先和写者有限

4. 读写锁使用样例代码

自旋锁

        在使用互斥锁对我们的临界资源上锁时,当其他线程抢不到锁,则会在系统调用中阻塞起来(操作系统将线程的 tcb 放入到等待队列中),等到抢到锁之后又会将线程唤醒(将线程的 tcb 从等待队列中放入到运行队列中)。但是假若在临界区运行的时间还没有将线程挂起等待的时间长,那么还不如让线程不断的轮询访问我们的锁,不将线程挂起等待,当锁一但释放就可以立即的获取到锁。

        自旋锁的应用中应用层中使用的非常少,但是在操作系统层面使用得很多,因为操作系统不会轻易的将自己挂起等待,所以对于某些资源需要等到访问的时候,通常都是轮询等待。

1. 自选锁概念和原理

        自旋锁概念:一种多线程同步机制,用于保护共享资源免受并发访问的影响。在多个线程尝试获取到锁时,它们会持续旋转(在循环中不断的检查锁是否可用)而不是立即进入休眠状态等待锁的释放。这种机制减少了线程切换的开销(因为假若是阻塞等待则会将线程挂起然后切换,而轮询则是在时间片内一直访问),适用于短时间内锁竞争的情况。但是假若不合理的使用(在临界区的运行时间较长),则会导致 CPU 的浪费。

        原理:自旋锁通常使用一个共享的标志位来表示锁的状态。当标志位为 true 时,表示锁已被某个线程占用;当标志位为 false 的时候,表示锁可用。当一个线程尝试获取自旋锁的时候,会不断的检查标志位。:

        1. 若标志位为 false,表示锁可以使用,线程将标志位设置为 true,表示占用了锁,然后进入到临界区中执行代码;        

        2. 若标志位为 true,表示锁已经被其他先占用,线程会在一个循环中不断自旋等待,直到锁被释放。

        对于自旋锁代码实现的伪代码如下:

typedef _Atomic struct
{
#if __GCC_ATOMIC_TEST_AND_SET_TRUEVAL == 1
_Bool __val;
#else
unsigned char __val;
#endif
} atomic_flag;

// ATOMIC_FLAG_INIT 值为0
atomic_flag spinlock = ATOMIC_FLAG_INIT;

// 尝试获取锁
void spinlock_lock() {
    // 没有获取到锁则一直运行,返回值为true
    // 获取到锁,返回值为false,跳出循环等待
    while (atomic_flag_test_and_set(&spinlock)) {
    // 如果锁被占用,则忙等待
    }
}

// 释放锁
void spinlock_unlock() {
    atomic_flag_clear(&spinlock);
}

atomic_flag_test_and_set 函数检查 atomic_flag 的当前状态。如果
atomic_flag 之前没有被设置过(即其值为 false 或“未设置”状态),则函数会将其
设置为 true(或“设置”状态),并返回先前的值(在这种情况下为 false)。如果
atomic_flag 之前已经被设置过(即其值为 true),则函数不会改变其状态,但会
返回 true。

        对于以上伪代码的实现,特别是对 atomic_flag 的操作一定得是原子的,这样才能保证 atomic_flag 的读取和修改在多线程环境中是不可分割的,同时这样保证了线程安全的问题。

2. 自旋锁的优缺点与接口使用

        优点:

        1. 低延迟:自旋锁适用于短时间内的锁竞争情况,因为他不会让线程进入休眠状态,从而避免了线程切换的开销,提高了锁操作的效率。

        2. 减少系统调度开销:等待锁的线程不会被阻塞,不需要切换上下文,减少了系统调度的开销。

        缺点:

        1. CPU 资源的浪费:若锁持有的时间较长,等到获取线程的线程会一直循环等待,导致 CPU 资源的浪费。

        2. 可能引起活锁:当多个线程同时自旋等待同一个锁的时候,若没有适当的退避政策,可能会导致所有线程都在不断检查锁的状态,从而无法进入临界区,形成活锁。

        所以对于自旋锁,常用于短暂等待的情况和多线程锁的使用(通常用于底层,同步多个 CPU 对共享资源的访问)。

        自旋锁常用接口的使用:

pthread_spinlock_t // 自旋锁类型

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);
// 初始化,第二个参数通常设置为0
int pthread_spin_destroy(pthread_spinlock_t *lock);
// 销毁

3. 自旋锁使用样例代码

        使用自旋锁实现一个抢票逻辑的代码,如下:

#include <iostream>
#include <pthread.h>
#include <unistd.h>

pthread_spinlock_t lock;
int tickets = 1000;

void* Routine(void* args) {
    const char* name = static_cast<const char*>(args);
    
    while (true) {
        pthread_spin_lock(&lock);
        if (tickets > 0) {
            usleep(1000); // 表示抢票时间
            std::cout << name << " get a ticket, the remaining tickets are: " << tickets << std::endl;
            tickets--;
            pthread_spin_unlock(&lock);
        } else {
            pthread_spin_unlock(&lock);
            break;
        }
    }

    return nullptr;
}

int main() {
    pthread_spin_init(&lock, 0);
    pthread_t t1, t2, t3, t4;
    
    pthread_create(&t1, nullptr, Routine, (void*)"thread-1");
    pthread_create(&t2, nullptr, Routine, (void*)"thread-1");
    pthread_create(&t3, nullptr, Routine, (void*)"thread-1");
    pthread_create(&t4, nullptr, Routine, (void*)"thread-1");

    pthread_join(t1, nullptr);
    pthread_join(t2, nullptr);
    pthread_join(t3, nullptr);
    pthread_join(t4, nullptr);

    pthread_spin_destroy(&lock);
    return 0;
}

        测试结果如下:

读写锁

        在编写多线程代码的时候,也会出现公共数据修改的机会较少的情况,也就是对于写入的数据,很少将其修改,既然不怎么修改,那么在读出数据的时候,也就是说我们根本就不需要加锁,不加锁多线程就可以并行运行。

1. 读者写者模型 vs 生产消费模型

        在读者写者模型中和生产消费模型中的,存在什么区别呢?

        其实读者写者模式和生产消费模型,都遵守321原则:三种关系、两个对象、一个场所(对于生产消费模型,在前文中已经介绍过,这里主要介绍读者写者模型)。

        在读者写者模型中:三种关系为:读者和读者、写者和写者、读者和写者;

        两个对象:读者和写者。

        一个场所:可以是一段内存空间、也可以是某种数据结构。

        让而在三种关系中,写者和写者的关系为互斥,一个时刻只能由一个线程写入信息,防止信息被覆盖,写者和读者的关系为互斥和同步关系,写的时候不能读,读的时候不可以写,但是当读完也需要通知写者来写,写完也需要通知读者来读;而对于读者和读者的关系则不同,读者和读者之间可以共享信息,可以一起读信息,互不干扰,所以读者和读者的关系为没有关系

        既然读者和读者之间没有关系,也就是说明在读消息的时候我们不需要加锁。

2. 读写锁伪代码和读写锁函数接口

        一下为一个读写锁的伪代码,如下:

        对于 Reader而言:

// 加锁
lock(count_lock);
// 当第一个读者进来的时候,要将写者锁住
if(reader_count == 0)
    lock(writer_lock);
++reader_count;
unlock(count_lock);

//解锁
lock(count_lock);
--reader_count;
// 当是最后一个读者离开的时候,将写者唤醒,让写者写入信息
if(reader_count == 0)
    unlock(writer_lock);
unlock(count_lock);

        对于 Writer 而言:

lock(writer_lock);
// write
unlock(writer_lock);

        对于读写锁的函数接口如下:

// 读写锁变量类型
pthread_rwlock_t

// 初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t *restrict attr);

// 销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock)
    
// 加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); // 读者加锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); // 写者加锁

int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); // 解锁通用


设置读写优先策略函数:
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
pref的三种取值:
PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥
饿情况
PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和 PTHREAD_RWLOCK_PREFER_READER_NP 一致
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递
归加锁

3. 读者优先和写者有限

        读者优先:

        在这种策略中,系统会尽可能多地允许多个读者同时访问资源,而不会优先考虑写者。这意味着当有读者正在读取时,新达到的读者会立即被运行进入到读取区,而写者会被阻塞,直到所有读者都离开读取区。读者优先策略可能会导致写者饥饿(写者长时间不能获取权限进入),特别是当读者频繁到达时。

        写者优先:

        在这种策略中,系统会优先考虑写者,当写者请求写入权限时,系统会尽可能地让写者进入写入区,即使此时有读者正在读取。意味着一旦有写者到达,所有后续的读者都会被阻塞,直到写者完成写入并离开写入区。写者优先策略可以减少写者等待的时间,但是会导致读者饥饿问题特别是写者频繁到达。

4. 读写锁使用样例代码

        如下,我们使用读写锁给出一个代码样例:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <ctime>
#include <cstdlib>

// 共享资源
int shared_data = 0;

// 读写锁
pthread_rwlock_t rwlock;

void* Reader(void* args) {
    // sleep(2); // 读者优先,一旦读者进入,写者就很难进入了
    const char* name = static_cast<const char*>(args);
    while (true) {
        pthread_rwlock_rdlock(&rwlock);
        std::cout << name << " is reading the shared data, the data is " << shared_data << std::endl;
        sleep(1); // 模拟读花的时间
        pthread_rwlock_unlock(&rwlock);
    }
    return nullptr;
}

void* Writer(void* args) {
    const char* name = static_cast<const char*>(args);
    while (true) {
        pthread_rwlock_wrlock(&rwlock);
        shared_data = rand() % 100;
        std::cout << name << " is writing the shared data, the data is " << shared_data << std::endl; 
        sleep(1); // 模拟写花的时间
        pthread_rwlock_unlock(&rwlock);
    }
    return nullptr;
}

int main() {
    srand(time(nullptr) ^ getpid());
    pthread_rwlock_init(&rwlock, nullptr);
    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++) {
        char* buff = new char[128];
        snprintf(buff, 128, "reader-%d", i + 1);
        pthread_create(&threads[i], nullptr, Reader, (void*)buff);
    }

    // 创建写线程
    for (int i = reader_num; i < total; i++) {
        char* buff = new char[128];
        snprintf(buff, 128, "writer-%d", i + 1);
        pthread_create(&threads[i], nullptr, Writer, (void*)buff);
    }

    for (int i = 0; i < total; i++) {
        pthread_join(threads[i], nullptr);
    }

    pthread_rwlock_destroy(&rwlock);
    return 0;
}

        测试结果如下:

        对于如上代码,特别容易出现读者优先,可以通过调整写者的数量和读者进入的时间来调整为写者优先

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2071970.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【信创】麒麟KylinOS V10打开root登录桌面权限

原文链接&#xff1a;【信创】麒麟KylinOS V10打开root登录桌面权限 Hello&#xff0c;大家好啊&#xff01;今天给大家带来一篇关于在麒麟KYLINOS V10上如何打开root用户登录桌面的文章。在大多数Linux发行版中&#xff0c;出于安全考虑&#xff0c;root用户默认情况下是禁止直…

KRTS网络模块:TCP服务端、客户端实例

KRTS网络模块:TCP服务端、客户端实例 目录 KRTS网络模块:TCP服务端、客户端实例TCP简介KRST服务端简介核心特性界面设计核心代码 KRTS客户端简介核心特性界面设置核心代码 运行实例 Socket模块基于Packet模块&#xff0c;实时提供更高的协议&#xff0c;如RAW-IP、TCP 和 UDP(参…

【求助帖】用PyTorch搭建MLP网络时遇到奇怪的问题

求助&#xff1a;我在测试自己搭建的通用MLP网络时&#xff0c;发现它与等价的参数写死的MLP网络相比效果奇差无比&#xff0c;不知道是哪里出了问题&#xff0c;请大佬们帮忙看下。 我写的通用MLP网络&#xff1a; class MLP(nn.Module):def __init__(self, feature_num, cl…

3、Unity【基础】Resources资源场景动态加载

文章目录 一、Resources资源动态加载1、Unity中特殊文件夹1、工程路径获取2、Resources资源文件夹3、StreamingAssets流动资源文件夹4、persistentDataPath持久数据文件夹5、Plugins插件文件夹6、Editor编辑器文件夹7、默认资源文件夹StandardAssets 2、Resources同步加载1、Re…

Auto-Editor

文章目录 一、关于 Auto-Editor安装系统兼容性版权 二、切割自动切割的方法看看自动编辑器删掉了什么 三、导出到编辑器命名时间线按 Clip 分割 四、手工编辑五、更多的选择 一、关于 Auto-Editor github : https://github.com/WyattBlue/auto-editor (2.8k star – 2408)主页…

ubuntu 20.04系统安装pytorch

1.1 安装gcc 安装cuda之前&#xff0c;首先应该安装gcc&#xff0c;安装cuda需要用到gcc&#xff0c;否则报错。可以先使用下方指令在终端查看是否已经安装gcc。 gcc --version 如果终端打印如下则说明已经安装。 如果显示“找不到命令 “gcc”......”使用下方指令安装 su…

阅读笔记5:董超底层视觉之美|时空的交错与融合——论视频超分辨率

原文链接&#xff1a;https://mp.weixin.qq.com/s/pmJ56Y0-dbIlYbHbJyrfAA 1. 多帧超分和时空超分 视频超分的本质就是多帧超分&#xff0c;多帧超分的历史远早于视频超分。 在早期&#xff0c;Super Resolution专指多帧超分&#xff0c;因为只有多帧超分才能补充进入真实的信…

Golang | Leetcode Golang题解之第368题最大整除子集

题目&#xff1a; 题解&#xff1a; func largestDivisibleSubset(nums []int) (res []int) {sort.Ints(nums)// 第 1 步&#xff1a;动态规划找出最大子集的个数、最大子集中的最大整数n : len(nums)dp : make([]int, n)for i : range dp {dp[i] 1}maxSize, maxVal : 1, 1fo…

对讲模块升级的重要性-OTA空中升级与串口升级

在现代通信设备的设计中&#xff0c;灵活的升级能力已成为评估模块性能的重要标准。无论是在开发过程中&#xff0c;还是在产品的生命周期内&#xff0c;支持OTA和串口升级的模块可以极大地提高设备的可维护性和适应性。 SA618F30&#xff0c;作为一款高性价比、高集成度的大功…

SSRF 302跳转攻击redis写入ssh公钥实现远程登录

目录 SSRF漏洞 SSRF攻击Redis 302跳转 漏洞复现&#xff1a; index.html: index.php: 攻击步骤&#xff1a; 1.生成ssh公钥数据&#xff1a; 2.用SSH公钥数据伪造Redis数据&#xff1a; 3.在自己的服务器上写302跳转&#xff1a; 4.最后尝试在.ssh目录下登录&#…

Golang | Leetcode Golang题解之第371题两整数之和

题目&#xff1a; 题解&#xff1a; func getSum(a, b int) int {for b ! 0 {carry : uint(a&b) << 1a ^ bb int(carry)}return a }

MySQL主从复制之GTID模式

目录 1 MySQL 主从复制 GTID 模式介绍 2 传统复制模式与GTID复制模式的区别 3 GTID模式核心参数 4 GTID 实现自动复制原理 4.1 GTID基本概念 4.2 GTID复制流程 5 GTID 实现自动定位 5.1 配置 my.cnf 5.2 配置 SLAVE 实现自动定位 5.3 测试 6 GTID 模式 故障转移的方法流程 6.1…

如何使用ssm实现宠物领养系统+vue

TOC ssm103宠物领养系统vue 课题背景 在当今的社会&#xff0c;可以说是信息技术的发展时代&#xff0c;在社会的方方面面无不涉及到各种信息的处理。信息是人们对客观世界的具体描述&#xff0c;是人们进行交流与联系的重要途径。人类社会就处在一个对信息进行有效合理的加…

mysql数据库----简单认识库的操作

目录 1.区分概念 2.什么是数据库 3.数据库的创建和销毁 4.数据库编码初识 5.查询系统默认编码配置 6.两个查询编码表的指令 7.创建指定编码的数据库 8.不同编码的区别 第一个编码方式&#xff1a; 第二个编码方式&#xff1a; 查询结果说明&#xff1a; 9.数据库的增…

QT Quick QML 网络助手——TCP客户端

GitHub 源码: QmlLearningPro &#xff0c;选择子工程 Nettools.pro QML 其它文章请点击这里: QT QUICK QML 学习笔记 ● 运行效果&#xff1a; 左侧为常用的网络调试工具&#xff0c;右侧为本项目 UI 效果&#xff0c;前端使用 QML &#xff0c;后端使用C &#xff…

ArkTs之:数据懒加载——LazyForEach的用法

官方描述 LazyForEach从提供的数据源中按需迭代数据&#xff0c;并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach&#xff0c;框架会根据滚动容器可视区域按需创建组件&#xff0c;当组件滑出可视区域外时&#xff0c;框架会进行组件销毁回收以降低内存占…

我在某日重新下载了idea

# 1 Maven设置 2 字体样式,字体颜色 3 插件 1,fitten code和通义灵码 2,one dark theme主题 3,mybatisX 4,Rainbow Brackets 5,Key Promoter X 设置 自动导入包

Ps:首选项 - 常规

Ps菜单&#xff1a;编辑/首选项 Edit/Preferences 快捷键&#xff1a;Ctrl K Photoshop 首选项中的“常规” General选项卡主要用于调整 Photoshop 的整体工作行为和用户体验。这些设置让用户可以根据个人习惯和工作流程定制软件的响应方式和界面布局&#xff0c;从而提高工作…

下载的word中的mathtype公式双击无法打开编辑器

原因分析&#xff1a; 该word中的此公式不是通过word内置的mathtype插入公式的&#xff0c;而是从mathtype编辑器中复制粘贴到word中的。 后者的方式当被其他人下载接收后&#xff0c;无法修改此公式&#xff0c;而且该公式也不能被其他人复制&#xff0c;会报错如下&#xff…

GPT-4o System Card is released

GPT-4o System Card is released, including red teaming, frontier risk evaluations, and other key practices for industrial-strength Large Language Models. https://openai.com/index/gpt-4o-system-card/ 报告链接 企业级生成式人工智能LLM大模型技术、算法及案例实战…