多线程背景下,读请求不断,写请求有机会执行吗?怎么分析?

开发 前端
在多线程编程中,读写锁(Read-Write Lock)的机制是否会导致写请求在持续读请求下无法执行(即 写线程饥饿),取决于锁的具体实现策略和场景特性。

先用代码测试下题目当中的情况(完整代码,可以直接复制用来测试,文末抽奖送书,欢迎参与) 

#include <shared_mutex>
#include <thread>
#include <vector>
#include <iostream>
#include <chrono>

std::shared_mutex rw_mutex;
std::string shared_data;

void reader(int id){
    while (true)
    {
        std::shared_lock lock(rw_mutex);
        std::cout << "Reader " << id << " reads: " << shared_data << std::endl;

        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

void writer(const std::string& new_data){
    
    while (true)
    {
        std::unique_lock lock(rw_mutex);
        shared_data = new_data;
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int main(){
    std::vector<std::thread> readers;
    for (int i = 0; i < 5; ++i) {
        readers.emplace_back(reader, i);
    }
    std::thread writer_thread(writer, "Updated Data");
    for (auto& t : readers) {
        t.join();
    }
    writer_thread.join();

    getchar();
    return0;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.

VS2022 执行代码后,观察控制台输出:

ImageImage

读线程持续打印 “Reader X reads: Updated Data”(初始值为空)。 

写线程在测试期间未能成功获取锁,导致 shared_data 未被更新为 “Updated Data” 

我这里代码故意在读处理中加了延时,读线程长时间持有锁,然后是 5 个读线程,可以发现写请求完全得不到机会来处理。 

这个现象有个专业名词叫:写线程饥饿。 

在多线程编程中,读写锁(Read-Write Lock)的机制是否会导致写请求在持续读请求下无法执行(即 写线程饥饿),取决于锁的具体实现策略和场景特性。以下是逐步分析: 

一、读写锁的基本行为

读写锁的核心规则是 “读共享,写独占”: 

  • 读锁(共享锁):允许多个线程同时读取资源。
  • 写锁(独占锁):同一时间仅允许一个线程写入资源,且写入时会阻塞所有读锁和写锁。

因此,当读锁持续占用时,写锁必须等待所有读锁释放后才能获取。但具体能否执行需结合锁的调度策略分析。 

锁获取优先级:  

无公平性策略:若读锁持续被获取,写锁可能无限等待(饥饿)。  

写优先策略:当写锁请求存在时,后续读锁会被阻塞,直到写锁完成。  

公平策略:交替服务读/写请求,避免单一方饥饿。 

二、写请求能否执行的场景分析

场景 1:读写锁无公平性策略(常见默认实现)

问题:若读线程持续获取读锁(无间隙释放锁),写线程可能永远无法执行。  

示例代码:  

std::shared_mutex rw_mutex;
void reader() {
    while (true) {
        std::shared_lock lock(rw_mutex); // 持续持有读锁
        // 读操作...
    }
}
void writer() {
    std::unique_lock lock(rw_mutex); // 永远无法获取写锁
    // 写操作...
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

结果:写线程饥饿。 

场景 2:读写锁支持写优先

策略:当有写锁等待时,新读锁请求被阻塞,直到写锁完成。  

实现方式:维护写等待标记(如计数器),读锁获取前检查该标记。  

结果:写线程最终能获得锁,但可能牺牲读吞吐量。 

场景 3:读写锁支持公平性

策略:通过队列或时间戳保证读/写请求按到达顺序交替执行。  

示例:Linux 内核的 rw_semaphore 使用公平队列。  

结果:写线程不会饥饿,但并发读性能下降。 

三、C++标准库 std::shared_mutex

我们文章开头的测试出现了写线程饥饿,那么 std::shared_mutex到底是公平性的还是非公平性的?还是说可以设置呢? 

1. C++ 标准的立场

C++ 标准仅定义 std::shared_mutex 的接口和行为规范(如“读锁共享,写锁独占”),但 未规定锁的获取策略是否公平。这意味着: 

公平性(如读/写锁的排队顺序、是否避免饥饿)由具体实现决定。 

不同平台(如 Linux、Windows)或编译器(如 GCC、Clang、MSVC)可能有不同行为。 

2. 常见实现的行为

Linux( GCC/libstdc++)  

底层通常基于 pthread_rwlock_t,默认采用 读优先策略(允许新读请求抢占等待的写锁),可能导致 写线程饥饿。(不同 Linux 发行版或 glibc 版本可能有不同默认行为,需查阅具体文档) 

示例:若某线程持有读锁时,其他读线程可以继续获取读锁,而写线程可能长时间无法获取锁。 

Windows( MSVC)  

底层可能使用 SRWLock(Slim Reader/Writer Lock),其特性是:无优先级保障的竞争式获取,可能但不必然导致写饥饿。 当锁释放时,等待的读/写线程通过竞争获取锁,不保证先到先得。 

写线程可能因竞争失败而饥饿,但实际行为依赖线程调度。 

四、解决方案:避免写饥饿的设计

1. 选择支持写优先的读写锁

手动实现(示例):  

class FairReadWriteLock {
    std::mutex mtx;
    std::condition_variable cv;
    int readers = 0;
    int writers_waiting = 0;
    bool writing = false;
    
public:
    void read_lock(){
        std::unique_lock lock(mtx);
        cv.wait(lock, [this] { 
            return !writing && writers_waiting == 0; // 无写者或等待的写者
        });
        readers++;
    }
    void read_unlock(){
        std::unique_lock lock(mtx);
        if (--readers == 0 && writers_waiting > 0) {
            cv.notify_one(); // 唤醒写者
        }
    }
    void write_lock(){
        std::unique_lock lock(mtx);
        writers_waiting++;
        cv.wait(lock, [this] { 
            return !writing && readers == 0; // 无活动的读/写者
        });
        writers_waiting--;
        writing = true;
    }
    void write_unlock(){
        std::unique_lock lock(mtx);
        writing = false;
        cv.notify_all(); // 唤醒所有读者和写者
    }
};
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.

效果:当有写者等待时,新读者被阻塞,确保写者最终执行。 

2. 使用操作系统级公平锁

Linux:通过 pthread_rwlockattr_setkind_np 设置 PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP。  

pthread_rwlock_t rwlock;
pthread_rwlockattr_t attr;
pthread_rwlockattr_init(&attr);
pthread_rwlockattr_setkind_np(&attr, PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP);
pthread_rwlock_init(&rwlock, &attr);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

3. 业务层限流

在读逻辑中插入条件检查,主动释放锁允许写操作:(此方法依赖线程调度器实现,可能缓解但无法彻底避免饥饿) 

void reader() {
    while (true) {
        {
            std::shared_lock lock(rw_mutex);
            // 读操作...
        }
        std::this_thread::yield(); // 主动让出CPU,增加写者机会
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

六、总结

写请求能否执行:取决于锁实现的公平性策略。  

无公平性策略 → 可能饥饿(需业务层干预)。  

写优先或公平队列 → 可避免饥饿。   

责任编辑:武晓燕 来源: CppPlayer
相关推荐

2011-12-16 20:37:16

webOS

2020-07-29 19:07:59

戴尔

2011-11-03 09:13:52

UbuntuCanonical移动

2020-01-13 14:16:32

区块链应用社交网络

2025-01-21 00:00:00

HashMap死循环数据损坏

2018-05-28 13:31:00

职场阿里巴巴

2019-01-07 20:30:48

NoSQLNewSQL数据库

2012-11-12 09:26:06

.NET多线程

2013-02-25 09:33:38

英特尔移动市场机会

2014-09-09 17:23:54

移动互联网App市场

2013-01-05 14:57:08

2013手机操作系统Ubuntu

2021-01-11 11:14:35

微服务架构调用

2018-04-11 10:51:25

多线程进程主线程

2010-05-25 10:19:18

谷歌苹果

2015-07-29 15:05:01

2013-02-27 10:51:36

2023-11-09 07:23:57

Istio路由分析

2019-03-28 11:00:37

前端网络请求开发

2021-06-05 23:41:47

NET异常 HttpClient

2015-10-22 15:56:27

RFID技术物联网
点赞
收藏

51CTO技术栈公众号