锁基本概念
熟悉锁的朋友应该知道,对于排它锁,同一时间只允许一个操作进行,不管这个操作是读还是写。
RCU可以支持一个写操作和多个读操作同时进行。
在上一篇文章中的并发和ABA问题的介绍中,我们提到了要解决ABA中的memory reclamation问题,有一个办法就是使用RCU。
COW的本质就是,在并发的环境中,如果想要更新某个对象,首先将它拷贝一份,在这个拷贝的对象中进行修改,最后把指向原对象的指针指回更新好的对象。
在并发情况下,COW其实还有一个问题没有处理,那就是对于拷贝出来的对象什么时候回收的问题,是不是可以马上将对象回收?有没有其他的线程在访问这个对象?处理这个问题就需要用到对象生命周期的跟踪技术,也就是RCU中的RCU-sync。
所以RCU和COW的关系就是:RCU是由RCU-sync和COW两部分组成的。
1 stomic::load 例子
#include <iostream>
#include <atomic>
#include <thread>
#include <chrono>
std::atomic<int> x(0);
void func()
{
std::cout << "Thread ID: " << std::this_thread::get_id() << ", x = " << x.load() << std::endl;
}
int main()
{
std::thread t1(func);
std::thread t2(func);
/// std::this_thread::sleep_for(std::chrono::seconds(1));
x.store(42);
t1.join();
t2.join();
return 0;
}
细粒度锁
细粒度锁(Fine-Grained Locking)是一种锁的实现方式,它与传统的大锁(Coarse-Grained Locking)相对应。细粒度锁的思想是将锁的范围缩小到最小,也就是在保证线程安全的前提下尽量减小锁的持有时间。这种锁的实现需要根据具体的应用场景进行,可以是对象级别、数据结构级别、甚至是操作级别的锁。
并发性能和吞吐量
并发性能和吞吐量是衡量系统性能的两个重要指标,它们之间存在一定的关系,但也有一些差别。
并发性能指的是系统在处理多个并发请求时的性能表现,即同时处理多个请求的能力。通常通过以下几个指标来衡量:
响应时间(Response Time):指从请求发出到收到响应所需要的时间,响应时间越短,表示系统的并发性能越好。
吞吐量(Throughput):指单位时间内能够处理的请求数量,吞吐量越高,表示系统的并发性能越好。
并发数(Concurrency):指同时处理的请求数量,并发数越高,表示系统的并发性能越好。
CAS 算法
一种常见的无锁算法是 CAS (Compare-And-Swap) 算法,也称为原子操作或者原子指令。
CAS 算法基于原子指令,在同一时刻只允许一个线程进行操作,因此可以避免多个线程同时修改同一块内存造成的数据竞争和冲突问题。CAS 算法的基本思想是,在操作数据之前,先比较内存中的值是否和期望值相等,如果相等则将内存中的值更新为新值,否则不进行任何操作。
下面以 C++ 11 的 std::atomic_compare_exchange_strong 为例,展示 CAS 算法的实现:
#include <atomic>
std::atomic<int> data(0);
void increment() {
int expected = data.load();
while (!data.compare_exchange_strong(expected, expected + 1)) {
expected = data.load();
}
}
在这个例子中,increment 函数通过 std::atomic_load 函数加载 data 的值,并将其存储在变量 expected 中。然后使用一个 while 循环,不断进行 CAS 操作,直到成功为止。如果 CAS 操作失败,则继续加载 data 的值,并将其存储在 expected 中,直到 CAS 操作成功。
这个例子中的 increment 函数可以被多个线程同时调用,而不会出现数据竞争和冲突问题,因为 CAS 算法保证了同一时刻只有一个线程能够成功修改 data 的值。
RCU(Read-Copy-Update)算法
RCU(Read-Copy-Update)算法是一种高性能的无锁并发访问共享数据结构的算法,主要用于多核、多线程的并发编程中。RCU算法主要解决的问题是在无锁(lock-free)的场景下如何实现共享数据结构的正确性和高性能。传统的无锁算法通过CAS(Compare And Swap)操作来实现多个线程之间的互斥访问,但在高并发的场景下,CAS操作会造成CPU总线竞争,降低性能。
RCU算法通过一种称为“读取快照”的技术来解决这个问题。RCU算法通过在共享数据结构中保留一个或多个版本的快照来实现并发访问。每个读线程都可以读取任意版本的快照,而写线程则在新版本中修改数据,而不是在原始版本上修改数据。这种方式可以避免写线程和读线程之间的竞争,从而提高并发性能。
具体来说,RCU算法通过以下步骤来实现并发访问共享数据结构:
读取快照:每个读线程首先读取当前版本的快照,并对其进行操作。
写入新版本:当写线程要修改数据时,它首先创建一个新版本的快照,并将修改操作应用于该新版本中的数据。
更新指针:写线程将一个指向新版本的指针替换原来的指针,以便读线程可以访问新版本的数据。
垃圾回收:当所有读线程都完成其操作后,写线程可以安全地删除旧版本的数据结构。
总之,RCU算法通过读取快照、写入新版本、更新指针和垃圾回收等步骤来实现共享数据结构的无锁并发访问,从而提高并发性能和吞吐量。
ABA(Atomicity、Consistency、Isolation)算法
ABA 算法是一种解决无锁(Lock-Free)算法中的 ABA 问题的方法,其中 ABA 问题指在多线程环境下,线程 A 将某个值从 X 改为了 Y,然后又改回 X,此时线程 B 取得这个值时发现它仍为 X,误以为它未被修改过。
ABA 算法的核心思想是在修改数据时,除了更新值外,还需要给它打上一个标记(tag)。在比较和交换时,除了比较值外还需要比较标记,只有在值和标记都相同时才执行交换。
例如,一个简单的 ABA 问题可以通过 ABA 算法来解决:
假设一个变量的初始值为 10,线程 A 将其改为 20,然后线程 B 将其改为 30,接着线程 A 又将其改回 10,此时线程 C 取得这个值时发现它为 10,但是它无法知道这个值是否被修改过。为了解决这个问题,我们可以引入一个计数器,每次修改值时将计数器加一,然后再将值和计数器打包成一个结构体,这样在比较和交换时就可以同时比较值和计数器了。这个过程就是 ABA 算法的基本思想。