返回文章列表

文章

怎么使用读写锁

读写锁(Read-Write Lock),也被称为共享-排他锁(Shared-Exclusive Lock),是并发编程中一种重要的同步机制,旨在解决互斥锁在读多写少场景下的性能瓶颈。

目录
  1. 读写锁(Read-Write Lock),也被称为共享-排他锁(Shared-Exclusive Lock),是并发编程中一种重要的同步机制,旨在解决互斥锁在读多写少场景下的性能瓶颈。它允许更高的并发度,允许多个线程同时读取共享资源,但在写入时则保证独占访问。
  2. 1. 读写锁:What & Why
  3. 1.1. 是什么 (What is it?)
  4. 1.2. 为什么引入 (Why was it introduced?)
  5. 2. 读写锁的实现与支持:How
  6. 2.1. C++
  7. 2.2. Java
  8. 2.3. Go
  9. 2.4. Rust
  10. } ```
  11. 3. 使用读写锁的注意事项
  12. 4. 读写锁的典型应用场景
  13. 📎 参考文章

读写锁(Read-Write Lock),也被称为共享-排他锁(Shared-Exclusive Lock),是并发编程中一种重要的同步机制,旨在解决互斥锁在读多写少场景下的性能瓶颈。它允许更高的并发度,允许多个线程同时读取共享资源,但在写入时则保证独占访问。#

1. 读写锁:What & Why#

1.1. 是什么 (What is it?)#

读写锁是一种优化版的锁机制。它遵循以下核心规则:

  • 读读兼容: 多个读线程可以同时持有读锁,并发读取共享资源。
  • 读写互斥: 当有读线程持有读锁时,写线程不能获取写锁;反之,当写线程持有写锁时,读线程不能获取读锁。
  • 写写互斥: 任何时候都只允许一个写线程持有写锁,独占性地修改共享资源。 这种设计使得在读取操作远多于写入操作的场景下,系统能够实现更高的并发性能,因为它避免了所有读操作也被互斥锁串行化的问题。

1.2. 为什么引入 (Why was it introduced?)#

引入读写锁的主要驱动力是为了解决标准**互斥锁(Mutex)**在特定并发模式下的效率低下问题:

  • 互斥锁的局限性: 传统的互斥锁无论读写都独占资源,即使是多个线程只是想读取数据,也必须排队等待,这在读操作频繁的场景下会严重限制并发性。
  • 性能提升: 读写锁通过允许多个并发读取来突破这一限制,显著提升了在读多写少应用中的性能。

2. 读写锁的实现与支持:How#

读写锁的内部实现通常比简单互斥锁复杂,一般会包含一个或多个互斥量、条件变量、读者计数器以及写者状态标志,以协调读写请求。不同语言的实现机制如下:

2.1. C++#

  • 机制: C++17 标准引入了 std::shared_mutex。它提供 lock() / unlock() 用于写锁,以及 lock_shared() / unlock_shared() 用于读锁。为了保证异常安全和简化资源管理,通常推荐使用 RAII (Resource Acquisition Is Initialization) 风格的锁卫(Lock Guards)std::unique_lock<std::shared_mutex> 用于写锁,而 std::shared_lock<std::shared_mutex> 用于读锁。std::shared_mutex 的实现通常是写者优先的,即当有写线程等待时,后续的读请求会被阻塞,以防止写者饥饿。
  • 示例:

#include #include <shared_mutex> // C++17 #include #include #include #include

std::string shared_data = "Initial data"; std::shared_mutex rw_mutex; // 读写锁实例

void reader(int id) { for (int i = 0; i < 5; ++i) { std::shared_lockstd::shared_mutex lock(rw_mutex); // 获取读锁 std::cout << "Reader " << id << " reads: " << shared_data << std::endl; lock.unlock(); // 释放读锁 (shared_lock会在作用域结束时自动释放,这里显式调用用于演示) std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟读取耗时 } }

void writer(int id) { for (int i = 0; i < 2; ++i) { std::unique_lockstd::shared_mutex lock(rw_mutex); // 获取写锁 shared_data = "Data written by writer " + std::to_string(id) + " at " + std::to_string(i); std::cout << "Writer " << id << " writes: " << shared_data << std::endl; lock.unlock(); // 释放写锁 std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟写入耗时 } }

int main() { std::vectorstd::thread threads;

// 创建3个读线程
for (int i = 0; i < 3; ++i) {
    threads.emplace_back(reader, i);
}

// 创建1个写线程
threads.emplace_back(writer, 0);

// 创建另外2个读线程
for (int i = 3; i < 5; ++i) {
    threads.emplace_back(reader, i);
}

for (auto& t : threads) {
    t.join();
}

std::cout << "Final data: " << shared_data << std::endl;
return 0;

} ```

2.2. Java#

  • 机制: Java 在 java.util.concurrent.locks 包中提供了 ReadWriteLock 接口,其标准库实现是 ReentrantReadWriteLock。它支持重入(Reentrancy),即持有锁的线程可以再次获取同类型的锁。ReentrantReadWriteLock 的实现通常也是写者优先的,以避免写者饥饿问题。你需要通过 readLock()writeLock() 方法分别获取读锁和写锁的 Lock 实例,然后调用它们的 lock()unlock() 方法。
  • 示例:

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockDemo { private String sharedData = "Initial data"; private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock(); private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();

public void read(int id) {
    for (int i = 0; i < 5; ++i) {
        readLock.lock(); // 获取读锁
        try {
            System.out.println("Reader " + id + " reads: " + sharedData);
            Thread.sleep(100); // 模拟读取耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            readLock.unlock(); // 释放读锁
        }
    }
}

public void write(int id) {
    for (int i = 0; i < 2; ++i) {
        writeLock.lock(); // 获取写锁
        try {
            sharedData = "Data written by writer " + id + " at " + i;
            System.out.println("Writer " + id + " writes: " + sharedData);
            Thread.sleep(500); // 模拟写入耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            writeLock.unlock(); // 释放写锁
        }
    }
}

public static void main(String[] args) {
    ReadWriteLockDemo demo = new ReadWriteLockDemo();
    Thread[] threads = new Thread[6];

    // 创建3个读线程
    for (int i = 0; i < 3; ++i) {
        threads[i] = new Thread(() -> demo.read(i), "Reader-" + i);
    }

    // 创建1个写线程
    threads[3] = new Thread(() -> demo.write(0), "Writer-0");

    // 创建另外2个读线程
    for (int i = 4; i < 6; ++i) {
        threads[i] = new Thread(() -> demo.read(i), "Reader-" + i);
    }

    for (Thread t : threads) {
        t.start();
    }

    for (Thread t : threads) {
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    System.out.println("Final data: " + demo.sharedData);
}

} ```

2.3. Go#

  • 机制: Go 语言在 sync 包中提供了 sync.RWMutex。这是一个读写互斥锁,其零值(默认值)即可直接使用。sync.RWMutex 通常采用写者优先策略,防止写者饥饿。它提供 Lock()Unlock() 用于写锁,以及 RLock()RUnlock() 用于读锁。Go 语言的 defer 关键字非常适合用于确保锁的释放。
  • 示例:

package main

import ( "fmt" "sync" "time" )

var sharedData = "Initial data" var rwMutex sync.RWMutex // 读写锁实例

func reader(id int) { for i := 0; i < 5; i++ { rwMutex.RLock() // 获取读锁 fmt.Printf("Reader %d reads: %s\n", id, sharedData) rwMutex.RUnlock() // 释放读锁 time.Sleep(100 * time.Millisecond) // 模拟读取耗时 } }

func writer(id int) { for i := 0; i < 2; i++ { rwMutex.Lock() // 获取写锁 sharedData = fmt.Sprintf("Data written by writer %d at %d", id, i) fmt.Printf("Writer %d writes: %s\n", id, sharedData) rwMutex.Unlock() // 释放写锁 time.Sleep(500 * time.Millisecond) // 模拟写入耗时 } }

func main() { var wg sync.WaitGroup

// 创建3个读协程
for i := 0; i < 3; i++ {
	wg.Add(1)
	go func(id int) {
		defer wg.Done()
		reader(id)
	}(i)
}

// 创建1个写协程
wg.Add(1)
go func(id int) {
	defer wg.Done()
	writer(id)
}(0)

// 创建另外2个读协程
for i := 3; i < 5; i++ {
	wg.Add(1)
	go func(id int) {
		defer wg.Done()
		reader(id)
	}(i)
}

wg.Wait()
fmt.Printf("Final data: %s\n", sharedData)

} ```

2.4. Rust#

  • 机制: Rust 标准库在 std::sync 模块中提供了 RwLock<T>。Rust 的 RwLock 具有强大的所有权和借用检查特性,它将共享数据封装在锁内部。通过 read() 方法获取一个 RwLockReadGuard(读锁),通过 write() 方法获取一个 RwLockWriteGuard(写锁)。这些 Guard 是智能指针,它们会在超出作用域时自动 drop,从而安全地释放锁,提供了出色的恐慌安全(panic safety)。Rust 的 RwLock 通常不保证公平性,可能导致读者饥饿
  • 示例:

use std::sync::{RwLock, Arc}; // Arc用于多线程共享所有权 use std::thread; use std::time::Duration;

fn main() { // RwLock 通常会封装共享数据,并使用Arc进行跨线程共享 let shared_data = Arc::new(RwLock::new(String::from("Initial data")));

let mut handles = vec![];

// 创建3个读线程
for i in 0..3 {
    let data_clone = Arc::clone(&shared_data);
    handles.push(thread::spawn(move || {
        for _ in 0..5 {
            // 获取读锁。read()返回Result,unwrap()处理Ok变体
            let data_guard = data_clone.read().unwrap();
            println!("Reader {} reads: {}", i, *data_guard);
            // data_guard 在这里作用域结束时自动释放锁
            thread::sleep(Duration::from_millis(100)); // 模拟读取耗时
        }
    }));
}

// 创建1个写线程
let data_clone_writer = Arc::clone(&shared_data);
handles.push(thread::spawn(move || {
    for i_val in 0..2 {
        // 获取写锁
        let mut data_guard = data_clone_writer.write().unwrap();
        *data_guard = format!("Data written by writer {} at {}", 0, i_val);
        println!("Writer {} writes: {}", 0, *data_guard);
        // data_guard 在这里作用域结束时自动释放锁
        thread::sleep(Duration::from_millis(500)); // 模拟写入耗时
    }
}));

// 创建另外2个读线程
for i in 3..5 {
    let data_clone = Arc::clone(&shared_data);
    handles.push(thread::spawn(move || {
        for _ in 0..5 {
            let data_guard = data_clone.read().unwrap();
            println!("Reader {} reads: {}", i, *data_guard);
            thread::sleep(Duration::from_millis(100));
        }
    }));
}

for handle in handles {
    handle.join().unwrap();
}

// 最终数据
let final_data = shared_data.read().unwrap();
println!("Final data: {}", *final_data);

} ```#

3. 使用读写锁的注意事项#

尽管读写锁能提升性能,但在使用时仍需谨慎,以避免常见的并发问题:

  1. 死锁 (Deadlock):
    • 读锁升级为写锁: 如果线程在持有读锁的同时尝试获取写锁,而大多数标准库的读写锁不直接支持这种“升级”操作,这极易导致死锁。正确的做法是先释放读锁,再尝试获取写锁。在这期间,数据可能会被其他写者修改,需要应用程序层面的重新验证或处理。
    • 多锁顺序: 如果线程需要获取多个读写锁,务必确保所有线程都遵循一致的锁获取顺序,否则可能陷入死锁。
  2. 饥饿 (Starvation):
    • 读者饥饿:写者优先的读写锁实现中(C++、Java、Go 的标准库默认行为),如果写请求持续不断,写线程可能会一直获取锁,导致等待的读线程长时间无法获取读锁。
    • 写者饥饿:读者优先的读写锁实现中(较少见),如果读请求持续不断,读线程可能会一直获取锁,导致等待的写线程长时间无法获取写锁。
    • 解决方案: 理想情况下应选择支持**公平(Fair)**策略的读写锁实现(如果库提供),或者在应用层面设计退避(backoff)和重试机制。
  3. 性能考量:
    • 开销: 读写锁的内部实现通常比互斥锁更复杂,其获取和释放的开销也更大。如果共享资源是写多读少,或者读写比例接近,使用简单的互斥锁可能反而更高效。
    • 临界区长度: 读写锁的临界区不宜过长。长时间持有读或写锁都会阻塞其他线程,从而降低并发性。
    • 缓存伪共享 (False Sharing): 在多核 CPU 环境下,如果被锁保护的共享数据很小,且与锁本身的数据结构或其他不相关的数据位于同一个 CPU 缓存行(Cache Line)中,可能导致缓存伪共享,从而降低性能。
  4. 异常安全/恐慌安全 (Exception Safety/Panic Safety):
    • 在 C++ 和 Java 中,如果在使用裸锁(lock()/unlock())时发生异常,需要确保在 finally 块或 RAII 机制中正确释放锁,否则会导致死锁。因此,C++ 强制推荐使用 std::unique_lock 和 ****std::shared_lock,Java 推荐使用 try-finally
    • Rust 的 RwLockGuard 和 Go 的 defer 关键字提供了原生的安全机制,确保锁在函数返回或恐慌时自动释放。
  5. 可重入性 (Reentrancy):
    • 只有 Java 的 ReentrantReadWriteLock可重入的,这意味着持有读锁的线程可以再次获取读锁,持有写锁的线程可以再次获取写锁。
    • C++ 的 std::shared_mutex 和 Go 的 sync.RWMutex 通常不是可重入的。尝试重新获取已持有的锁可能会导致死锁,需要开发者明确这种行为。

4. 读写锁的典型应用场景#

读写锁的最佳舞台是那些读操作远多于写操作的共享资源。在这种场景下,它能显著提升系统的并发吞吐量:

  1. 缓存系统 (Caching Systems):
    • 描述: 各种内存缓存(如Web服务器缓存、数据库查询缓存)频繁被查询,但更新或失效相对较少。
    • 应用: 读取缓存数据时加读锁,更新或清理缓存时加写锁。
    • 优点: 允许大量并发读请求同时访问缓存,极大地提高了响应速度和缓存命中率。
  2. 配置管理 (Configuration Management):
    • 描述: 应用程序的配置信息(如数据库连接、服务端口)通常在启动时加载一次,之后被频繁读取,只有在管理员调整配置时才会写入。
    • 应用: 读取配置信息时加读锁,修改配置时加写锁。
    • 优点: 大量并发线程可以同时读取配置,只有配置更新时才有短暂阻塞。
  3. 并发数据结构 (Concurrent Data Structures):
    • 描述: 对于自定义的复杂数据结构,如内存中的链表、树、哈希表,查询(读)操作远多于插入、删除、修改(写)操作。
    • 应用: 遍历、查找等只读操作加读锁;插入、删除、修改等操作加写锁。
    • 优点: 多个线程可以同时进行查询,只有在修改数据结构时才需要排他访问。
  4. 统计计数器或聚合器:
    • 描述: 对某个指标(如在线用户数、商品总销量)进行频繁查询,但每次更新(新用户上线、新订单)的频率相对较低。
    • 应用: 读取计数器值加读锁,更新计数器值加写锁。
    • 优点: 大多数时间可以并发读取,避免互斥锁的瓶颈。
  5. 内存中的字典/映射表:
    • 描述: 维护一个键值对集合,其中查询某个键的值的频率远高于添加、删除或更新键值对的频率。
    • 应用: 查询操作加读锁,修改操作加写锁。
    • 优点: 高效支持高并发的查找请求。 读写锁是并发编程工具箱中的一个强大工具,尤其适用于读密集型的工作负载。然而,正确地选择和使用它,需要对底层机制、语言特性以及潜在的并发问题有深入的理解和考量。

📎 参考文章#

  • 一些引用
  • 引用文章