Design de alto desempenho da estrutura de programação simultânea Disruptor

Adquira o hábito de escrever juntos! Este é o 7º dia da minha participação no "Nuggets Daily New Plan · April Update Challenge", clique para ver os detalhes do evento .

Arquitetura UML

1 gravação de thread único

A razão pela qual o RingBuffer do Disruptor pode ser completamente livre de travas é também por causa da "escrita de thread único", que é a premissa de todas as "pré-condições". Sem essa premissa, nenhuma tecnologia pode ser completamente livre de travas. O design de estruturas técnicas de alto desempenho, como Redis e Netty, é a ideia central.

2 Otimização da Memória do Sistema - Barreira de Memória

Para alcançar o lock-free, outra tecnologia chave é necessária: barreira de memória.

Correspondente à linguagem Java, é a variável valotile e acontece antes da semântica.

Veja: Memory Barrier - smp_wmb()/smp_rmb() do Linux Kernel do sistema: como o kfifo do Linux: smp_wmb(), tanto a leitura quanto a gravação subjacentes usam o smp_wmb github.com/opennetwork do Linux…

3 Otimização de cache do sistema - Elimine o compartilhamento falso

O sistema de cache é armazenado em unidades de linhas de cache. Uma linha de cache é uma potência inteira de 2 bytes consecutivos, normalmente 32-256 bytes. O tamanho de linha de cache mais comum é de 64 bytes.

Quando vários encadeamentos modificam variáveis ​​independentes umas das outras, se essas variáveis ​​compartilharem a mesma linha de cache, isso afetará inadvertidamente o desempenho um do outro, que é um compartilhamento falso.

Núcleo: Sequência

Pode ser considerado como um AtomicLong, que é usado para identificar o progresso. Também evita o falso compartilhamento de caches de CPU entre diferentes Sequências (Flase Sharing).

如下设计保证保存的 value 永远在一个缓存行中。(8 个long,正好 64 字节),空间换时间。这些变量就是没有实际意义,只是帮助我们进行缓存行填充(Padding Cache Line),使得我们能够尽可能地用上CPU高速缓存(CPU Cache) 若访问内置在CPU的L1 Cache或L2 Cache,访问延时是内存的1/15乃至1/100。而内存访问速度远慢于CPU。想追求极限性能,需尽可能多从CPU Cache拿数据,而非从内存。

CPU Cache装载内存里的数据,不是一个个字段加载,而是加载整个缓存行。 如定义长度64的long类型数组,则数据从内存加载到CPU Cache,不是一个个数组元素加载,而是一次性加载固定长度的一个缓存行。

64位Intel CPU计算机的缓存行通常64个字节(Bytes)。一个long数据需8字节,所以一下会加载8个long数据。 即一次加载数组里面连续的8个数值。这样的加载使得遍历数组元素时,会很快。因为后面连续7次的数据访问都会命中缓存,无需重新从内存里读取数据。

但不使用数组,而使用单独变量时,这就出问题了。 Disruptor RingBuffer(环形缓冲区)定义了RingBufferFields类,里面有indexMask和其他几个变量存放RingBuffer的内部状态信息。 CPU在加载数据时,自然也会把这个数据从内存加载到高速缓存。 但这时,高速缓存除了这个数据,还会加载这个数据前后定义的其他变量

这时,问题就来了,Disruptor是个多线程的服务器框架,在这个数据前后定义的其他变量,可能会被多个不同线程更新、读取数据。这些写入及读取的请求,会来自不同 CPU Core。于是,为保证数据的同步更新,不得不把CPU Cache里的数据,重新写回内存或重新从内存里加载数据。

这些CPU Cache的写回和加载,都不是以一个变量作为单位。这些都是以整个Cache Line作为单位。 所以,当INITIAL_CURSOR_VALUE 前后的那些变量被写回到内存时,这个字段自己也写回到了内存,这个常量的缓存也就失效了。 当要再次读取这个值时,要再重新从内存读取。这就意味着,读取速度大大变慢。 对此,Disruptor利用了缓存行填充,在 RingBufferFields里面定义的变量的前后,分别定义了7个long类型的变量:

  • 前面7个来自继承的 RingBufferPad 类
  • 后面7个直接定义在 RingBuffer 类

这14个变量无任何实际用途。我们既不读他们,也不写他们。

而RingBufferFields里面定义的这些变量都是final,第一次写入后就不会再修改。 所以,一旦它被加载到CPU Cache后,只要被频繁读取访问,就不会再被换出Cache。这意味着,对于该值的读取速度,会一直是CPU Cache的访问速度,而非内存的访问速度。

使用RingBuffer,利用缓存和分支预测

这利用CPU Cache的性能的思路,贯穿整个Disruptor。Disruptor整个框架,就是个高速的生产者-消费者模型(Producer-Consumer)下的队列。

O produtor continua produzindo novas tarefas pendentes para a fila e o consumidor continua processando essas tarefas da fila. Para implementar uma fila, o mais adequado é uma lista encadeada. Apenas mantenha a cabeça e a cauda da lista vinculada. O produtor só precisa continuar inserindo novos nós até o final da cadeia, e o consumidor só precisa continuar retirando o nó mais antigo do cabeçote para processamento. LinkedBlockingQueue pode ser usado diretamente no padrão produtor-consumidor. Disruptor não usa LinkedBlockingQueue, mas usa uma estrutura de dados RingBuffer.A camada inferior deste RingBuffer é uma matriz de comprimento fixo. Comparado com uma lista encadeada, os dados de um array terão localidade espacial na memória.

Vários elementos consecutivos do array serão carregados no cache da CPU ao mesmo tempo, de modo que a velocidade de acesso e travessia será mais rápida. A maioria dos dados de cada nó na lista vinculada não aparecerá no espaço de memória adjacente e, naturalmente, não terá a vantagem de acessar continuamente os dados do cache depois que toda a linha de cache for carregada.

Outra grande vantagem do acesso transversal de dados é que a previsão de ramificação no nível da CPU será muito precisa. Isso nos permite fazer uso mais eficiente do pipeline de vários estágios na CPU, e nosso programa será executado mais rapidamente. Se você não se lembrar do princípio desta parte, você pode voltar e revisar o conteúdo da previsão de desvio na Aula 25.

4 Otimização de Algoritmo - Mecanismo de Cerca de Número de Sequência

Quando entregamos eventos de produtores, sempre usamos:

 long sequence = ringBuffer.next();
复制代码

No Disruptor 3.0, SequenceBarrier e Sequence são usados ​​juntos. Coordena e gerencia o ritmo de trabalho dos consumidores e produtores, evitando o uso de fechaduras e CAS.

  • O valor do número de série do consumidor deve ser menor que o valor do número de série do produtor
  • O valor do número de série do consumidor deve ser menor que o valor do número de série de seu consumidor predecessor (dependência)
  • O número de série do produtor não pode ser maior que o menor número de série do consumidor
  • Para evitar que o produtor seja muito rápido, ele cobrirá as mensagens que ainda não foram consumidas.

SingleProducerSequencerPad#next

     /**
      * @see Sequencer#next(int)
      */
     @Override
     public long next(int n) // 1
     {
         if (n < 1) // 初始值:sequence = -1
         {
             throw new IllegalArgumentException("n must be > 0");
         }
     // 语义级别的
     // nextValue为SingleProducerSequencer的变量
         long nextValue = this.nextValue;
 ​
         long nextSequence = nextValue + n;
         // 用于判断当前序号是否绕过整个 ringbuffer 容器
         long wrapPoint = nextSequence - bufferSize;
         // 用于缓存优化
         long cachedGatingSequence = this.cachedValue;
 ​
         if (wrapPoint > cachedGatingSequence || cachedGatingSequence > nextValue)
         {
             long minSequence;
             while (wrapPoint > (minSequence = Util.getMinimumSequence(gatingSequences, nextValue)))
             {
                 LockSupport.parkNanos(1L); // TODO: Use waitStrategy to spin?
             }
 ​
             this.cachedValue = minSequence;
         }
 ​
         this.nextValue = nextSequence;
 ​
         return nextSequence;
     }
复制代码

referir-se

Acho que você gosta

Origin juejin.im/post/7083848134157140005
Recomendado
Clasificación