关于double-check 和barrier

本文探讨了多线程环境下复杂对象的懒加载初始化问题,介绍了双检锁机制的实现方式,并讨论了如何确保线程安全及提高性能。

参考:http://blog.sina.com.cn/s/blog_597a437101011o66.html 最后的barrier部分自己补充的。

多线程问题也常常和一种lazy-initialize的设计模式联系在一起。在这里就会慢慢引出double-check。lazy-initialize讲的是,对于一些特别复杂的对象,让程序在第一次调用它的时候再对它进行初始化,而且保证仅仅初始化一次。
首先想到的设计是这样的:
Class A
{
private ComplexClass _result = null;
public ComplexClass GetResult()
{
if(_result == null)
{
_result = new ComplexClass();
}
return _result;
}
}
但是这样有一个问题。ComplexClass的构造过程较长的话,当第一个线程还在进行ComplexClass构造的时候,_result可能是null,也可能指向了一个尚未初始化完成的对象。这样,要么两个线程初始化了两次ComplexClass,要么第二个线程会返回一个指向不完整对象的引用。所以,在这里需要用到一个锁,如下所示:
ComplexClass GetResult()
{
lock(_lock)
{
if(_result == null)
{
_result = new ComplexClass();
}
}
return _result;
}
这样,虽然多线程的问题解决了,但是每一次需要使用result时都会请求锁,而请求锁对程序的性能是有很大影响的,因此我们在lock的外面再加一层check:
ComplexClass GetResult()
{
if(_result == null)
{
lock(_lock)
{
if(_result == null)
{
_result = new ComplexClass();
}
}
}
return _result;
}
这样,对于所有初始化完成后的请求,就都不用请求锁,而是直接返回_result。也就是说两个if的作用是为了让锁的请求调用开销减小。
但是还是存在一点问题。对于一些编程语言来说,_result = new ComplexClass();这句代码是三个步骤:1:分配动态内存,2:在内存上进行构造函数的调用,3:返回内存地址给指针_result。但是步骤2和步骤3有时候顺序会颠倒。这会使得_result指向一个仅有内存但是还没有完全初始化的对象。如果此时,线程B并发访问,那么线程B会判断_result已经不是null了,而这时其实初始化尚未完成,这时线程B就直接返回了一个部分初始化的对象,会造成程序的崩溃。那么,这个问题怎么解决呢?一般的解决方法是在程序内部再加一个局部变量(标识变量)做一层缓冲:
ComplexClass GetResult()
{
ComplexClass result;
if(_result == null)
{
lock(_lock)
{
if(_result == null)
{
result = new ComplexClass();
_result = result;
}
}
}
return _result;
}
当然,CPU提供的barrier指令也有上述功能,该指令的作用是让barrier前后的指令不会互相越界,就是barrier前面的指令不会跑到后面执行,反之亦然。所以上面的代码也可以再加上一个barrier()。
ComplexClass GetResult()
{
if(_result == null)
{
lock(_lock)
{
if(_result == null)
{
ComplexClass result = new ComplexClass();
barrier();
_result = result;
}
}
}
return _result;
}

这样,上面的问题就彻底解决了~

/* * Inlined fastpath so that allocation functions (kmalloc, kmem_cache_alloc) * have the fastpath folded into their functions. So no function call * overhead for requests that can be satisfied on the fastpath. * * The fastpath works by first checking if the lockless freelist can be used. * If not then __slab_alloc is called for slow processing. * * Otherwise we can simply pick the next object from the lockless free list. */ static __always_inline void *slab_alloc_node(struct kmem_cache *s, gfp_t gfpflags, int node, unsigned long addr) { void *object; struct kmem_cache_cpu *c; struct page *page; unsigned long tid; s = slab_pre_alloc_hook(s, gfpflags); if (!s) return NULL; redo: /* * Must read kmem_cache cpu data via this cpu ptr. Preemption is * enabled. We may switch back and forth between cpus while * reading from one cpu area. That does not matter as long * as we end up on the original cpu again when doing the cmpxchg. * * We should guarantee that tid and kmem_cache are retrieved on * the same cpu. It could be different if CONFIG_PREEMPT so we need * to check if it is matched or not. */ do { tid = this_cpu_read(s->cpu_slab->tid); c = raw_cpu_ptr(s->cpu_slab); } while (IS_ENABLED(CONFIG_PREEMPT) && unlikely(tid != READ_ONCE(c->tid))); /* * Irqless object alloc/free algorithm used here depends on sequence * of fetching cpu_slab's data. tid should be fetched before anything * on c to guarantee that object and page associated with previous tid * won't be used with current tid. If we fetch tid first, object and * page could be one associated with next tid and our alloc/free * request will be failed. In this case, we will retry. So, no problem. */ barrier(); /* * The transaction ids are globally unique per cpu and per operation on * a per cpu queue. Thus they can be guarantee that the cmpxchg_double * occurs on the right processor and that there was no operation on the * linked list in between. */ object = c->freelist; page = c->page; if (unlikely(!object || !node_match(page, node))) { object = __slab_alloc(s, gfpflags, node, addr, c); stat(s, ALLOC_SLOWPATH); } else { void *next_object = get_freepointer_safe(s, object); /* * The cmpxchg will only match if there was no additional * operation and if we are on the right processor. * * The cmpxchg does the following atomically (without lock * semantics!) * 1. Relocate first pointer to the current per cpu area. * 2. Verify that tid and freelist have not been changed * 3. If they were not changed replace tid and freelist * * Since this is without lock semantics the protection is only * against code executing on this cpu *not* from access by * other cpus. */ if (unlikely(!this_cpu_cmpxchg_double( s->cpu_slab->freelist, s->cpu_slab->tid, object, tid, next_object, next_tid(tid)))) { note_cmpxchg_failure("slab_alloc", s, tid); goto redo; } prefetch_freepointer(s, next_object); stat(s, ALLOC_FASTPATH); } if (unlikely(gfpflags & __GFP_ZERO) && object) memset(object, 0, s->object_size); slab_post_alloc_hook(s, gfpflags, 1, &object); return object; }
08-06
Linux 内核中的 slab 分配器是用于管理内核对象的高效内存分配机制。在频繁分配释放对象的场景中,slab allocator 的 fastpath 机制能够显著减少锁竞争并提升性能。fastpath 通常是指分配或释放对象时无需加锁即可完成操作的路径,这种机制在 kmalloc kmem_cache_alloc 等接口中得到了广泛应用。 在 fastpath 的实现中,slab 分配器依赖于 per-CPU 的本地缓存(`struct kmem_cache_cpu`),每个 CPU 维护一个指向当前可用 slab 的指针,并维护一个无锁的空闲对象链表(freelist)。这种设计使得大多数分配操作可以在不加锁的情况下完成,从而减少了并发访问时的性能开销。 具体而言,`kmem_cache_alloc` 函数内部调用了 `slab_alloc_node`,该函数首先尝试从当前 CPU 的本地缓存中获取一个空闲对象。如果本地缓存中有空闲对象,则直接返回该对象,而无需进入全局锁保护的路径[^3]。这一过程通过读取 `struct kmem_cache_cpu` 中的 `freelist` 指针实现,且由于每个 CPU 都有独立的本地缓存,因此可以避免多核竞争,实现无锁操作。 在 `struct kmem_cache_cpu` 中,`freelist` 指针指向当前 slab 中的下一个可用对象,`page` 成员指向当前正在使用的 slab 页面。这种设计允许快速访问对象,而无需遍历全局链表或获取全局锁。此外,为了进一步减少锁竞争,SLUB 实现还引入了“per-CPU partial list”的概念,允许每个 CPU 在本地缓存不足时,先尝试从本地的 partial slab 中获取对象,而不是立即访问全局节点的缓存[^2]。 当本地缓存中没有空闲对象时,分配器会进入 slowpath,尝试从全局节点(`kmem_cache_node`)中获取新的 slab。`kmem_cache_node` 结构体中维护了 partial slab 链表空闲 slab 的相关信息。如果全局节点中也没有可用对象,则会调用 `new_slab_objects` 函数,最终通过 `allocate_slab` 分配新的 slab 页面[^4]。 在无锁的 freelist 处理中,SLUB 使用原子操作内存屏障来确保在并发访问下的数据一致性。例如,当多个线程同时从同一个 CPU 的本地缓存中分配对象时,使用原子操作来更新 `freelist` 指针,以防止数据竞争[^5]。 ### 示例代码:fastpath 分配流程 以下是一个简化的 `slab_alloc` 函数流程,展示了 fastpath 的实现逻辑: ```c static __always_inline void *slab_alloc(struct kmem_cache *s, gfp_t gfpflags, unsigned long addr) { void *object; struct kmem_cache_cpu *c = get_cpu_slab(s, smp_processor_id()); if (likely(c->freelist)) { /* Fastpath: 从本地缓存中直接获取对象 */ object = c->freelist; c->freelist = get_next_freelist(object); return object; } /* Slowpath: 本地缓存为空,尝试从全局节点获取 */ return __slab_alloc(s, gfpflags, addr, c); } ``` 在这个简化的实现中,`get_cpu_slab` 获取当前 CPU 的本地缓存,如果 `freelist` 不为空,则直接从中取出一个对象并更新 `freelist` 指针,整个过程无需加锁。只有当本地缓存为空时,才会进入慢速路径,尝试从全局节点中获取新的 slab。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值