Sharing Data Among Threads Without Contention

简介:

原文地址:http://www.oraclejavamagazine-digital.com/javamagazine/20120304/?pg=56&pm=1&u1=friend  作者 Trisha

The London Multi-Asset Exchange (LMAX) Disruptor is an open source concurrency framework that recently won the 2011 Duke’s Choice Award for Innovative Programming Framework. In this article, I use diagrams to describe what the Disruptor is; what it does; and, to some extent, how it works.

What Is the Disruptor?

The Disruptor is a framework for interthread communication (ITC), that is, the sharing of data among threads. LMAX created the Disruptor as part of its reliable messaging architecture and developed it into an extremely fast way of handing off data between different components.

Using mechanical sympathy (an understanding of how the underlying hardware works), fundamental computer science, and domain-driven design, the Disruptor has evolved into a framework that developers can use to do much of the heavy lifting for concurrent programming.

In many architectures, it is common to use a queue to share data (that is, pass messages) among threads. Figure 1 shows an example of passing messages between stages using a queue. (Each little blue spinner is meant to represent a thread.)

 1

Figure 1

This architecture allows the producing thread (Stage 1 in Figure 1) to move on to the next piece of work if Stage 2 is too busy to accept the data immediately, which provides a way to deal with bursts of traffic in a system. The queue acts as a buffer between the threads.

In its simplest form, the Disruptor can be used in place of the queue in the architecture shown in Figure 2, in which messages are passed between stages using the Disruptor.

 2

Figure 2

The data structure storing the messages is a RingBuffer, implemented as an array. Stage 1 places items into the RingBuffer, and Stage 2 reads items from the RingBuffer.

You’ll see in Figure 2 that each spot in the RingBuffer is indexed by a sequence number, and the RingBuffer tracks the highest (most recent) sequence number, which is the index pointing to the last item in the RingBuffer. This sequence number continually increases as more data is added to the ring.

The key thing about the Disruptor is that it was designed to have zero contention within the framework. This is achieved by following the single-writer principle—only one thing can ever write to a single piece of data. Following this principle eliminates the need for expensive lock or compare and swap (CAS) operations, which is one of the reasons the Disruptor is so fast.

One source of contention has been removed by having the RingBuffer and each EventProcessor track their own sequence numbers. This way, the only thing that ever updates a sequence number is the thing that owns the sequence number. This concept is covered in more detail when I describe writing to and reading from the RingBuffer.

Publishing to the Disruptor

Writing to the RingBuffer is achieved using a two-phase commit. First, Stage 1 (the Publisher) has to determine the next available slot in the RingBuffer, as shown in Figure 3.

 3

Figure 3

The RingBuffer has the sequence number of the most recently written-to slot (Slot 18 in Figure 3) and, therefore, it can determine the next sequence number and, consequently, the corresponding slot in the array.

The RingBuffer determines whether the next slot is free by inspecting the sequence number of any EventProcessor that is reading from the RingBuffer. Figure 4 shows the next slot being claimed.

4

Figure 4

When the Publisher gets the next sequence number, it asks the RingBuffer for the object in that slot. The Publisher can then do what it likes to that object. You can think of the slot as a simple container that you write values into.

All the while, the RingBuffer sequence number is still Slot 18, so anything reading from the RingBuffer will not be able to read the event in Slot 19 while the Publisher is still working on it.

Figure 5 shows changes being committed to the RingBuffer.

 5

Figure 5

Finally, when the Publisher is finished writing things into Slot 19, it tells the RingBuffer to publish the item that is in Slot 19. At this point, the RingBuffer updates its sequence number, and anything that wants to read from the RingBuffer can see the item in Slot 19.

Reading from the RingBuffer

The Disruptor framework includes a BatchEventProcessor that will read events from the RingBuffer for you, but I’m going to outline how that works to highlight the design.

While the Publisher needed to ask the RingBuffer for the number of the next slot to write to, an EventProcessor, which is kind of like a consumer except it doesn’t actually consume (remove) things from the RingBuffer, will track the last sequence number it processed and ask for the next sequence number it wants.

Figure 6 shows the EventProcessor waiting for the next expected sequence number.

 6

Figure 6

Rather than asking the RingBuffer directly for its sequence number, an EventProcessor has a SequenceBarrier do this job. This detail is not important for the case we’re looking at now, but its purpose will become apparent later.

In Figure 6, Stage 2, the EventProcessor has seen up to sequence number 16. It wants the item at Slot 17 next, so it calls waitFor(17) on the SequenceBarrier. Stage 2 can happily hang around waiting for the next sequence number, because it might be that nothing else has written to the RingBuffer. If so, there’s nothing else to process. However, in the case shown in Figure 6, the RingBuffer has been populated up to Slot 18, so waitFor returns the number 18, telling the EventProcessor it can safely read anything up to and including Slot 18, as shown in Figure 7.

 7

Figure 7

This methodology provides some really nice batching behavior, which you can see in the BatchEventProcessor code. The code simply asks the RingBuffer for every item from the next value it needs up to the highest available sequence number.

You can use this batching code by implementing EventHandler. There are examples of how to use batching in the Disruptor performance tests, for example FizzBuzzEventHandler.

 

Is It a Low-Latency Queue? 

Sure, it can be used as such. We have figures from testing early versions of the Disruptor that show how much faster it is than ArrayBlockingQueue for a three-stage pipeline running on a 2.2 GHz Intel Core i7-2720QM processor using Java 1.6.0_25 64-bit on Ubuntu 11.04. Table 1 shows the latency per hop in the pipeline. For more details about this test, see the Disruptor technical paper.

But don’t think that these latency figures mean the Disruptor is a specialist solution for a specific performance niche, because it’s not. 

More Cool Stuff

One of the interesting things about the Disruptor is how it supports graphs of dependencies between system components without incurring any of the cost of contention usually associated with sharing data among threads.

The Disruptor achieves contention free design by adhering to the single writer principle, so each bit of data can be written only by a single thread. However, that doesn’t mean you can’t have multiple readers. And that’s exactly what the Disruptor enables.

The system the Disruptor was originally designed to support had a staged pipeline of events that needed to happen in a particular order, which is not an uncommon requirement in an enterprise application. Figure 8 shows a standard three-stage pipeline.

 8

Figure 8

First, each event is written to disk (journaling) for recovery purposes. Next, the events are replicated to secondary servers. Only after both these stages occur can the system be allowed to proceed with the real business logic.

Doing these tasks sequentially is a logical approach, but it is not the most efficient. Journaling and replication can happen in parallel, because they are not dependent on each other. But the business logic is dependent upon the other two events having taken place. Figure 9 shows how to parallelize the dependencies.

 9

Figure 9

If you wire up the dependencies using the Disruptor, the first two stages (journaling and replication) can read directly from the RingBuffer. As with the simple case in Figure 7, they both use a single sequence barrier to get the next available sequence number from the RingBuffer. They track their own sequence numbers, so they know which events they’ve seen, and can process batches of events using a BatchEventProcessor.

The business logic can also read events from the same RingBuffer, but it is limited to processing only those events that the first two stages have successfully dealt with. This is achieved with a second SequenceBarrier, which is configured to inspect the sequence numbers of the journaling EventProcessor and the replication EventProcessor and return the lower of the two numbers when it is asked for a number that is safe to read up to.

After every EventProcessor has used the sequence barriers to determine the events that are safe to read from the RingBuffer, it can request them from the RingBuffer (see Figure 10).

 10

Figure 10

There’s a lot of reading of different sequence numbers—those from the RingBuffer, the journaling EventProcessor, and the replication EventProcessor—but only one thing can ever increment each sequence number. This ensures no contention on these values among the different threads.

What About Multiple Publishers?

 The Disruptor also supports multiple publishers writing to the RingBuffer. However, because that entails two different things writing to the same place, this is a scenario in which you will get contention. The Disruptor provides different ClaimStrategy types for cases where you have more than one publisher.

Conclusion

I’ve talked at a high level about how the Disruptor is a framework for sharing data among threads in a very high performance fashion. I’ve also described a little about how it works. I didn’t touch on features such as more-advanced event processors and various strategies for claiming a slot in the RingBuffer and waiting for the next sequence in the RingBuffer. The Disruptor is open source—go explore the code!

目录
相关文章
|
10月前
|
安全 算法 小程序
蓝牙信标人员定位从技术核心特点、定位机制、优势到应用场景详解
蓝牙信标采用单向广播机制,具备低功耗(纽扣电池续航5年)、低成本、易部署等优势,支持区域感知与米级精准定位。兼容智能手机,无需专用硬件,广泛适用于医院导引、商超导航、智慧养老等大规模室内场景。如果您想进一步了解蓝牙信标定位的案例,欢迎关注、评论留言~也可搜索维构lbs智能定位。
|
8月前
|
缓存 Java 开发者
吃透 Spring Bean 生命周期:从源码底层到实战落地
本文深度解析Spring 6.2.3 Bean生命周期,涵盖BeanDefinition注册、实例化、属性填充、Aware回调、BeanPostProcessor前后置处理、初始化(@PostConstruct/InitializingBean/init-method)、AOP代理、单例缓存及销毁全流程,结合源码、实战示例与生产问题排查,助你彻底掌握IoC核心机制。
1247 3
|
11月前
|
存储 弹性计算 安全
阿里云服务器计算型c9a实例怎么样?实例特点、收费标准、适用场景及活动价格参考
阿里云服务器c9a实例属于第 9 代 AMD 企业级实例,采用阿里云全新CIPU架构,搭配AMD最新EPYC™ Turin 处理器,可提供稳定的算力输出、更强劲的I/O引擎以及芯片级的安全加固。目前阿里云针对c9a实例推出了首次优惠,本文为大家介绍c9a实例与u1和u2i实例区别,c9a实例的性能特点、收费标准和适用场景以及最新活动价格情况,以供参考和选择。
2720 4
|
8月前
|
安全 Linux Android开发
Cisco Secure Client 5.1.15.287 发布,新增功能简介
Cisco Secure Client 5.1.15.287 发布,新增功能简介
417 1
Cisco Secure Client 5.1.15.287 发布,新增功能简介
|
机器学习/深度学习 人工智能 自然语言处理
通用人工智能的标准是什么,与大模型有何区别?发展到什么程度了?
本文深入解析2025年迅猛发展的通用人工智能(AGI),梳理其核心概念、关键技术与现实应用,对比当前主流大模型的差异,并探讨普通人如何在日常生活与工作中体验和应用这一颠覆性技术,展望AGI带来的社会变革与伦理挑战。
3135 5
|
8月前
|
存储 人工智能 安全
🦞 OpenClaw:当AI开始"做事",我们该如何选择?
本文客观分析开源AI工具OpenClaw(“AI龙虾”):它实现从“对话”到“执行”的突破,可自动处理文件、邮件、数据等任务,提升效率;但高权限也带来真实安全风险,如权限滥用、配置暴露等。普通人应结合自身技术能力与风险承受力理性决策,而非盲目跟风或过度恐慌。(239字)
|
9月前
|
SQL 安全 前端开发
WordPress Fontsy Plugin SQL注入漏洞深度剖析与防御指南
本文深度解析WordPress Fontsy插件CVE-2022-4447高危SQL注入漏洞:影响1.8.6及以下所有版本,未授权即可通过AJAX接口(get_fonts等)触发,CVSS最高9.8分。详述成因(参数未过滤+未用$wpdb->prepare)、危害(数据窃取、服务器沦陷)及紧急卸载、WAF拦截、安全开发等防御方案。(239字)
386 8
QA1
该程序使用栈检测算术表达式中括号是否匹配。支持圆括号、方括号和花括号,检查配对与嵌套顺序,输入以#结束,输出匹配结果或错误信息。
505 5
|
并行计算 前端开发 物联网
全网首发!真·从0到1!万字长文带你入门Qwen2.5-Coder——介绍、体验、本地部署及简单微调
2024年11月12日,阿里云通义大模型团队正式开源通义千问代码模型全系列,包括6款Qwen2.5-Coder模型,每个规模包含Base和Instruct两个版本。其中32B尺寸的旗舰代码模型在多项基准评测中取得开源最佳成绩,成为全球最强开源代码模型,多项关键能力超越GPT-4o。Qwen2.5-Coder具备强大、多样和实用等优点,通过持续训练,结合源代码、文本代码混合数据及合成数据,显著提升了代码生成、推理和修复等核心任务的性能。此外,该模型还支持多种编程语言,并在人类偏好对齐方面表现出色。本文为周周的奇妙编程原创,阿里云社区首发,未经同意不得转载。
34607 18
|
程序员 测试技术 项目管理
项目管理中分工问题如何用RACI责任矩阵解决?
RACI责任矩阵是团队协作中的高效工具,通过明确任务中每个人的角色(负责、批准、咨询、知会),避免责任不清、重复工作和信息不对称等问题。
1659 10
项目管理中分工问题如何用RACI责任矩阵解决?

热门文章

最新文章