那么,古尔丹,读写锁的代价是什么呢

古尔丹,代价是什么呢

简介

《Linux多线程服务端编程(使用muduo C++网络库)(博文视点出品)》 书中提到 读写锁的开销比互斥锁更大 会更容易产生问题

读写锁可能产生问题:

  • 读取锁可重入 任何可重入的锁都不推荐使用 (可以用过编码规范自我约束不使用读取锁的重入)
  • 读锁里不可以做写入操作 需要其他人熟悉代码遵守

但是古尔丹 读写锁的代价是什么呢

对读写锁的性能测试用例

测试代码:g++ rwlock.cpp -O0 -g -Wall -lpthread

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>

#define READ_THREAD_CNT 20
#define WRITE_THREAD_CNT 4

#define READ_TIMEWAIT_US  (50)
#define WRITE_TIMEWAIT_US (100)

#define READ_LOOP_CNT           50000
#define WRITE_LOOP_CNT          1000

// #define LOCK_WITH_RWLOCK        1
#define LOCK_WITH_SPINLOCK      1
// #define CRITICAL_SECTION_MINI   1

pthread_rwlock_t rwlock;
pthread_mutex_t mutex;
pthread_spinlock_t spinlock;

/**
 * @brief 经典读写锁
 * 
 * @param args 
 * @return * void* 
 */
void* thread_read(void* args) {
    __useconds_t rdwait_us = *(__useconds_t*)args;

    for (int i = 0; i < READ_LOOP_CNT; i++) {
        #if LOCK_WITH_RWLOCK
        if (pthread_rwlock_rdlock(&rwlock) == 0) {
        #elif LOCK_WITH_SPINLOCK
        if (pthread_spin_lock(&spinlock) == 0) {
        #else
        if (pthread_mutex_lock(&mutex) == 0) {
        #endif

            /* 临界区非常小 这里没有延迟 */
            #ifndef CRITICAL_SECTION_MINI
            usleep(rdwait_us);
            #endif

            #if LOCK_WITH_RWLOCK
            pthread_rwlock_unlock(&rwlock);
            #elif LOCK_WITH_SPINLOCK
            pthread_spin_unlock(&spinlock);
            #else
            pthread_mutex_unlock(&mutex);
            #endif
        }

        /** 真正的读取 */
        #ifdef CRITICAL_SECTION_MINI
        usleep(rdwait_us);
        #endif
    }

    return nullptr;
}

void* thread_write(void* args) {
    __useconds_t rdwait_us = *(__useconds_t*)args;

    #if LOCK_WITH_RWLOCK
    if (pthread_rwlock_wrlock(&rwlock) == 0) {
    #elif LOCK_WITH_SPINLOCK
    if (pthread_spin_lock(&spinlock) == 0) {
    #else
    if (pthread_mutex_lock(&mutex) == 0) {
    #endif

        /* 临界区非常小 这里没有延迟 */
        #ifndef CRITICAL_SECTION_MINI
        usleep(rdwait_us);
        #endif

        #if LOCK_WITH_RWLOCK
        pthread_rwlock_unlock(&rwlock);
        #elif LOCK_WITH_SPINLOCK
        pthread_spin_unlock(&spinlock);
        #else
        pthread_mutex_unlock(&mutex);
        #endif
    }

    /** 真正的写入 */
    #ifdef CRITICAL_SECTION_MINI
    usleep(rdwait_us);
    #endif

    return nullptr;
}

int main(int args, char* argv[]) {
    #if LOCK_WITH_RWLOCK
    pthread_rwlock_init(&rwlock, nullptr);
    #elif LOCK_WITH_SPINLOCK
    pthread_spin_init(&spinlock, 0);
    #else
    pthread_mutex_init(&mutex, nullptr);
    #endif

    __useconds_t rdwait_us = READ_TIMEWAIT_US;
    __useconds_t wrwait_us = WRITE_TIMEWAIT_US;

    pthread_t threads_r[READ_THREAD_CNT];
    for (int i = 0; i < READ_THREAD_CNT; i++) {
        pthread_create(&threads_r[i], nullptr, thread_read, &rdwait_us);
    }

    pthread_t threads_w[WRITE_THREAD_CNT];
    for (int i = 0; i < WRITE_THREAD_CNT; i++) {
        pthread_create(&threads_w[i], nullptr, thread_write, &wrwait_us);
    }

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

    // for (int i = 0; i < WRITE_THREAD_CNT; i++) {
    //     pthread_join(threads_w[i], nullptr);
    // }
}

20个读取线程,4个写入线程,测试机器有32个核心
临界区有两个测试方法,临界区有延迟,模拟在临界区内较大的操作,临界区内没有延迟,临界区外有延迟,模拟内存中的对象的读写,如对象的读取,写入时替换对象指针的形式。

注意:
文件读写不属于临界区内的大操作,因为文件读不多线程安全的,而且多线程读取也需要考虑文件的偏移量问题

测试结果

  • 读写锁 无临界区

    ./a.out  1.70s user 4.49s system 114% cpu 5.430 total
    
  • 读写锁 有临界区

    ./a.out  1.81s user 4.37s system 113% cpu 5.423 total
    
  • 互斥锁 无临界区

    ./a.out  1.49s user 4.70s system 114% cpu 5.420 total
    
  • 互斥锁 有临界区

    ./a.out  1.04s user 15.44s system 14% cpu 1:51.35 total
    
  • 自旋锁 无临界区

    ./a.out  1.33s user 4.88s system 114% cpu 5.415 total
    
  • 自旋锁 有临界区

    ./a.out  1197.30s user 0.44s system 198% cpu 10:02.12 total
    

总结

在有临界区的情况下,读写锁是有作用的
但是,有临界区不属于优秀的程序设计,程序设计应该减少资源互斥,尽可能临界区足够
可能是临界区的存在,读锁多线程同时访问起到了作用,读锁的开销≈(互斥锁开销+互斥锁导致的调度开销)

在优秀的多线程设计中,临界区足够小的时候,读写锁确实没有优势,不如互斥锁那么简洁不易产生问题。
有些锁的封装库或是新的语言有互斥锁,但没有提供读写锁,也行也是这个问题

例如下面的伪代码,使用非常小的临界区对共享资源读写,优秀设计

read() {
    {                       # 临界区
        pthread_lock()
        ptr = g_ptr
        pthread_unlock()
    }
    
    read ptr something...
}

write() {
    if is_lock
    {
        ptr = g_ptr.clone   # 在新的变量中修改
        ptr.inser

        pthread_lock()
        g_ptr = ptr         # 替代全局指针 这样临界区非常小
        pthread_unlock()
    }
    else
    {
        /** 虽然 is_lock 到这里可能又被 lock 了。。。 */
        pthread_lock()
        g_ptr.insert
        pthread_unlock()
    }
}

额外验证

调整代码里的线程数量方便观测

#define READ_THREAD_CNT 200
#define WRITE_THREAD_CNT 40

注释所有 usleep(rdwait_us);

当取消线程里的睡眠时,验证上下文切换次数与总时间

  • 读写锁
    ./a.out  15.96s user 0.01s system 199% cpu 8.018 total
    
  • 互斥锁
    ./a.out  0.91s user 13.87s system 201% cpu 7.350 total
    
  • 自旋锁
    ./a.out  133.39s user 0.03s system 198% cpu 1:07.15 total
    

日常时候这个机器的上下文切换次数 大概是2.5w/s

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0      0 3874004 101660 54298452    0    0     0    28 15759 23820  0  0 99  0  0
 0  0      0 3875000 101660 54298544    0    0     0     0 15112 23020  0  0 100  0  0
 0  0      0 3874900 101668 54298544    0    0     0    12 16210 24982  0  0 99  0  0

读写锁和自旋锁时的数据和上面差不多,互斥锁时候则不一样

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
192  0      0 3865540 101636 54298324    0    0     0    36 20042 37480  0  3 96  0  0
199  0      0 3865392 101636 54298324    0    0     0     0 21989 46440  0  4 95  0  0
 4  0      0 3864968 101636 54298424    0    0     0     0 22588 46159  1  5 95  0  0
193  0      0 3865240 101644 54298424    0    0     0    84 20732 42499  1  5 95  0  0
188  0      0 3865868 101644 54298428    0    0     0    68 21320 44422  1  5 95  0  0
175  0      0 3865960 101652 54298452    0    0     0   164 18943 39816  0  4 95  0  0
126  0      0 3865184 101652 54298452    0    0     0     0 20536 43004  0  4 95  0  0

调整读取更多,写入更少时,读写锁才能发挥出一点点的优势

#define READ_THREAD_CNT 200
#define WRITE_THREAD_CNT 1

读写锁 ./a.out  13.66s user 0.02s system 199% cpu 6.863 total
互斥锁 ./a.out  0.90s user 14.06s system 199% cpu 7.489 total

读写均衡时候

#define READ_THREAD_CNT 100
#define WRITE_THREAD_CNT 100

#define READ_LOOP_CNT           50000
#define WRITE_LOOP_CNT          50000

读写锁 ./a.out  6.61s user 0.02s system 203% cpu 3.254 total
互斥锁 ./a.out  6.40s user 0.00s system 200% cpu 3.193 total

读写百万倍差异时候

#define READ_THREAD_CNT 10
#define WRITE_THREAD_CNT 10

#define READ_LOOP_CNT           50000000
#define WRITE_LOOP_CNT          50

读写锁 ./a.out  301.76s user 0.01s system 198% cpu 2:31.68 total
互斥锁 ./a.out  41.19s user 196.56s system 199% cpu 1:59.46 total
  • 总计

互斥锁会明显增加上下文切换次数,但性能表现和读写锁相近,验证读锁的开销 仅仅略微小于 (互斥锁开销+互斥锁导致的调度开销)

智能网联汽车的安全员高级考试涉及多个方面的专业知识,包括但不限于自动驾驶技术原理、车辆传感器融合、网络安全防护以及法律法规等内容。以下是针对该主题的一些核心知识解析: ### 关于智能网联车安全员高级考试的核心内容 #### 1. 自动驾驶分级标准 国际自动机工程师学会(SAE International)定义了六个级别的自动驾驶等级,从L0到L5[^1]。其中,L3及以上级别需要安全员具备更高的应急处理能力。 #### 2. 车辆感知系统的组成与功能 智能网联车通常配备多种传感器,如激光雷达、毫米波雷达、摄像头和超声波传感器等。这些设备协同工作以实现环境感知、障碍物检测等功能[^2]。 #### 3. 数据通信与网络安全 智能网联车依赖V2X(Vehicle-to-Everything)技术进行数据交换,在此过程中需防范潜在的网络攻击风险,例如中间人攻击或恶意软件入侵[^3]。 #### 4. 法律法规要求 不同国家和地区对于无人驾驶测试及运营有着严格的规定,考生应熟悉当地交通法典中有关自动化驾驶部分的具体条款[^4]。 ```python # 示例代码:模拟简单决策逻辑 def decide_action(sensor_data): if sensor_data['obstacle'] and not sensor_data['emergency']: return 'slow_down' elif sensor_data['pedestrian_crossing']: return 'stop_and_yield' else: return 'continue_driving' example_input = {'obstacle': True, 'emergency': False, 'pedestrian_crossing': False} action = decide_action(example_input) print(f"Action to take: {action}") ``` 需要注意的是,“同学”作为特定平台上的学习资源名称,并不提供官方认证的标准答案集;建议通过正规渠道获取教材并参加培训课程来准备此类资格认证考试
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值