消费者如何读取数据?

前一篇是生产者的处理,这一篇讲消费者的处理

我们都知道,消费者无非就是不停地从队列中读取数据,处理数据。但是与BlockedQueue不同的是,RingBuffer的消费者不会对队列进行上锁,那它是怎样实现的呢?

概括地说,就是通过CAS原子性地得到一个可消费的序号,然后再根据序号取出数据进行处理。

在看代码之前,我们先把能想到的东西先罗列一下:

1.需要一个尾指针来追踪消费状态

2.如何防止一个数据被多个消费者重复消费?

3.消费速度不能超过生产者,如何限制?

4.当没有可处理数据的时候消费者该做什么,自旋还是挂起等待生产者唤醒?

5.如果4选择挂起,那么如果RingBuffer关闭,如何唤醒消费者以终结线程任务?

6.RingBuffer构造的时候需要传入线程工厂,RingBuffer是如何使用线程的,多个任务使用一个线程调度?

7.消费者何时启动?

好,问题有了,现在我们来看代码,下面是EventProcessor的一个实现,WorkProcessor的部分代码。

public final class WorkProcessor<T>
implements EventProcessor
{
private final AtomicBoolean running = new AtomicBoolean(false); //当前处理器状态
private final Sequence sequence = new Sequence(Sequencer.INITIAL_CURSOR_VALUE); //当前已消费过的最新序号
private final RingBuffer<T> ringBuffer; //保留此引用,方便取数据
private final SequenceBarrier sequenceBarrier; //用于等待下一个最大可用序号,可与多个Processor共用
private final WorkHandler<? super T> workHandler; //实际上的处理器
private final ExceptionHandler<? super T> exceptionHandler;
private final Sequence workSequence; //多个Processor共用的workSequence,可以得到下一个待处理的序号 //....
@Override
public void run()
{
if (!running.compareAndSet(false, true)) //防止run方法重复调用造成的问题
{
throw new IllegalStateException("Thread is already running");
}
sequenceBarrier.clearAlert(); notifyStart(); boolean processedSequence = true;
long cachedAvailableSequence = Long.MIN_VALUE;
long nextSequence = sequence.get();
T event = null;
while (true) //死循环
{
try
{
if (processedSequence)
{
if (!running.get()) //如果检查到已关闭,则唤醒在同一个Barrier上的其他processor线程
{
sequenceBarrier.alert(); //唤醒其他线程
sequenceBarrier.checkAlert(); //抛出异常,终结此线程
}
processedSequence = false;
do
{
//workSequence可能和多个Processor共用
nextSequence = workSequence.get() + 1L;
//这个sequence才是当前处理器处理过的序号,生产者判断尾指针的时候就是按照这个来的,这个就是gatingSequence
//拿到下一个新序号的时候,说明workSequence前一个数据已经处理过了
sequence.set(nextSequence - 1L);
}
//由于workSequence可能由多个Processor共用,故存在竞争情况,需要使用CAS
while (!workSequence.compareAndSet(nextSequence - 1L, nextSequence));
} //如果没有超过上一次缓存生产者的最大序号,则表明数据可取
if (cachedAvailableSequence >= nextSequence)
{
//取出序号对应位置的数据
event = ringBuffer.get(nextSequence);
//交给handler处理
workHandler.onEvent(event);
processedSequence = true;
}
else
{
//阻塞等待下一个可用的序号
//如果就是nextSequence,就返回nextSequence
//如果可用的大于nextSequence,则返回最新可用的sequence
cachedAvailableSequence = sequenceBarrier.waitFor(nextSequence);
}
}
catch (final TimeoutException e)
{
notifyTimeout(sequence.get());
}
catch (final AlertException ex) //checkAlert()抛出的
{
if (!running.get()) //如果已经结束,则终结循环,线程任务结束
{
break;
}
}
catch (final Throwable ex) //其他异常,则交给异常处理器处理
{
// handle, mark as processed, unless the exception handler threw an exception
exceptionHandler.handleEventException(ex, nextSequence, event);
processedSequence = true;
}
} notifyShutdown(); running.set(false);
}
//... }

 针对问题一:需要一个尾指针来追踪消费状态

你们注意到代码中有两个Sequence,workSequence和sequence。为啥需要两个呢?

workSequence消费者使用的最新序号(该序号的数据未被处理过,只是被消费者标记成可消费);而sequence序号的数据则是被消费过的,这个序号正是前一篇中的gatingSequence。

针对问题二:如何防止一个数据被多个消费者重复消费?

问题二的解决方案就是WorkPool,即让多个WorkProcessor共用一个workSequence,这样它们就会竞争序号,一个序号只能被消费一次。

public final class WorkerPool<T>
{
private final AtomicBoolean started = new AtomicBoolean(false);
private final Sequence workSequence = new Sequence(Sequencer.INITIAL_CURSOR_VALUE); //从-1开始
private final RingBuffer<T> ringBuffer; //RingBuffer引用,用于构造Processor,取数据
private final WorkProcessor<?>[] workProcessors;
//...
public WorkerPool(
final RingBuffer<T> ringBuffer,
final SequenceBarrier sequenceBarrier,
final ExceptionHandler<? super T> exceptionHandler,
final WorkHandler<? super T>... workHandlers)
{
this.ringBuffer = ringBuffer;
final int numWorkers = workHandlers.length;
workProcessors = new WorkProcessor[numWorkers]; //每个handler构造一个Processor
for (int i = 0; i < numWorkers; i++)
{
workProcessors[i] = new WorkProcessor<>(
ringBuffer,
sequenceBarrier, //共用同一个sequenceBarrier
workHandlers[i],
exceptionHandler,
workSequence); //共用同一个workSequence
}
} //...
} public class Disruptor<T>
{ //...
//为每个WorkHandler构造一个WorkProcessor,再包装成一个WorkerPool
public final EventHandlerGroup<T> handleEventsWithWorkerPool(final WorkHandler<T>... workHandlers)
{
return createWorkerPool(new Sequence[0], workHandlers);
}
//.... }

针对问题三、四:消费速度不能超过生产者,如何限制?当没有可处理数据的时候消费者该做什么,自旋还是挂起等待生产者唤醒?

使用SequenceBarrier,从WorkProcessor的代码中我们可以知道,消费者会缓存上次获取的最大可消费序号,然后在这序号范围内都可以直接竞争。每次获取最小可用序号的时候,则会触发waitStrategy等待策略进行等待。

 final class ProcessingSequenceBarrier implements SequenceBarrier
{
private final WaitStrategy waitStrategy; //等待策略
private final Sequence dependentSequence; //依赖的序号,默认为RingBuffer的sequence
private volatile boolean alerted = false;
private final Sequence cursorSequence; //RingBuffer的sequence
private final Sequencer sequencer; //...
public long waitFor(final long sequence)
throws AlertException, InterruptedException, TimeoutException
{
checkAlert(); //如果已shutdown,则抛出异常,终结任务 //sequence为消费者想要的下一个序号
//cursorSequence为RingBuffer的序号(生产者最新序号)
//dependentSequence默认就是cursorSequence
//特殊情况下,例如消费者B要求只能消费消费者A消费过的,则dependentSequence就会是消费者A的sequence
long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this); if (availableSequence < sequence)
{
return availableSequence;
} //得到的序号是生产者用过的序号,但是该序号对应的数据可能未发布,如果访问未发布的数据,就会影响正确性,因为可能该数据还处于translate阶段
return sequencer.getHighestPublishedSequence(sequence, availableSequence);
}
//...
}

其中等待策略有很多中,常见的就是BlockingWaitStategy,该等待策略会挂起执行线程。当生产者publishEvent的时候,则会调用WaitStrategy#signalAllWhenBlocking()方法唤醒所有等待线程。

public final class BlockingWaitStrategy implements WaitStrategy
{
private final Object mutex = new Object(); //使用对象内置的条件队列 @Override
public long waitFor(long sequence, Sequence cursorSequence, Sequence dependentSequence, SequenceBarrier barrier)
throws AlertException, InterruptedException
{
long availableSequence;
if (cursorSequence.get() < sequence) //当生产者序号小于消费者需要的序号时,挂起等待唤醒
{
synchronized (mutex)
{
while (cursorSequence.get() < sequence) //使用while是为了防止被错误唤醒,所以被唤醒后还会再判断条件是否满足
{
barrier.checkAlert();
mutex.wait();
}
}
} //生产者序号满足后,查看依赖项是否满足
//如果依赖的消费者的序号小于需求序号,即依赖的消费者还没消费过需求序号
//则自旋等待
while ((availableSequence = dependentSequence.get()) < sequence)
{
barrier.checkAlert();
ThreadHints.onSpinWait();
} return availableSequence;
} @Override
public void signalAllWhenBlocking() //开设接口,用于唤醒条件队列内的等待线程
{
synchronized (mutex)
{
mutex.notifyAll();
}
} @Override
public String toString()
{
return "BlockingWaitStrategy{" +
"mutex=" + mutex +
'}';
}
}

针对问题六、七:RingBuffer构造的时候需要传入线程工厂,RingBuffer是如何使用线程的,多个任务使用一个线程调度?消费者何时启动?

消费者随Disruptor启动,Disruptor启动时会从ConsumerRepository中取出Consumer,提交给Executor执行。

public RingBuffer<T> start()
{
checkOnlyStartedOnce();
for (final ConsumerInfo consumerInfo : consumerRepository)
{
consumerInfo.start(executor);
} return ringBuffer;
}

其中,在新版的Disruptor中,不建议使用外部传入的Executor,而是只传ThreadFactory,然后由内部构造一个Executor,就是BasicExecutor。它的实现就是每次提交的任务都创建一个新的线程负责。所以它的线程模型就是一个消费者一个线程。

public class Disruptor<T>
{
//...
public Disruptor(final EventFactory<T> eventFactory, final int ringBufferSize, final ThreadFactory threadFactory)
{
this(RingBuffer.createMultiProducer(eventFactory, ringBufferSize), new BasicExecutor(threadFactory));
}
//...
} public class BasicExecutor implements Executor
{
private final ThreadFactory factory;
private final Queue<Thread> threads = new ConcurrentLinkedQueue<>(); public BasicExecutor(ThreadFactory factory)
{
this.factory = factory;
} @Override
public void execute(Runnable command)
{
//每提交一个任务就新建一个新的线程处理这个任务
final Thread thread = factory.newThread(command);
if (null == thread)
{
throw new RuntimeException("Failed to create thread to run: " + command);
} thread.start(); threads.add(thread);
}
//...
}

【源码】RingBuffer(二)——消费者的更多相关文章

  1. 多线程之美8一 AbstractQueuedSynchronizer源码分析<二>

    目录 AQS的源码分析 该篇主要分析AQS的ConditionObject,是AQS的内部类,实现等待通知机制. 1.条件队列 条件队列与AQS中的同步队列有所不同,结构图如下: 两者区别: 1.链表 ...

  2. Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题

    4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...

  3. Netty 源码(二)NioEventLoop 之 Channel 注册

    Netty 源码(二)NioEventLoop 之 Channel 注册 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) 一 ...

  4. 框架-springmvc源码分析(二)

    框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...

  5. Zookeeper 源码(二)序列化组件 Jute

    Zookeeper 源码(二)序列化组件 Jute 一.序列化组件 Jute 对于一个网络通信,首先需要解决的就是对数据的序列化和反序列化处理,在 ZooKeeper 中,使用了Jute 这一序列化组 ...

  6. 一点一点看JDK源码(二)java.util.List

    一点一点看JDK源码(二)java.util.List liuyuhang原创,未经允许进制转载 本文举例使用的是JDK8的API 目录:一点一点看JDK源码(〇) 1.综述 List译为表,一览表, ...

  7. Tomcat源码分析二:先看看Tomcat的整体架构

    Tomcat源码分析二:先看看Tomcat的整体架构 Tomcat架构图 我们先来看一张比较经典的Tomcat架构图: 从这张图中,我们可以看出Tomcat中含有Server.Service.Conn ...

  8. 十、Spring之BeanFactory源码分析(二)

    Spring之BeanFactory源码分析(二) 前言 在前面我们简单的分析了BeanFactory的结构,ListableBeanFactory,HierarchicalBeanFactory,A ...

  9. Mybatis源码解析(二) —— 加载 Configuration

    Mybatis源码解析(二) -- 加载 Configuration    正如上文所看到的 Configuration 对象保存了所有Mybatis的配置信息,也就是说mybatis-config. ...

  10. Vue源码分析(二) : Vue实例挂载

    Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...

随机推荐

  1. stand up meeting 12/8/2015

    part 组员 今日工作 工作耗时/h 明日计划 工作耗时/h UI 冯晓云  --------------    --  -----------  -- PDF Reader 朱玉影         ...

  2. Hash记录字符串

    Hash记录字符串模板: mod常常取1e9+7,base常常取299,,127等等等....有的题目会卡Hash,因为可能会有两个不同的Hash但却有相通的Hash值...这个时候可以用双Hash来 ...

  3. 改善 Python 程序的 91 个建议

    1.引论 建议1:理解Pythonic概念—-详见Python中的<Python之禅> 建议2:编写Pythonic代码 避免不规范代码,比如只用大小写区分变量.使用容易混淆的变量名.害怕 ...

  4. 【轮询】【ajax】【js】【spring boot】ajax超时请求:前端轮询处理超时请求解决方案 + spring boot服务设置接口超时时间的设置

    场景描述: ajax设置timeout在本机测试有效,但是在生产环境等外网环境无效的问题 1.ajax的timeout属性设置 前端请求超时事件[网络连接不稳定时候,就无效了] var data = ...

  5. XSS Cheat Sheet(basics and advanced)

    XSS Cheat Sheet BASICS HTML注入 当输入位于HTML标记的属性值内或标记的外部(下一种情况中描述的标记除外)时使用.如果输入在HTML注释中,则在payload前加上&quo ...

  6. ThinkPHP3.2自定义配置和加载

    有时候我们会有一些规则定义每个数字对应的实际内容,比如说在下拉菜单的时候: <select name="reasonAndType" id=""> ...

  7. 2019-2020-1 20199325《Linux内核原理与分析》第五周作业

    第五周作业主要是选择一个系统调用(13号系统调用time除外),使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用,在实验楼Linux虚拟机环境下完成实验. 系统调用的列表参见 http ...

  8. RedHat 的 crontab

    Chapter 39. Automated Tasks In Linux, tasks can be configured to run automatically within a specifie ...

  9. 获取 ProgramData 文件夹路径

    ]; if (SHGetFolderPathA( NULL, CSIDL_COMMON_STARTUP, NULL, , startUpDir) != S_OK) { printf("SHG ...

  10. WMware中Ubuntu系统安装VMware tools

    在VMware的虚拟机中安装完ubuntu之后,继续安装VMware tools. 一般情况下,这时都有光驱的图标,点开就能找到"VMwareTools-10.0.10-4301679.ta ...